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,2665 @@
|
|
|
1
|
+
from PyQt5.QtWidgets import (
|
|
2
|
+
QDialog,
|
|
3
|
+
QVBoxLayout,
|
|
4
|
+
QHBoxLayout,
|
|
5
|
+
QGroupBox,
|
|
6
|
+
QRadioButton,
|
|
7
|
+
QLineEdit,
|
|
8
|
+
QLabel,
|
|
9
|
+
QPushButton,
|
|
10
|
+
QSpinBox,
|
|
11
|
+
QDoubleSpinBox,
|
|
12
|
+
QGridLayout,
|
|
13
|
+
QFormLayout,
|
|
14
|
+
QDialogButtonBox,
|
|
15
|
+
QComboBox,
|
|
16
|
+
QCheckBox,
|
|
17
|
+
QFileDialog,
|
|
18
|
+
QMessageBox,
|
|
19
|
+
QListWidget,
|
|
20
|
+
QListWidgetItem,
|
|
21
|
+
QPlainTextEdit,
|
|
22
|
+
QButtonGroup,
|
|
23
|
+
QWidget,
|
|
24
|
+
QProgressDialog,
|
|
25
|
+
QFrame,
|
|
26
|
+
)
|
|
27
|
+
from PyQt5.QtGui import QIcon
|
|
28
|
+
from PyQt5.QtCore import Qt, QSize
|
|
29
|
+
import pkg_resources
|
|
30
|
+
import numpy as np
|
|
31
|
+
import os
|
|
32
|
+
import multiprocessing
|
|
33
|
+
import glob
|
|
34
|
+
from PyQt5.QtWidgets import QApplication
|
|
35
|
+
import uuid
|
|
36
|
+
import traceback
|
|
37
|
+
import time
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# Standalone function for multiprocessing
|
|
41
|
+
def process_single_file_hpc(args):
|
|
42
|
+
"""Process a single file for HPC conversion - standalone function for multiprocessing
|
|
43
|
+
|
|
44
|
+
Parameters:
|
|
45
|
+
-----------
|
|
46
|
+
args : tuple
|
|
47
|
+
Tuple containing (input_file, output_path, stokes, process_id)
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
--------
|
|
51
|
+
dict
|
|
52
|
+
Result dictionary with processing outcome
|
|
53
|
+
"""
|
|
54
|
+
input_file, output_path, stokes, process_id = args
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
result = {
|
|
58
|
+
"input_file": input_file,
|
|
59
|
+
"output_path": output_path,
|
|
60
|
+
"stokes": stokes,
|
|
61
|
+
"success": False,
|
|
62
|
+
"error": None,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
# Import the function here to ensure we have it in the subprocess
|
|
66
|
+
from .helioprojective import convert_and_save_hpc
|
|
67
|
+
|
|
68
|
+
# Generate a unique file suffix for this process to avoid conflicts
|
|
69
|
+
temp_suffix = f"_proc_{process_id}_{uuid.uuid4().hex[:8]}"
|
|
70
|
+
|
|
71
|
+
# Convert file with unique temp file handling
|
|
72
|
+
success = convert_and_save_hpc(
|
|
73
|
+
input_file,
|
|
74
|
+
output_path,
|
|
75
|
+
Stokes=stokes,
|
|
76
|
+
overwrite=True,
|
|
77
|
+
temp_suffix=temp_suffix,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
result["success"] = success
|
|
81
|
+
return result
|
|
82
|
+
except Exception as e:
|
|
83
|
+
result["error"] = str(e)
|
|
84
|
+
result["traceback"] = traceback.format_exc()
|
|
85
|
+
return result
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class ContourSettingsDialog(QDialog):
|
|
89
|
+
"""Dialog for configuring contour settings with a more compact layout."""
|
|
90
|
+
|
|
91
|
+
def __init__(self, parent=None, settings=None):
|
|
92
|
+
super().__init__(parent)
|
|
93
|
+
self.setWindowTitle("Contour Settings")
|
|
94
|
+
self.settings = settings.copy() if settings else {}
|
|
95
|
+
|
|
96
|
+
# Set stylesheet BEFORE creating widgets so styles apply correctly
|
|
97
|
+
self.setStyleSheet(
|
|
98
|
+
"""
|
|
99
|
+
QGroupBox {
|
|
100
|
+
border: 1px solid #666666;
|
|
101
|
+
border-radius: 25px;
|
|
102
|
+
margin-top: 16px;
|
|
103
|
+
padding: 15px;
|
|
104
|
+
padding-top: 10px;
|
|
105
|
+
font-weight: bold;
|
|
106
|
+
}
|
|
107
|
+
QGroupBox::title {
|
|
108
|
+
subcontrol-origin: margin;
|
|
109
|
+
subcontrol-position: top left;
|
|
110
|
+
left: 15px;
|
|
111
|
+
top: 2px;
|
|
112
|
+
padding: 2px 12px;
|
|
113
|
+
background-color: palette(window);
|
|
114
|
+
border-radius: 15px;
|
|
115
|
+
}
|
|
116
|
+
QLineEdit {
|
|
117
|
+
padding: 5px;
|
|
118
|
+
border: 1px solid #666666;
|
|
119
|
+
border-radius: 8px;
|
|
120
|
+
}
|
|
121
|
+
QLineEdit:disabled {
|
|
122
|
+
background-color: #555555;
|
|
123
|
+
color: #888888;
|
|
124
|
+
}
|
|
125
|
+
QComboBox {
|
|
126
|
+
padding: 5px;
|
|
127
|
+
border: 1px solid #666666;
|
|
128
|
+
border-radius: 8px;
|
|
129
|
+
}
|
|
130
|
+
QComboBox:disabled {
|
|
131
|
+
background-color: #555555;
|
|
132
|
+
color: #888888;
|
|
133
|
+
}
|
|
134
|
+
QRadioButton:disabled {
|
|
135
|
+
color: #888888;
|
|
136
|
+
}
|
|
137
|
+
QSpinBox, QDoubleSpinBox {
|
|
138
|
+
border-radius: 8px;
|
|
139
|
+
}
|
|
140
|
+
QSpinBox:disabled, QDoubleSpinBox:disabled {
|
|
141
|
+
background-color: #555555;
|
|
142
|
+
color: #888888;
|
|
143
|
+
}
|
|
144
|
+
"""
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
self.setup_ui()
|
|
148
|
+
|
|
149
|
+
def setup_ui(self):
|
|
150
|
+
|
|
151
|
+
main_layout = QVBoxLayout(self)
|
|
152
|
+
main_layout.setSpacing(10)
|
|
153
|
+
main_layout.setContentsMargins(10, 10, 10, 10)
|
|
154
|
+
|
|
155
|
+
# Top row: Source selection and Stokes parameter side by side
|
|
156
|
+
top_layout = QHBoxLayout()
|
|
157
|
+
top_layout.setSpacing(10)
|
|
158
|
+
|
|
159
|
+
# Source selection group
|
|
160
|
+
source_group = QGroupBox("Contour Source")
|
|
161
|
+
source_layout = QVBoxLayout(source_group)
|
|
162
|
+
source_layout.setSpacing(10)
|
|
163
|
+
source_layout.setContentsMargins(10, 15, 10, 10)
|
|
164
|
+
|
|
165
|
+
# Main radio buttons for source selection
|
|
166
|
+
source_radio_layout = QHBoxLayout()
|
|
167
|
+
source_radio_layout.setSpacing(20)
|
|
168
|
+
self.same_image_radio = QRadioButton("Current Image")
|
|
169
|
+
self.external_image_radio = QRadioButton("External Image")
|
|
170
|
+
if self.settings.get("source") == "external":
|
|
171
|
+
self.external_image_radio.setChecked(True)
|
|
172
|
+
else:
|
|
173
|
+
self.same_image_radio.setChecked(True)
|
|
174
|
+
source_radio_layout.addWidget(self.same_image_radio)
|
|
175
|
+
source_radio_layout.addWidget(self.external_image_radio)
|
|
176
|
+
source_radio_layout.addStretch()
|
|
177
|
+
source_layout.addLayout(source_radio_layout)
|
|
178
|
+
|
|
179
|
+
# External image options in a subgroup
|
|
180
|
+
self.external_group = (
|
|
181
|
+
QWidget()
|
|
182
|
+
) # Changed from QGroupBox to QWidget for better visual
|
|
183
|
+
external_layout = QVBoxLayout(self.external_group)
|
|
184
|
+
external_layout.setSpacing(8)
|
|
185
|
+
external_layout.setContentsMargins(20, 0, 0, 0) # Add left indent
|
|
186
|
+
|
|
187
|
+
# Radio buttons for file type selection
|
|
188
|
+
file_type_layout = QHBoxLayout()
|
|
189
|
+
file_type_layout.setSpacing(20)
|
|
190
|
+
self.radio_casa_image = QRadioButton("CASA Image")
|
|
191
|
+
self.radio_fits_file = QRadioButton("FITS File")
|
|
192
|
+
self.radio_casa_image.setChecked(True)
|
|
193
|
+
file_type_layout.addWidget(self.radio_casa_image)
|
|
194
|
+
file_type_layout.addWidget(self.radio_fits_file)
|
|
195
|
+
file_type_layout.addStretch()
|
|
196
|
+
external_layout.addLayout(file_type_layout)
|
|
197
|
+
|
|
198
|
+
# Browse layout
|
|
199
|
+
browse_layout = QHBoxLayout()
|
|
200
|
+
browse_layout.setSpacing(8)
|
|
201
|
+
self.file_path_edit = QLineEdit(self.settings.get("external_image", ""))
|
|
202
|
+
self.file_path_edit.setPlaceholderText("Select CASA image directory...")
|
|
203
|
+
self.file_path_edit.setMinimumWidth(
|
|
204
|
+
250
|
|
205
|
+
) # Set minimum width for better appearance
|
|
206
|
+
|
|
207
|
+
self.browse_button = QPushButton()
|
|
208
|
+
self.browse_button.setObjectName("IconOnlyNBGButton")
|
|
209
|
+
self.browse_button.setIcon(
|
|
210
|
+
QIcon(
|
|
211
|
+
pkg_resources.resource_filename(
|
|
212
|
+
"solar_radio_image_viewer", "assets/browse.png"
|
|
213
|
+
)
|
|
214
|
+
)
|
|
215
|
+
)
|
|
216
|
+
self.browse_button.setIconSize(QSize(24, 24))
|
|
217
|
+
self.browse_button.setToolTip("Browse")
|
|
218
|
+
self.browse_button.setFixedSize(32, 32)
|
|
219
|
+
self.browse_button.clicked.connect(self.browse_file)
|
|
220
|
+
|
|
221
|
+
# Store both icon variants for theme switching
|
|
222
|
+
self.browse_icon_light = QIcon(
|
|
223
|
+
pkg_resources.resource_filename(
|
|
224
|
+
"solar_radio_image_viewer", "assets/browse.png"
|
|
225
|
+
)
|
|
226
|
+
)
|
|
227
|
+
self.browse_icon_dark = QIcon(
|
|
228
|
+
pkg_resources.resource_filename(
|
|
229
|
+
"solar_radio_image_viewer", "assets/browse_light.png"
|
|
230
|
+
)
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
# Set initial icon based on palette
|
|
234
|
+
self._update_browse_icon()
|
|
235
|
+
|
|
236
|
+
self.browse_button.setStyleSheet(
|
|
237
|
+
"""
|
|
238
|
+
QPushButton {
|
|
239
|
+
background-color: transparent;
|
|
240
|
+
border: none;
|
|
241
|
+
padding: 4px;
|
|
242
|
+
}
|
|
243
|
+
QPushButton:hover {
|
|
244
|
+
background-color: #484848;
|
|
245
|
+
}
|
|
246
|
+
QPushButton:pressed {
|
|
247
|
+
background-color: #303030;
|
|
248
|
+
}
|
|
249
|
+
QPushButton:disabled {
|
|
250
|
+
background-color: transparent;
|
|
251
|
+
}
|
|
252
|
+
"""
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
browse_layout.addWidget(self.file_path_edit)
|
|
256
|
+
browse_layout.addWidget(self.browse_button)
|
|
257
|
+
external_layout.addLayout(browse_layout)
|
|
258
|
+
|
|
259
|
+
source_layout.addWidget(self.external_group)
|
|
260
|
+
top_layout.addWidget(source_group)
|
|
261
|
+
|
|
262
|
+
# Stokes parameter group
|
|
263
|
+
stokes_group = QGroupBox("Stokes Parameter")
|
|
264
|
+
stokes_layout = QHBoxLayout(stokes_group)
|
|
265
|
+
stokes_layout.setContentsMargins(10, 15, 10, 10)
|
|
266
|
+
|
|
267
|
+
stokes_label = QLabel("Stokes:")
|
|
268
|
+
stokes_label.setMinimumWidth(55) # Minimum width to prevent cutoff
|
|
269
|
+
self.stokes_combo = QComboBox()
|
|
270
|
+
self.stokes_combo.addItems(
|
|
271
|
+
["I", "Q", "U", "V", "Q/I", "U/I", "V/I", "L", "Lfrac", "PANG"]
|
|
272
|
+
)
|
|
273
|
+
self.stokes_combo.setFixedWidth(80)
|
|
274
|
+
current_stokes = self.settings.get("stokes", "I")
|
|
275
|
+
self.stokes_combo.setCurrentText(current_stokes)
|
|
276
|
+
|
|
277
|
+
stokes_layout.addWidget(stokes_label)
|
|
278
|
+
stokes_layout.addWidget(self.stokes_combo)
|
|
279
|
+
stokes_layout.addStretch()
|
|
280
|
+
|
|
281
|
+
# Set fixed size for stokes group to match source group height
|
|
282
|
+
stokes_group.setFixedHeight(source_group.sizeHint().height())
|
|
283
|
+
stokes_group.setMinimumWidth(200) # Set minimum width
|
|
284
|
+
top_layout.addWidget(stokes_group)
|
|
285
|
+
|
|
286
|
+
main_layout.addLayout(top_layout)
|
|
287
|
+
|
|
288
|
+
# Create button group for CASA/FITS selection
|
|
289
|
+
self.file_type_button_group = QButtonGroup()
|
|
290
|
+
self.file_type_button_group.addButton(self.radio_casa_image)
|
|
291
|
+
self.file_type_button_group.addButton(self.radio_fits_file)
|
|
292
|
+
|
|
293
|
+
# Connect signals for enabling/disabling external options
|
|
294
|
+
self.external_image_radio.toggled.connect(self.update_external_options)
|
|
295
|
+
self.radio_casa_image.toggled.connect(self.update_placeholder_text)
|
|
296
|
+
self.radio_fits_file.toggled.connect(self.update_placeholder_text)
|
|
297
|
+
|
|
298
|
+
# Initially update states
|
|
299
|
+
self.update_external_options(self.external_image_radio.isChecked())
|
|
300
|
+
self.update_placeholder_text()
|
|
301
|
+
|
|
302
|
+
# Middle row: Contour Levels and Appearance side by side
|
|
303
|
+
mid_layout = QHBoxLayout()
|
|
304
|
+
|
|
305
|
+
# Contour Levels group with a form layout
|
|
306
|
+
levels_group = QGroupBox("Contour Levels")
|
|
307
|
+
levels_layout = QFormLayout(levels_group)
|
|
308
|
+
self.level_type_combo = QComboBox()
|
|
309
|
+
self.level_type_combo.addItems(["fraction", "absolute", "sigma"])
|
|
310
|
+
current_level_type = self.settings.get("level_type", "fraction")
|
|
311
|
+
self.level_type_combo.setCurrentText(current_level_type)
|
|
312
|
+
levels_layout.addRow("Level Type:", self.level_type_combo)
|
|
313
|
+
self.pos_levels_edit = QLineEdit(
|
|
314
|
+
", ".join(
|
|
315
|
+
str(level)
|
|
316
|
+
for level in self.settings.get("pos_levels", [0.1, 0.3, 0.5, 0.7, 0.9])
|
|
317
|
+
)
|
|
318
|
+
)
|
|
319
|
+
levels_layout.addRow("Positive Levels:", self.pos_levels_edit)
|
|
320
|
+
self.neg_levels_edit = QLineEdit(
|
|
321
|
+
", ".join(
|
|
322
|
+
str(level)
|
|
323
|
+
for level in self.settings.get("neg_levels", [0.1, 0.3, 0.5, 0.7, 0.9])
|
|
324
|
+
)
|
|
325
|
+
)
|
|
326
|
+
levels_layout.addRow("Negative Levels:", self.neg_levels_edit)
|
|
327
|
+
|
|
328
|
+
# Connect level type change to update default levels
|
|
329
|
+
self.level_type_combo.currentTextChanged.connect(self.on_level_type_changed)
|
|
330
|
+
|
|
331
|
+
mid_layout.addWidget(levels_group)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
# Appearance group with a form layout
|
|
335
|
+
appearance_group = QGroupBox("Appearance")
|
|
336
|
+
appearance_layout = QFormLayout(appearance_group)
|
|
337
|
+
self.color_combo = QComboBox()
|
|
338
|
+
self.color_combo.addItems(
|
|
339
|
+
["white", "black", "red", "green", "blue", "yellow", "cyan", "magenta"]
|
|
340
|
+
)
|
|
341
|
+
current_color = self.settings.get("color", "white")
|
|
342
|
+
self.color_combo.setCurrentText(current_color)
|
|
343
|
+
appearance_layout.addRow("Color:", self.color_combo)
|
|
344
|
+
self.linewidth_spin = QDoubleSpinBox()
|
|
345
|
+
self.linewidth_spin.setRange(0.1, 5.0)
|
|
346
|
+
self.linewidth_spin.setSingleStep(0.1)
|
|
347
|
+
self.linewidth_spin.setValue(self.settings.get("linewidth", 1.0))
|
|
348
|
+
appearance_layout.addRow("Line Width:", self.linewidth_spin)
|
|
349
|
+
self.pos_linestyle_combo = QComboBox()
|
|
350
|
+
self.pos_linestyle_combo.addItems(["-", "--", "-.", ":"])
|
|
351
|
+
current_pos_linestyle = self.settings.get("pos_linestyle", "-")
|
|
352
|
+
self.pos_linestyle_combo.setCurrentText(current_pos_linestyle)
|
|
353
|
+
appearance_layout.addRow("Positive Style:", self.pos_linestyle_combo)
|
|
354
|
+
self.neg_linestyle_combo = QComboBox()
|
|
355
|
+
self.neg_linestyle_combo.addItems(["-", "--", "-.", ":"])
|
|
356
|
+
current_neg_linestyle = self.settings.get("neg_linestyle", "--")
|
|
357
|
+
self.neg_linestyle_combo.setCurrentText(current_neg_linestyle)
|
|
358
|
+
appearance_layout.addRow("Negative Style:", self.neg_linestyle_combo)
|
|
359
|
+
mid_layout.addWidget(appearance_group)
|
|
360
|
+
|
|
361
|
+
main_layout.addLayout(mid_layout)
|
|
362
|
+
|
|
363
|
+
# Bottom row: RMS Calculation Region in a compact grid layout
|
|
364
|
+
rms_group = QGroupBox("RMS Calculation Region")
|
|
365
|
+
rms_layout = QGridLayout(rms_group)
|
|
366
|
+
self.use_default_rms_box = QCheckBox("Use default RMS region")
|
|
367
|
+
self.use_default_rms_box.setChecked(
|
|
368
|
+
self.settings.get("use_default_rms_region", True)
|
|
369
|
+
)
|
|
370
|
+
self.use_default_rms_box.stateChanged.connect(self.toggle_rms_inputs)
|
|
371
|
+
rms_layout.addWidget(self.use_default_rms_box, 0, 0, 1, 4)
|
|
372
|
+
# Arrange X min and Y min side by side, then X max and Y max
|
|
373
|
+
rms_layout.addWidget(QLabel("X min:"), 1, 0)
|
|
374
|
+
self.rms_xmin = QSpinBox()
|
|
375
|
+
self.rms_xmin.setRange(0, 10000)
|
|
376
|
+
self.rms_xmin.setValue(self.settings.get("rms_box", (0, 200, 0, 130))[0])
|
|
377
|
+
rms_layout.addWidget(self.rms_xmin, 1, 1)
|
|
378
|
+
rms_layout.addWidget(QLabel("Y min:"), 1, 2)
|
|
379
|
+
self.rms_ymin = QSpinBox()
|
|
380
|
+
self.rms_ymin.setRange(0, 10000)
|
|
381
|
+
self.rms_ymin.setValue(self.settings.get("rms_box", (0, 200, 0, 130))[2])
|
|
382
|
+
rms_layout.addWidget(self.rms_ymin, 1, 3)
|
|
383
|
+
rms_layout.addWidget(QLabel("X max:"), 2, 0)
|
|
384
|
+
self.rms_xmax = QSpinBox()
|
|
385
|
+
self.rms_xmax.setRange(0, 10000)
|
|
386
|
+
self.rms_xmax.setValue(self.settings.get("rms_box", (0, 200, 0, 130))[1])
|
|
387
|
+
rms_layout.addWidget(self.rms_xmax, 2, 1)
|
|
388
|
+
rms_layout.addWidget(QLabel("Y max:"), 2, 2)
|
|
389
|
+
self.rms_ymax = QSpinBox()
|
|
390
|
+
self.rms_ymax.setRange(0, 10000)
|
|
391
|
+
self.rms_ymax.setValue(self.settings.get("rms_box", (0, 200, 0, 130))[3])
|
|
392
|
+
rms_layout.addWidget(self.rms_ymax, 2, 3)
|
|
393
|
+
main_layout.addWidget(rms_group)
|
|
394
|
+
|
|
395
|
+
# Initialize RMS inputs state
|
|
396
|
+
self.toggle_rms_inputs()
|
|
397
|
+
|
|
398
|
+
# Button box at the bottom
|
|
399
|
+
button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
|
400
|
+
button_box.accepted.connect(self.accept)
|
|
401
|
+
button_box.rejected.connect(self.reject)
|
|
402
|
+
main_layout.addWidget(button_box)
|
|
403
|
+
|
|
404
|
+
def toggle_rms_inputs(self):
|
|
405
|
+
"""Update the enabled state and visual appearance of RMS inputs."""
|
|
406
|
+
enabled = not self.use_default_rms_box.isChecked()
|
|
407
|
+
|
|
408
|
+
# Create a widget list for consistent state management
|
|
409
|
+
rms_inputs = [self.rms_xmin, self.rms_xmax, self.rms_ymin, self.rms_ymax]
|
|
410
|
+
|
|
411
|
+
# Update enabled state for all inputs
|
|
412
|
+
for widget in rms_inputs:
|
|
413
|
+
widget.setEnabled(enabled)
|
|
414
|
+
|
|
415
|
+
# Clear any custom styles - let palette handle colors
|
|
416
|
+
for widget in rms_inputs:
|
|
417
|
+
widget.setStyleSheet("")
|
|
418
|
+
|
|
419
|
+
def update_external_options(self, enabled):
|
|
420
|
+
"""Update the enabled state and visual appearance of external options."""
|
|
421
|
+
#print(f"update_external_options called with enabled={enabled}")
|
|
422
|
+
|
|
423
|
+
# Explicitly disable/enable each widget
|
|
424
|
+
self.radio_casa_image.setEnabled(enabled)
|
|
425
|
+
self.radio_fits_file.setEnabled(enabled)
|
|
426
|
+
self.file_path_edit.setEnabled(enabled)
|
|
427
|
+
self.browse_button.setEnabled(enabled)
|
|
428
|
+
|
|
429
|
+
# Also set the parent group
|
|
430
|
+
self.external_group.setEnabled(enabled)
|
|
431
|
+
|
|
432
|
+
#print(f" radio_casa_image.isEnabled() = {self.radio_casa_image.isEnabled()}")
|
|
433
|
+
#print(f" radio_fits_file.isEnabled() = {self.radio_fits_file.isEnabled()}")
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def _update_browse_icon(self):
|
|
437
|
+
"""Update browse button icon based on current palette (light/dark mode)."""
|
|
438
|
+
# Check if we're in light or dark mode by examining window color
|
|
439
|
+
palette = self.palette()
|
|
440
|
+
window_color = palette.color(palette.Window)
|
|
441
|
+
# If window color is light (high luminance), use dark icon
|
|
442
|
+
luminance = 0.299 * window_color.red() + 0.587 * window_color.green() + 0.114 * window_color.blue()
|
|
443
|
+
if luminance > 128:
|
|
444
|
+
# Light mode - use dark icon
|
|
445
|
+
if hasattr(self, 'browse_icon_dark'):
|
|
446
|
+
self.browse_button.setIcon(self.browse_icon_dark)
|
|
447
|
+
else:
|
|
448
|
+
# Dark mode - use light icon
|
|
449
|
+
if hasattr(self, 'browse_icon_light'):
|
|
450
|
+
self.browse_button.setIcon(self.browse_icon_light)
|
|
451
|
+
|
|
452
|
+
def on_level_type_changed(self, level_type):
|
|
453
|
+
"""Update default levels when level type changes."""
|
|
454
|
+
# Define default levels for each type
|
|
455
|
+
defaults = {
|
|
456
|
+
"fraction": {
|
|
457
|
+
"pos": [0.1, 0.3, 0.5, 0.7, 0.9],
|
|
458
|
+
"neg": [0.1, 0.3, 0.5, 0.7, 0.9]
|
|
459
|
+
},
|
|
460
|
+
"sigma": {
|
|
461
|
+
"pos": [3, 6, 9, 12, 15, 20, 25, 30],
|
|
462
|
+
"neg": [3, 6, 9, 12, 15, 20, 25, 30]
|
|
463
|
+
},
|
|
464
|
+
"absolute": {
|
|
465
|
+
"pos": [50, 100, 500, 1000, 5000, 10000],
|
|
466
|
+
"neg": [50, 100, 500, 1000, 5000, 10000]
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if level_type in defaults:
|
|
471
|
+
pos_levels = defaults[level_type]["pos"]
|
|
472
|
+
neg_levels = defaults[level_type]["neg"]
|
|
473
|
+
self.pos_levels_edit.setText(", ".join(str(l) for l in pos_levels))
|
|
474
|
+
self.neg_levels_edit.setText(", ".join(str(l) for l in neg_levels))
|
|
475
|
+
|
|
476
|
+
def update_placeholder_text(self):
|
|
477
|
+
|
|
478
|
+
if self.radio_casa_image.isChecked():
|
|
479
|
+
self.file_path_edit.setPlaceholderText("Select CASA image directory...")
|
|
480
|
+
else:
|
|
481
|
+
self.file_path_edit.setPlaceholderText("Select FITS file...")
|
|
482
|
+
|
|
483
|
+
def browse_file(self):
|
|
484
|
+
if self.radio_casa_image.isChecked():
|
|
485
|
+
# Select CASA image directory
|
|
486
|
+
directory = QFileDialog.getExistingDirectory(
|
|
487
|
+
self, "Select a CASA Image Directory"
|
|
488
|
+
)
|
|
489
|
+
if directory:
|
|
490
|
+
self.file_path_edit.setText(directory)
|
|
491
|
+
else:
|
|
492
|
+
# Select FITS file
|
|
493
|
+
file_path, _ = QFileDialog.getOpenFileName(
|
|
494
|
+
self, "Select a FITS file", "", "FITS files (*.fits);;All files (*)"
|
|
495
|
+
)
|
|
496
|
+
if file_path:
|
|
497
|
+
self.file_path_edit.setText(file_path)
|
|
498
|
+
|
|
499
|
+
def get_settings(self):
|
|
500
|
+
settings = {}
|
|
501
|
+
settings["source"] = (
|
|
502
|
+
"external" if self.external_image_radio.isChecked() else "same"
|
|
503
|
+
)
|
|
504
|
+
settings["external_image"] = self.file_path_edit.text()
|
|
505
|
+
settings["stokes"] = self.stokes_combo.currentText()
|
|
506
|
+
settings["level_type"] = self.level_type_combo.currentText()
|
|
507
|
+
try:
|
|
508
|
+
pos_levels_text = self.pos_levels_edit.text()
|
|
509
|
+
settings["pos_levels"] = [
|
|
510
|
+
float(level.strip())
|
|
511
|
+
for level in pos_levels_text.split(",")
|
|
512
|
+
if level.strip()
|
|
513
|
+
]
|
|
514
|
+
except ValueError:
|
|
515
|
+
settings["pos_levels"] = [0.1, 0.3, 0.5, 0.7, 0.9]
|
|
516
|
+
try:
|
|
517
|
+
neg_levels_text = self.neg_levels_edit.text()
|
|
518
|
+
settings["neg_levels"] = [
|
|
519
|
+
float(level.strip())
|
|
520
|
+
for level in neg_levels_text.split(",")
|
|
521
|
+
if level.strip()
|
|
522
|
+
]
|
|
523
|
+
except ValueError:
|
|
524
|
+
settings["neg_levels"] = [0.1, 0.3, 0.5, 0.7, 0.9]
|
|
525
|
+
settings["levels"] = settings["pos_levels"]
|
|
526
|
+
settings["use_default_rms_region"] = self.use_default_rms_box.isChecked()
|
|
527
|
+
settings["rms_box"] = (
|
|
528
|
+
self.rms_xmin.value(),
|
|
529
|
+
self.rms_xmax.value(),
|
|
530
|
+
self.rms_ymin.value(),
|
|
531
|
+
self.rms_ymax.value(),
|
|
532
|
+
)
|
|
533
|
+
settings["color"] = self.color_combo.currentText()
|
|
534
|
+
settings["linewidth"] = self.linewidth_spin.value()
|
|
535
|
+
settings["pos_linestyle"] = self.pos_linestyle_combo.currentText()
|
|
536
|
+
settings["neg_linestyle"] = self.neg_linestyle_combo.currentText()
|
|
537
|
+
settings["linestyle"] = settings["pos_linestyle"]
|
|
538
|
+
if "contour_data" in self.settings:
|
|
539
|
+
settings["contour_data"] = self.settings["contour_data"]
|
|
540
|
+
else:
|
|
541
|
+
settings["contour_data"] = None
|
|
542
|
+
return settings
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
class BatchProcessDialog(QDialog):
|
|
546
|
+
def __init__(self, parent=None):
|
|
547
|
+
super().__init__(parent)
|
|
548
|
+
self.setWindowTitle("Batch Processing")
|
|
549
|
+
self.setMinimumWidth(500)
|
|
550
|
+
self.setStyleSheet("background-color: #484848; color: #ffffff;")
|
|
551
|
+
self.image_list = QListWidget()
|
|
552
|
+
self.add_button = QPushButton("Add Image")
|
|
553
|
+
self.remove_button = QPushButton("Remove Selected")
|
|
554
|
+
self.threshold_spin = QSpinBox()
|
|
555
|
+
self.threshold_spin.setRange(1, 9999)
|
|
556
|
+
self.threshold_spin.setValue(10)
|
|
557
|
+
lbl_thresh = QLabel("Threshold:")
|
|
558
|
+
self.run_button = QPushButton("Run Process")
|
|
559
|
+
button_box = QDialogButtonBox(QDialogButtonBox.Close)
|
|
560
|
+
button_box.rejected.connect(self.reject)
|
|
561
|
+
layout = QVBoxLayout(self)
|
|
562
|
+
layout.addWidget(self.image_list)
|
|
563
|
+
ctrl_layout = QHBoxLayout()
|
|
564
|
+
ctrl_layout.addWidget(self.add_button)
|
|
565
|
+
ctrl_layout.addWidget(self.remove_button)
|
|
566
|
+
layout.addLayout(ctrl_layout)
|
|
567
|
+
thr_layout = QHBoxLayout()
|
|
568
|
+
thr_layout.addWidget(lbl_thresh)
|
|
569
|
+
thr_layout.addWidget(self.threshold_spin)
|
|
570
|
+
layout.addLayout(thr_layout)
|
|
571
|
+
layout.addWidget(self.run_button)
|
|
572
|
+
layout.addWidget(button_box)
|
|
573
|
+
self.add_button.clicked.connect(self.add_image)
|
|
574
|
+
self.remove_button.clicked.connect(self.remove_image)
|
|
575
|
+
self.run_button.clicked.connect(self.run_process)
|
|
576
|
+
|
|
577
|
+
def add_image(self):
|
|
578
|
+
directory = QFileDialog.getExistingDirectory(
|
|
579
|
+
self, "Select a CASA Image Directory"
|
|
580
|
+
)
|
|
581
|
+
if directory:
|
|
582
|
+
self.image_list.addItem(directory)
|
|
583
|
+
|
|
584
|
+
def remove_image(self):
|
|
585
|
+
for item in self.image_list.selectedItems():
|
|
586
|
+
self.image_list.takeItem(self.image_list.row(item))
|
|
587
|
+
|
|
588
|
+
def run_process(self):
|
|
589
|
+
threshold = self.threshold_spin.value()
|
|
590
|
+
results = []
|
|
591
|
+
for i in range(self.image_list.count()):
|
|
592
|
+
imagename = self.image_list.item(i).text()
|
|
593
|
+
try:
|
|
594
|
+
from .utils import get_pixel_values_from_image
|
|
595
|
+
|
|
596
|
+
pix, _, _ = get_pixel_values_from_image(imagename, "I", threshold)
|
|
597
|
+
flux = float(np.sum(pix))
|
|
598
|
+
results.append(f"{imagename}: threshold={threshold}, flux={flux:.2f}")
|
|
599
|
+
except Exception as e:
|
|
600
|
+
results.append(f"{imagename}: ERROR - {str(e)}")
|
|
601
|
+
QMessageBox.information(self, "Batch Results", "\n".join(results))
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
class ImageInfoDialog(QDialog):
|
|
605
|
+
def __init__(self, parent=None, info_text=""):
|
|
606
|
+
super().__init__(parent)
|
|
607
|
+
self.setWindowTitle("Image Metadata / Info")
|
|
608
|
+
self.setMinimumSize(500, 400)
|
|
609
|
+
layout = QVBoxLayout(self)
|
|
610
|
+
self.text_area = QPlainTextEdit()
|
|
611
|
+
self.text_area.setReadOnly(True)
|
|
612
|
+
self.text_area.setPlainText(info_text)
|
|
613
|
+
button_box = QDialogButtonBox(QDialogButtonBox.Ok)
|
|
614
|
+
button_box.accepted.connect(self.accept)
|
|
615
|
+
layout.addWidget(self.text_area)
|
|
616
|
+
layout.addWidget(button_box)
|
|
617
|
+
self.setLayout(layout)
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
class PhaseShiftDialog(QDialog):
|
|
621
|
+
"""Dialog for configuring and executing solar phase center shifting."""
|
|
622
|
+
|
|
623
|
+
def __init__(self, parent=None, imagename=None):
|
|
624
|
+
super().__init__(parent)
|
|
625
|
+
self.setWindowTitle("Solar Phase Center Shifting")
|
|
626
|
+
self.setMinimumSize(1000, 800)
|
|
627
|
+
self.imagename = imagename
|
|
628
|
+
|
|
629
|
+
# Set the dialog size to match the parent window if available
|
|
630
|
+
"""if parent and parent.size().isValid():
|
|
631
|
+
self.resize(parent.size())
|
|
632
|
+
# Center the dialog relative to the parent
|
|
633
|
+
self.move(
|
|
634
|
+
parent.frameGeometry().topLeft()
|
|
635
|
+
+ parent.rect().center()
|
|
636
|
+
- self.rect().center()
|
|
637
|
+
)"""
|
|
638
|
+
|
|
639
|
+
self.setup_ui()
|
|
640
|
+
|
|
641
|
+
def setup_ui(self):
|
|
642
|
+
from .move_phasecenter import SolarPhaseCenter
|
|
643
|
+
|
|
644
|
+
main_layout = QVBoxLayout(self)
|
|
645
|
+
main_layout.setSpacing(15)
|
|
646
|
+
main_layout.setContentsMargins(15, 15, 15, 15)
|
|
647
|
+
|
|
648
|
+
# Add a description at the top
|
|
649
|
+
description = QLabel(
|
|
650
|
+
"This tool shifts the coordinate system so that the solar center aligns with the image phase center. "
|
|
651
|
+
"This is useful for properly aligning solar observations in heliographic coordinates."
|
|
652
|
+
)
|
|
653
|
+
description.setWordWrap(True)
|
|
654
|
+
description.setStyleSheet("color: #BBB; font-style: italic;")
|
|
655
|
+
main_layout.addWidget(description)
|
|
656
|
+
|
|
657
|
+
# Mode selection: Single file or batch processing
|
|
658
|
+
mode_container = QWidget()
|
|
659
|
+
mode_container_layout = QHBoxLayout(mode_container)
|
|
660
|
+
mode_container_layout.setContentsMargins(0, 0, 0, 0)
|
|
661
|
+
|
|
662
|
+
mode_group = QGroupBox("Processing Mode")
|
|
663
|
+
mode_layout = QHBoxLayout(mode_group)
|
|
664
|
+
mode_layout.setContentsMargins(10, 15, 10, 10)
|
|
665
|
+
|
|
666
|
+
self.single_mode_radio = QRadioButton("Single File")
|
|
667
|
+
self.batch_mode_radio = QRadioButton("Batch Processing")
|
|
668
|
+
self.single_mode_radio.setChecked(True)
|
|
669
|
+
|
|
670
|
+
mode_layout.addWidget(self.single_mode_radio)
|
|
671
|
+
mode_layout.addWidget(self.batch_mode_radio)
|
|
672
|
+
mode_layout.addStretch(1)
|
|
673
|
+
|
|
674
|
+
# Stokes parameter selection - moved next to mode selection
|
|
675
|
+
stokes_group = QGroupBox("Stokes Parameter")
|
|
676
|
+
stokes_group_layout = QVBoxLayout(stokes_group)
|
|
677
|
+
stokes_group_layout.setContentsMargins(10, 15, 10, 10)
|
|
678
|
+
|
|
679
|
+
# Add radio buttons for Stokes mode selection
|
|
680
|
+
stokes_mode_layout = QHBoxLayout()
|
|
681
|
+
self.single_stokes_radio = QRadioButton("Single Stokes")
|
|
682
|
+
self.full_stokes_radio = QRadioButton("Full Stokes")
|
|
683
|
+
self.single_stokes_radio.setChecked(True)
|
|
684
|
+
stokes_mode_layout.addWidget(self.single_stokes_radio)
|
|
685
|
+
stokes_mode_layout.addWidget(self.full_stokes_radio)
|
|
686
|
+
stokes_mode_layout.addStretch(1)
|
|
687
|
+
stokes_group_layout.addLayout(stokes_mode_layout)
|
|
688
|
+
|
|
689
|
+
# Add stokes combo box for selection
|
|
690
|
+
stokes_select_layout = QHBoxLayout()
|
|
691
|
+
self.stokes_combo = QComboBox()
|
|
692
|
+
self.stokes_combo.addItems(["I", "Q", "U", "V"])
|
|
693
|
+
stokes_select_layout.addWidget(self.stokes_combo)
|
|
694
|
+
stokes_select_layout.addStretch(1)
|
|
695
|
+
stokes_group_layout.addLayout(stokes_select_layout)
|
|
696
|
+
|
|
697
|
+
# Connect stokes mode radios to update UI
|
|
698
|
+
self.single_stokes_radio.toggled.connect(self.update_stokes_mode)
|
|
699
|
+
self.full_stokes_radio.toggled.connect(self.update_stokes_mode)
|
|
700
|
+
|
|
701
|
+
# Add the two groups to the container
|
|
702
|
+
mode_container_layout.addWidget(mode_group, 1)
|
|
703
|
+
mode_container_layout.addWidget(stokes_group, 1)
|
|
704
|
+
main_layout.addWidget(mode_container)
|
|
705
|
+
|
|
706
|
+
# Connect mode radios to update UI
|
|
707
|
+
self.single_mode_radio.toggled.connect(self.update_mode_ui)
|
|
708
|
+
self.batch_mode_radio.toggled.connect(self.update_mode_ui)
|
|
709
|
+
|
|
710
|
+
# Input and Output options in two columns
|
|
711
|
+
io_container = QWidget()
|
|
712
|
+
io_layout = QHBoxLayout(io_container)
|
|
713
|
+
io_layout.setContentsMargins(0, 0, 0, 0)
|
|
714
|
+
io_layout.setSpacing(15)
|
|
715
|
+
|
|
716
|
+
# Input options group (left column)
|
|
717
|
+
self.input_group = QGroupBox("Input Settings")
|
|
718
|
+
input_layout = QVBoxLayout(self.input_group)
|
|
719
|
+
input_layout.setSpacing(10)
|
|
720
|
+
input_layout.setContentsMargins(10, 15, 10, 10)
|
|
721
|
+
|
|
722
|
+
# Single file mode controls
|
|
723
|
+
self.single_file_widget = QWidget()
|
|
724
|
+
single_file_layout = QFormLayout(self.single_file_widget)
|
|
725
|
+
single_file_layout.setContentsMargins(0, 0, 0, 0)
|
|
726
|
+
single_file_layout.setVerticalSpacing(8)
|
|
727
|
+
|
|
728
|
+
# Image selection
|
|
729
|
+
image_layout = QHBoxLayout()
|
|
730
|
+
self.image_path_edit = QLineEdit(self.imagename or "")
|
|
731
|
+
self.image_path_edit.setReadOnly(True)
|
|
732
|
+
self.browse_button = QPushButton("Browse...")
|
|
733
|
+
self.browse_button.clicked.connect(self.browse_image)
|
|
734
|
+
image_layout.addWidget(self.image_path_edit, 1)
|
|
735
|
+
image_layout.addWidget(self.browse_button)
|
|
736
|
+
single_file_layout.addRow("Image:", image_layout)
|
|
737
|
+
|
|
738
|
+
# Batch mode controls
|
|
739
|
+
self.batch_file_widget = QWidget()
|
|
740
|
+
batch_file_layout = QFormLayout(self.batch_file_widget)
|
|
741
|
+
batch_file_layout.setContentsMargins(0, 0, 0, 0)
|
|
742
|
+
batch_file_layout.setVerticalSpacing(8)
|
|
743
|
+
|
|
744
|
+
# Reference image selection for batch mode
|
|
745
|
+
reference_image_layout = QHBoxLayout()
|
|
746
|
+
self.reference_image_edit = QLineEdit("")
|
|
747
|
+
self.reference_image_edit.setReadOnly(True)
|
|
748
|
+
self.reference_image_edit.setPlaceholderText(
|
|
749
|
+
"Select reference image for phase center calculation"
|
|
750
|
+
)
|
|
751
|
+
self.reference_browse_button = QPushButton("Browse...")
|
|
752
|
+
self.reference_browse_button.clicked.connect(self.browse_reference_image)
|
|
753
|
+
reference_image_layout.addWidget(self.reference_image_edit, 1)
|
|
754
|
+
reference_image_layout.addWidget(self.reference_browse_button)
|
|
755
|
+
batch_file_layout.addRow("Reference Image:", reference_image_layout)
|
|
756
|
+
|
|
757
|
+
# Input pattern selection
|
|
758
|
+
input_pattern_layout = QHBoxLayout()
|
|
759
|
+
self.input_pattern_edit = QLineEdit("")
|
|
760
|
+
self.input_pattern_edit.setPlaceholderText("e.g., /path/to/images/*.fits")
|
|
761
|
+
self.input_pattern_button = QPushButton("Browse...")
|
|
762
|
+
self.input_pattern_button.clicked.connect(self.browse_input_pattern)
|
|
763
|
+
input_pattern_layout.addWidget(self.input_pattern_edit, 1)
|
|
764
|
+
input_pattern_layout.addWidget(self.input_pattern_button)
|
|
765
|
+
batch_file_layout.addRow("Apply To Pattern:", input_pattern_layout)
|
|
766
|
+
|
|
767
|
+
# MS File selection (optional) - common for both modes
|
|
768
|
+
ms_layout = QHBoxLayout()
|
|
769
|
+
self.ms_path_edit = QLineEdit("")
|
|
770
|
+
self.ms_path_edit.setPlaceholderText(
|
|
771
|
+
"Optional MS file for phase center calculation"
|
|
772
|
+
)
|
|
773
|
+
self.ms_browse_button = QPushButton("Browse...")
|
|
774
|
+
self.ms_browse_button.clicked.connect(self.browse_ms)
|
|
775
|
+
ms_layout.addWidget(self.ms_path_edit, 1)
|
|
776
|
+
ms_layout.addWidget(self.ms_browse_button)
|
|
777
|
+
|
|
778
|
+
# Add widgets to input layout
|
|
779
|
+
input_layout.addWidget(self.single_file_widget)
|
|
780
|
+
input_layout.addWidget(self.batch_file_widget)
|
|
781
|
+
self.batch_file_widget.setVisible(False)
|
|
782
|
+
|
|
783
|
+
# Add MS file row directly to the input layout
|
|
784
|
+
ms_form_container = QWidget()
|
|
785
|
+
ms_form_layout = QFormLayout(ms_form_container)
|
|
786
|
+
ms_form_layout.setContentsMargins(0, 0, 0, 0)
|
|
787
|
+
ms_form_layout.setVerticalSpacing(8)
|
|
788
|
+
ms_form_layout.addRow("MS File (optional):", ms_layout)
|
|
789
|
+
input_layout.addWidget(ms_form_container)
|
|
790
|
+
|
|
791
|
+
# Output options group (right column)
|
|
792
|
+
output_group = QGroupBox("Output Settings")
|
|
793
|
+
output_layout = QVBoxLayout(output_group)
|
|
794
|
+
output_layout.setSpacing(10)
|
|
795
|
+
output_layout.setContentsMargins(10, 15, 10, 10)
|
|
796
|
+
|
|
797
|
+
# Single file output
|
|
798
|
+
self.single_output_widget = QWidget()
|
|
799
|
+
single_output_layout = QFormLayout(self.single_output_widget)
|
|
800
|
+
single_output_layout.setContentsMargins(0, 0, 0, 0)
|
|
801
|
+
single_output_layout.setVerticalSpacing(8)
|
|
802
|
+
|
|
803
|
+
# Output file selection for single file
|
|
804
|
+
output_file_layout = QHBoxLayout()
|
|
805
|
+
self.output_path_edit = QLineEdit("")
|
|
806
|
+
self.output_path_edit.setPlaceholderText("Leave empty to modify input image")
|
|
807
|
+
self.output_browse_button = QPushButton("Browse...")
|
|
808
|
+
self.output_browse_button.clicked.connect(self.browse_output)
|
|
809
|
+
output_file_layout.addWidget(self.output_path_edit, 1)
|
|
810
|
+
output_file_layout.addWidget(self.output_browse_button)
|
|
811
|
+
single_output_layout.addRow("Output File:", output_file_layout)
|
|
812
|
+
output_layout.addWidget(self.single_output_widget)
|
|
813
|
+
|
|
814
|
+
# Batch file output
|
|
815
|
+
self.batch_output_widget = QWidget()
|
|
816
|
+
batch_output_layout = QVBoxLayout(self.batch_output_widget)
|
|
817
|
+
batch_output_layout.setContentsMargins(0, 0, 0, 0)
|
|
818
|
+
batch_output_layout.setSpacing(8)
|
|
819
|
+
|
|
820
|
+
# Output pattern for batch mode
|
|
821
|
+
output_pattern_form = QFormLayout()
|
|
822
|
+
output_pattern_form.setVerticalSpacing(8)
|
|
823
|
+
output_pattern_layout = QHBoxLayout()
|
|
824
|
+
self.output_pattern_edit = QLineEdit("shifted_*.fits")
|
|
825
|
+
self.output_pattern_edit.setPlaceholderText("e.g., shifted_*.fits")
|
|
826
|
+
self.output_pattern_button = QPushButton("Browse Directory...")
|
|
827
|
+
self.output_pattern_button.clicked.connect(self.browse_output_dir)
|
|
828
|
+
output_pattern_layout.addWidget(self.output_pattern_edit, 1)
|
|
829
|
+
output_pattern_layout.addWidget(self.output_pattern_button)
|
|
830
|
+
output_pattern_form.addRow("Output Pattern:", output_pattern_layout)
|
|
831
|
+
batch_output_layout.addLayout(output_pattern_form)
|
|
832
|
+
|
|
833
|
+
# Add a help text for pattern
|
|
834
|
+
pattern_help = QLabel(
|
|
835
|
+
"Use * in the pattern as a placeholder for the original filename."
|
|
836
|
+
)
|
|
837
|
+
pattern_help.setStyleSheet("color: #BBB; font-style: italic;")
|
|
838
|
+
batch_output_layout.addWidget(pattern_help)
|
|
839
|
+
|
|
840
|
+
output_layout.addWidget(self.batch_output_widget)
|
|
841
|
+
self.batch_output_widget.setVisible(False)
|
|
842
|
+
|
|
843
|
+
# Add the input and output groups to the container
|
|
844
|
+
io_layout.addWidget(self.input_group, 1)
|
|
845
|
+
io_layout.addWidget(output_group, 1)
|
|
846
|
+
main_layout.addWidget(io_container)
|
|
847
|
+
|
|
848
|
+
# Method settings and Visual centering in one row
|
|
849
|
+
method_container = QWidget()
|
|
850
|
+
method_container_layout = QHBoxLayout(method_container)
|
|
851
|
+
method_container_layout.setContentsMargins(0, 0, 0, 0)
|
|
852
|
+
method_container_layout.setSpacing(15)
|
|
853
|
+
|
|
854
|
+
# Method options group
|
|
855
|
+
method_group = QGroupBox("Method Settings")
|
|
856
|
+
method_layout = QVBoxLayout(method_group)
|
|
857
|
+
method_layout.setSpacing(10)
|
|
858
|
+
method_layout.setContentsMargins(10, 15, 10, 10)
|
|
859
|
+
|
|
860
|
+
# Gaussian fitting option
|
|
861
|
+
self.fit_gaussian_check = QCheckBox("Use Gaussian fitting for solar center")
|
|
862
|
+
self.fit_gaussian_check.setChecked(False)
|
|
863
|
+
method_layout.addWidget(self.fit_gaussian_check)
|
|
864
|
+
|
|
865
|
+
# Sigma threshold for center-of-mass
|
|
866
|
+
sigma_layout = QHBoxLayout()
|
|
867
|
+
sigma_layout.addWidget(QLabel("Sigma threshold for center-of-mass:"))
|
|
868
|
+
self.sigma_spinbox = QDoubleSpinBox()
|
|
869
|
+
self.sigma_spinbox.setRange(1.0, 20.0)
|
|
870
|
+
self.sigma_spinbox.setValue(10.0)
|
|
871
|
+
self.sigma_spinbox.setSingleStep(0.5)
|
|
872
|
+
sigma_layout.addWidget(self.sigma_spinbox)
|
|
873
|
+
sigma_layout.addStretch()
|
|
874
|
+
method_layout.addLayout(sigma_layout)
|
|
875
|
+
|
|
876
|
+
# Visual centering option
|
|
877
|
+
self.visual_center_check = QCheckBox(
|
|
878
|
+
"Create a visually centered image (moves pixel data)"
|
|
879
|
+
)
|
|
880
|
+
self.visual_center_check.setChecked(False)
|
|
881
|
+
method_layout.addWidget(self.visual_center_check)
|
|
882
|
+
|
|
883
|
+
# Multiprocessing option for batch mode
|
|
884
|
+
self.multiprocessing_check = QCheckBox(
|
|
885
|
+
"Use multiprocessing for batch operations (faster)"
|
|
886
|
+
)
|
|
887
|
+
self.multiprocessing_check.setChecked(True)
|
|
888
|
+
self.multiprocessing_check.setToolTip(
|
|
889
|
+
"Enable parallel processing for batch operations"
|
|
890
|
+
)
|
|
891
|
+
method_layout.addWidget(self.multiprocessing_check)
|
|
892
|
+
|
|
893
|
+
# CPU cores selection
|
|
894
|
+
cores_layout = QHBoxLayout()
|
|
895
|
+
cores_layout.addWidget(QLabel("Number of CPU cores to use:"))
|
|
896
|
+
self.cores_spinbox = QSpinBox()
|
|
897
|
+
self.cores_spinbox.setRange(1, multiprocessing.cpu_count())
|
|
898
|
+
self.cores_spinbox.setValue(
|
|
899
|
+
max(1, multiprocessing.cpu_count() - 1)
|
|
900
|
+
) # Default to N-1 cores
|
|
901
|
+
self.cores_spinbox.setSingleStep(1)
|
|
902
|
+
self.cores_spinbox.setToolTip(
|
|
903
|
+
f"Maximum: {multiprocessing.cpu_count()} cores available"
|
|
904
|
+
)
|
|
905
|
+
cores_layout.addWidget(self.cores_spinbox)
|
|
906
|
+
cores_layout.addStretch()
|
|
907
|
+
method_layout.addLayout(cores_layout)
|
|
908
|
+
|
|
909
|
+
# Connect multiprocessing checkbox to enable/disable cores spinbox
|
|
910
|
+
self.multiprocessing_check.toggled.connect(self.cores_spinbox.setEnabled)
|
|
911
|
+
|
|
912
|
+
# Add the method group to the container (full width)
|
|
913
|
+
method_container_layout.addWidget(method_group)
|
|
914
|
+
main_layout.addWidget(method_container)
|
|
915
|
+
|
|
916
|
+
# Add a status text area
|
|
917
|
+
status_group = QGroupBox("Status / Results")
|
|
918
|
+
status_layout = QVBoxLayout(status_group)
|
|
919
|
+
status_layout.setContentsMargins(10, 15, 10, 10)
|
|
920
|
+
|
|
921
|
+
self.status_text = QPlainTextEdit()
|
|
922
|
+
self.status_text.setReadOnly(True)
|
|
923
|
+
self.status_text.setPlaceholderText("Status and results will appear here")
|
|
924
|
+
self.status_text.setMinimumHeight(100)
|
|
925
|
+
status_layout.addWidget(self.status_text)
|
|
926
|
+
|
|
927
|
+
main_layout.addWidget(status_group)
|
|
928
|
+
|
|
929
|
+
# Add dialog buttons
|
|
930
|
+
button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
|
931
|
+
button_box.accepted.connect(self.apply_phase_shift)
|
|
932
|
+
button_box.rejected.connect(self.reject)
|
|
933
|
+
main_layout.addWidget(button_box)
|
|
934
|
+
|
|
935
|
+
# Set the Ok button text based on mode
|
|
936
|
+
self.ok_button = button_box.button(QDialogButtonBox.Ok)
|
|
937
|
+
self.ok_button.setText("Apply Shift")
|
|
938
|
+
|
|
939
|
+
# Apply consistent styling to the dialog
|
|
940
|
+
self.setStyleSheet(
|
|
941
|
+
"""
|
|
942
|
+
QGroupBox {
|
|
943
|
+
border: 1px solid #555555;
|
|
944
|
+
border-radius: 3px;
|
|
945
|
+
margin-top: 0.5em;
|
|
946
|
+
padding-top: 0.5em;
|
|
947
|
+
}
|
|
948
|
+
QGroupBox::title {
|
|
949
|
+
subcontrol-origin: margin;
|
|
950
|
+
left: 10px;
|
|
951
|
+
padding: 0 3px 0 3px;
|
|
952
|
+
}
|
|
953
|
+
QLabel {
|
|
954
|
+
margin-top: 2px;
|
|
955
|
+
margin-bottom: 2px;
|
|
956
|
+
}
|
|
957
|
+
QRadioButton, QCheckBox {
|
|
958
|
+
min-height: 20px;
|
|
959
|
+
}
|
|
960
|
+
"""
|
|
961
|
+
)
|
|
962
|
+
|
|
963
|
+
def update_mode_ui(self):
|
|
964
|
+
"""Update UI components based on the selected mode"""
|
|
965
|
+
single_mode = self.single_mode_radio.isChecked()
|
|
966
|
+
|
|
967
|
+
# Update visibility of widgets
|
|
968
|
+
self.single_file_widget.setVisible(single_mode)
|
|
969
|
+
self.batch_file_widget.setVisible(not single_mode)
|
|
970
|
+
self.single_output_widget.setVisible(single_mode)
|
|
971
|
+
self.batch_output_widget.setVisible(not single_mode)
|
|
972
|
+
|
|
973
|
+
# Update button text
|
|
974
|
+
if single_mode:
|
|
975
|
+
self.ok_button.setText("Apply Shift")
|
|
976
|
+
else:
|
|
977
|
+
self.ok_button.setText("Apply Batch Shift")
|
|
978
|
+
|
|
979
|
+
def update_stokes_mode(self):
|
|
980
|
+
"""Update UI based on selected Stokes mode"""
|
|
981
|
+
single_stokes = self.single_stokes_radio.isChecked()
|
|
982
|
+
self.stokes_combo.setEnabled(single_stokes)
|
|
983
|
+
|
|
984
|
+
def browse_image(self):
|
|
985
|
+
"""Browse for input image file"""
|
|
986
|
+
file_path, _ = QFileDialog.getOpenFileName(
|
|
987
|
+
self, "Select Image File", "", "FITS Files (*.fits);;CASA Images (*)"
|
|
988
|
+
)
|
|
989
|
+
if file_path:
|
|
990
|
+
self.image_path_edit.setText(file_path)
|
|
991
|
+
self.imagename = file_path
|
|
992
|
+
|
|
993
|
+
# Set default output filename pattern
|
|
994
|
+
if not self.output_path_edit.text():
|
|
995
|
+
file_dir = os.path.dirname(file_path)
|
|
996
|
+
file_name = os.path.basename(file_path)
|
|
997
|
+
output_path = os.path.join(file_dir, f"shifted_{file_name}")
|
|
998
|
+
self.output_path_edit.setText(output_path)
|
|
999
|
+
|
|
1000
|
+
def browse_input_pattern(self):
|
|
1001
|
+
"""Browse for directory and help set input pattern"""
|
|
1002
|
+
dir_path = QFileDialog.getExistingDirectory(
|
|
1003
|
+
self, "Select Directory for Input Files"
|
|
1004
|
+
)
|
|
1005
|
+
if dir_path:
|
|
1006
|
+
# Set a default pattern in the selected directory
|
|
1007
|
+
self.input_pattern_edit.setText(os.path.join(dir_path, "*.fits"))
|
|
1008
|
+
|
|
1009
|
+
def browse_output_dir(self):
|
|
1010
|
+
"""Browse for output directory for batch processing"""
|
|
1011
|
+
dir_path = QFileDialog.getExistingDirectory(
|
|
1012
|
+
self, "Select Directory for Output Files"
|
|
1013
|
+
)
|
|
1014
|
+
if dir_path:
|
|
1015
|
+
# Preserve the filename pattern but update the directory
|
|
1016
|
+
pattern = os.path.basename(self.output_pattern_edit.text())
|
|
1017
|
+
if not pattern:
|
|
1018
|
+
pattern = "shifted_*.fits"
|
|
1019
|
+
self.output_pattern_edit.setText(os.path.join(dir_path, pattern))
|
|
1020
|
+
|
|
1021
|
+
def browse_ms(self):
|
|
1022
|
+
"""Browse for MS file"""
|
|
1023
|
+
dir_path = QFileDialog.getExistingDirectory(
|
|
1024
|
+
self, "Select Measurement Set Directory"
|
|
1025
|
+
)
|
|
1026
|
+
if dir_path:
|
|
1027
|
+
self.ms_path_edit.setText(dir_path)
|
|
1028
|
+
|
|
1029
|
+
def browse_output(self):
|
|
1030
|
+
"""Browse for output file location"""
|
|
1031
|
+
file_path, _ = QFileDialog.getSaveFileName(
|
|
1032
|
+
self, "Save Output As", "", "FITS Files (*.fits);;CASA Images (*)"
|
|
1033
|
+
)
|
|
1034
|
+
if file_path:
|
|
1035
|
+
self.output_path_edit.setText(file_path)
|
|
1036
|
+
|
|
1037
|
+
def browse_reference_image(self):
|
|
1038
|
+
"""Browse for reference image file for batch processing"""
|
|
1039
|
+
file_path, _ = QFileDialog.getOpenFileName(
|
|
1040
|
+
self, "Select Reference Image", "", "FITS Files (*.fits);;CASA Images (*)"
|
|
1041
|
+
)
|
|
1042
|
+
if file_path:
|
|
1043
|
+
self.reference_image_edit.setText(file_path)
|
|
1044
|
+
|
|
1045
|
+
# Set default input pattern in the same directory
|
|
1046
|
+
if not self.input_pattern_edit.text():
|
|
1047
|
+
file_dir = os.path.dirname(file_path)
|
|
1048
|
+
self.input_pattern_edit.setText(os.path.join(file_dir, "*.fits"))
|
|
1049
|
+
|
|
1050
|
+
def apply_phase_shift(self):
|
|
1051
|
+
"""Apply the phase shift to the image(s)"""
|
|
1052
|
+
import os
|
|
1053
|
+
from .move_phasecenter import SolarPhaseCenter
|
|
1054
|
+
|
|
1055
|
+
# Check if we're in batch mode or single file mode
|
|
1056
|
+
batch_mode = self.batch_mode_radio.isChecked()
|
|
1057
|
+
|
|
1058
|
+
# Check if we're processing full Stokes
|
|
1059
|
+
full_stokes = self.full_stokes_radio.isChecked()
|
|
1060
|
+
|
|
1061
|
+
# Validate inputs
|
|
1062
|
+
if batch_mode:
|
|
1063
|
+
if not self.reference_image_edit.text():
|
|
1064
|
+
QMessageBox.warning(
|
|
1065
|
+
self,
|
|
1066
|
+
"Input Error",
|
|
1067
|
+
"Please select a reference image for phase center calculation",
|
|
1068
|
+
)
|
|
1069
|
+
return
|
|
1070
|
+
if not self.input_pattern_edit.text():
|
|
1071
|
+
QMessageBox.warning(
|
|
1072
|
+
self, "Input Error", "Please specify a pattern for files to process"
|
|
1073
|
+
)
|
|
1074
|
+
return
|
|
1075
|
+
else:
|
|
1076
|
+
if not self.image_path_edit.text():
|
|
1077
|
+
QMessageBox.warning(self, "Input Error", "Please select an input image")
|
|
1078
|
+
return
|
|
1079
|
+
|
|
1080
|
+
try:
|
|
1081
|
+
# Get common parameters
|
|
1082
|
+
msname = self.ms_path_edit.text() or None
|
|
1083
|
+
|
|
1084
|
+
# Create SolarPhaseCenter instance - removing cellsize and imsize parameters
|
|
1085
|
+
spc = SolarPhaseCenter(msname=msname)
|
|
1086
|
+
|
|
1087
|
+
# Determine Stokes parameter to use
|
|
1088
|
+
if full_stokes:
|
|
1089
|
+
stokes_list = ["I", "Q", "U", "V"]
|
|
1090
|
+
self.status_text.appendPlainText(
|
|
1091
|
+
"Processing all Stokes parameters: I, Q, U, V"
|
|
1092
|
+
)
|
|
1093
|
+
else:
|
|
1094
|
+
stokes_list = [self.stokes_combo.currentText()]
|
|
1095
|
+
self.status_text.appendPlainText(
|
|
1096
|
+
f"Processing Stokes {self.stokes_combo.currentText()}"
|
|
1097
|
+
)
|
|
1098
|
+
|
|
1099
|
+
if batch_mode:
|
|
1100
|
+
# Batch processing mode
|
|
1101
|
+
reference_image = self.reference_image_edit.text()
|
|
1102
|
+
input_pattern = self.input_pattern_edit.text()
|
|
1103
|
+
output_pattern = (
|
|
1104
|
+
self.output_pattern_edit.text()
|
|
1105
|
+
if self.output_pattern_edit.text()
|
|
1106
|
+
else None
|
|
1107
|
+
)
|
|
1108
|
+
|
|
1109
|
+
self.status_text.appendPlainText(
|
|
1110
|
+
f"Using reference image: {reference_image}"
|
|
1111
|
+
)
|
|
1112
|
+
self.status_text.appendPlainText(
|
|
1113
|
+
f"Processing files matching pattern: {input_pattern}"
|
|
1114
|
+
)
|
|
1115
|
+
if output_pattern:
|
|
1116
|
+
self.status_text.appendPlainText(
|
|
1117
|
+
f"Output pattern: {output_pattern}"
|
|
1118
|
+
)
|
|
1119
|
+
else:
|
|
1120
|
+
self.status_text.appendPlainText(
|
|
1121
|
+
f"Will modify input files in-place"
|
|
1122
|
+
)
|
|
1123
|
+
|
|
1124
|
+
# First calculate phase shift from the reference image
|
|
1125
|
+
self.status_text.appendPlainText(
|
|
1126
|
+
f"Calculating solar center position using reference image: {reference_image}"
|
|
1127
|
+
)
|
|
1128
|
+
|
|
1129
|
+
# Check if any files match the pattern
|
|
1130
|
+
matching_files = glob.glob(input_pattern)
|
|
1131
|
+
if not matching_files:
|
|
1132
|
+
QMessageBox.warning(
|
|
1133
|
+
self,
|
|
1134
|
+
"Input Error",
|
|
1135
|
+
f"No files found matching pattern: {input_pattern}",
|
|
1136
|
+
)
|
|
1137
|
+
return
|
|
1138
|
+
|
|
1139
|
+
self.status_text.appendPlainText(
|
|
1140
|
+
f"Found {len(matching_files)} files matching the pattern"
|
|
1141
|
+
)
|
|
1142
|
+
|
|
1143
|
+
# Calculate phase shift based on the reference image
|
|
1144
|
+
ra, dec, needs_shift = spc.cal_solar_phaseshift(
|
|
1145
|
+
imagename=reference_image,
|
|
1146
|
+
fit_gaussian=self.fit_gaussian_check.isChecked(),
|
|
1147
|
+
sigma=self.sigma_spinbox.value(),
|
|
1148
|
+
)
|
|
1149
|
+
|
|
1150
|
+
self.status_text.appendPlainText(
|
|
1151
|
+
f"Calculated solar center: RA = {ra} deg, DEC = {dec} deg"
|
|
1152
|
+
)
|
|
1153
|
+
|
|
1154
|
+
if not needs_shift:
|
|
1155
|
+
self.status_text.appendPlainText(
|
|
1156
|
+
"No phase shift needed. Solar center is already aligned with phase center."
|
|
1157
|
+
)
|
|
1158
|
+
result = QMessageBox.question(
|
|
1159
|
+
self,
|
|
1160
|
+
"No Shift Needed",
|
|
1161
|
+
"No phase shift is needed as the solar center is already aligned. Proceed anyway?",
|
|
1162
|
+
QMessageBox.Yes | QMessageBox.No,
|
|
1163
|
+
QMessageBox.No,
|
|
1164
|
+
)
|
|
1165
|
+
if result == QMessageBox.No:
|
|
1166
|
+
return
|
|
1167
|
+
|
|
1168
|
+
# Apply to all files
|
|
1169
|
+
visual_center = self.visual_center_check.isChecked()
|
|
1170
|
+
use_multiprocessing = self.multiprocessing_check.isChecked()
|
|
1171
|
+
max_processes = (
|
|
1172
|
+
self.cores_spinbox.value() if use_multiprocessing else None
|
|
1173
|
+
)
|
|
1174
|
+
|
|
1175
|
+
for stokes in stokes_list:
|
|
1176
|
+
self.status_text.appendPlainText(f"\nProcessing Stokes {stokes}...")
|
|
1177
|
+
|
|
1178
|
+
if use_multiprocessing:
|
|
1179
|
+
self.status_text.appendPlainText(
|
|
1180
|
+
f"Using multiprocessing with {max_processes} CPU cores"
|
|
1181
|
+
)
|
|
1182
|
+
|
|
1183
|
+
results = spc.apply_shift_to_multiple_fits(
|
|
1184
|
+
ra=ra,
|
|
1185
|
+
dec=dec,
|
|
1186
|
+
input_pattern=input_pattern,
|
|
1187
|
+
output_pattern=output_pattern,
|
|
1188
|
+
stokes=stokes,
|
|
1189
|
+
visual_center=visual_center,
|
|
1190
|
+
use_multiprocessing=use_multiprocessing,
|
|
1191
|
+
max_processes=max_processes,
|
|
1192
|
+
)
|
|
1193
|
+
|
|
1194
|
+
if visual_center:
|
|
1195
|
+
self.status_text.appendPlainText(
|
|
1196
|
+
"Visually centered images were also created with '_centered' suffix."
|
|
1197
|
+
)
|
|
1198
|
+
|
|
1199
|
+
self.status_text.appendPlainText(
|
|
1200
|
+
f"Successfully processed {results[0]} out of {results[1]} files for Stokes {stokes}"
|
|
1201
|
+
)
|
|
1202
|
+
|
|
1203
|
+
QMessageBox.information(
|
|
1204
|
+
self,
|
|
1205
|
+
"Success",
|
|
1206
|
+
f"Batch processing completed: {results[0]} out of {results[1]} files processed successfully.",
|
|
1207
|
+
)
|
|
1208
|
+
self.accept()
|
|
1209
|
+
|
|
1210
|
+
else:
|
|
1211
|
+
# Single file mode
|
|
1212
|
+
imagename = self.image_path_edit.text()
|
|
1213
|
+
|
|
1214
|
+
# Calculate phase shift
|
|
1215
|
+
self.status_text.appendPlainText("Calculating solar center position...")
|
|
1216
|
+
ra, dec, needs_shift = spc.cal_solar_phaseshift(
|
|
1217
|
+
imagename=imagename,
|
|
1218
|
+
fit_gaussian=self.fit_gaussian_check.isChecked(),
|
|
1219
|
+
sigma=self.sigma_spinbox.value(),
|
|
1220
|
+
)
|
|
1221
|
+
|
|
1222
|
+
self.status_text.appendPlainText(
|
|
1223
|
+
f"Calculated solar center: RA = {ra} deg, DEC = {dec} deg"
|
|
1224
|
+
)
|
|
1225
|
+
|
|
1226
|
+
if not needs_shift:
|
|
1227
|
+
self.status_text.appendPlainText(
|
|
1228
|
+
"No phase shift needed. Solar center is already aligned with phase center."
|
|
1229
|
+
)
|
|
1230
|
+
result = QMessageBox.question(
|
|
1231
|
+
self,
|
|
1232
|
+
"No Shift Needed",
|
|
1233
|
+
"No phase shift is needed as the solar center is already aligned. Proceed anyway?",
|
|
1234
|
+
QMessageBox.Yes | QMessageBox.No,
|
|
1235
|
+
QMessageBox.No,
|
|
1236
|
+
)
|
|
1237
|
+
if result == QMessageBox.No:
|
|
1238
|
+
return
|
|
1239
|
+
|
|
1240
|
+
# Process all requested Stokes parameters
|
|
1241
|
+
for stokes_param in stokes_list:
|
|
1242
|
+
output_file = self.output_path_edit.text() or imagename
|
|
1243
|
+
|
|
1244
|
+
# For multi-Stokes mode, append stokes parameter to filename if output is specified
|
|
1245
|
+
if (
|
|
1246
|
+
full_stokes
|
|
1247
|
+
and self.output_path_edit.text()
|
|
1248
|
+
and len(stokes_list) > 1
|
|
1249
|
+
):
|
|
1250
|
+
base, ext = os.path.splitext(output_file)
|
|
1251
|
+
stokes_output_file = f"{base}_{stokes_param}{ext}"
|
|
1252
|
+
else:
|
|
1253
|
+
stokes_output_file = output_file
|
|
1254
|
+
|
|
1255
|
+
self.status_text.appendPlainText(
|
|
1256
|
+
f"\nProcessing Stokes {stokes_param}..."
|
|
1257
|
+
)
|
|
1258
|
+
self.status_text.appendPlainText(
|
|
1259
|
+
f"Output file: {stokes_output_file}"
|
|
1260
|
+
)
|
|
1261
|
+
|
|
1262
|
+
# If output is different from input, make a copy
|
|
1263
|
+
if stokes_output_file != imagename:
|
|
1264
|
+
import shutil
|
|
1265
|
+
|
|
1266
|
+
if os.path.isdir(imagename):
|
|
1267
|
+
os.system(f"rm -rf {stokes_output_file}")
|
|
1268
|
+
os.system(f"cp -r {imagename} {stokes_output_file}")
|
|
1269
|
+
else:
|
|
1270
|
+
shutil.copy(imagename, stokes_output_file)
|
|
1271
|
+
target = stokes_output_file
|
|
1272
|
+
else:
|
|
1273
|
+
target = imagename
|
|
1274
|
+
|
|
1275
|
+
self.status_text.appendPlainText(
|
|
1276
|
+
f"Applying phase shift to {target}..."
|
|
1277
|
+
)
|
|
1278
|
+
|
|
1279
|
+
result = spc.shift_phasecenter(
|
|
1280
|
+
imagename=target, ra=ra, dec=dec, stokes=stokes_param
|
|
1281
|
+
)
|
|
1282
|
+
|
|
1283
|
+
if result == 0:
|
|
1284
|
+
self.status_text.appendPlainText(
|
|
1285
|
+
"Phase shift successfully applied."
|
|
1286
|
+
)
|
|
1287
|
+
|
|
1288
|
+
# Create visually centered image if requested
|
|
1289
|
+
if self.visual_center_check.isChecked():
|
|
1290
|
+
# Generate output filename for visually centered image
|
|
1291
|
+
if stokes_output_file == imagename:
|
|
1292
|
+
# If modifying in place, create a separate centered file
|
|
1293
|
+
base_path = os.path.splitext(target)[0]
|
|
1294
|
+
ext = os.path.splitext(target)[1]
|
|
1295
|
+
visual_output = f"{base_path}_centered{ext}"
|
|
1296
|
+
else:
|
|
1297
|
+
# If already creating a new file, derive from that filename
|
|
1298
|
+
base_path = os.path.splitext(stokes_output_file)[0]
|
|
1299
|
+
ext = os.path.splitext(stokes_output_file)[1]
|
|
1300
|
+
visual_output = f"{base_path}_centered{ext}"
|
|
1301
|
+
|
|
1302
|
+
try:
|
|
1303
|
+
# Get the reference pixel values from the shifted image
|
|
1304
|
+
from astropy.io import fits
|
|
1305
|
+
|
|
1306
|
+
header = fits.getheader(target)
|
|
1307
|
+
crpix1 = int(header["CRPIX1"])
|
|
1308
|
+
crpix2 = int(header["CRPIX2"])
|
|
1309
|
+
|
|
1310
|
+
self.status_text.appendPlainText(
|
|
1311
|
+
f"Creating visually centered image: {visual_output}"
|
|
1312
|
+
)
|
|
1313
|
+
|
|
1314
|
+
# Create the visually centered image
|
|
1315
|
+
success = spc.visually_center_image(
|
|
1316
|
+
target, visual_output, crpix1, crpix2
|
|
1317
|
+
)
|
|
1318
|
+
|
|
1319
|
+
if success:
|
|
1320
|
+
self.status_text.appendPlainText(
|
|
1321
|
+
"Visually centered image created successfully."
|
|
1322
|
+
)
|
|
1323
|
+
else:
|
|
1324
|
+
self.status_text.appendPlainText(
|
|
1325
|
+
"Failed to create visually centered image."
|
|
1326
|
+
)
|
|
1327
|
+
except Exception as vis_error:
|
|
1328
|
+
self.status_text.appendPlainText(
|
|
1329
|
+
f"Error creating visually centered image: {str(vis_error)}"
|
|
1330
|
+
)
|
|
1331
|
+
elif result == 1:
|
|
1332
|
+
self.status_text.appendPlainText("Phase shift not needed.")
|
|
1333
|
+
else:
|
|
1334
|
+
self.status_text.appendPlainText(
|
|
1335
|
+
f"Error applying phase shift for Stokes {stokes_param}."
|
|
1336
|
+
)
|
|
1337
|
+
|
|
1338
|
+
QMessageBox.information(
|
|
1339
|
+
self,
|
|
1340
|
+
"Success",
|
|
1341
|
+
f"Solar phase center shift completed successfully for {len(stokes_list)} Stokes parameters.",
|
|
1342
|
+
)
|
|
1343
|
+
self.accept()
|
|
1344
|
+
|
|
1345
|
+
except Exception as e:
|
|
1346
|
+
import traceback
|
|
1347
|
+
|
|
1348
|
+
self.status_text.appendPlainText(f"Error: {str(e)}")
|
|
1349
|
+
self.status_text.appendPlainText(traceback.format_exc())
|
|
1350
|
+
QMessageBox.critical(self, "Error", f"An error occurred: {str(e)}")
|
|
1351
|
+
|
|
1352
|
+
def showEvent(self, event):
|
|
1353
|
+
"""Handle the show event to ensure correct sizing"""
|
|
1354
|
+
super().showEvent(event)
|
|
1355
|
+
|
|
1356
|
+
# Ensure the dialog size matches the parent when shown
|
|
1357
|
+
if self.parent() and self.parent().size().isValid():
|
|
1358
|
+
# Set size to match parent
|
|
1359
|
+
# self.resize(self.parent().size())
|
|
1360
|
+
|
|
1361
|
+
# Center relative to parent
|
|
1362
|
+
self.move(
|
|
1363
|
+
self.parent().frameGeometry().topLeft()
|
|
1364
|
+
+ self.parent().rect().center()
|
|
1365
|
+
- self.rect().center()
|
|
1366
|
+
)
|
|
1367
|
+
|
|
1368
|
+
|
|
1369
|
+
class HPCBatchConversionDialog(QDialog):
|
|
1370
|
+
"""Dialog for batch conversion of images to helioprojective coordinates."""
|
|
1371
|
+
|
|
1372
|
+
def __init__(self, parent=None, current_file=None):
|
|
1373
|
+
super().__init__(parent)
|
|
1374
|
+
self.setWindowTitle("Batch Conversion to Helioprojective Coordinates")
|
|
1375
|
+
self.setMinimumSize(900, 600)
|
|
1376
|
+
self.parent = parent
|
|
1377
|
+
self.current_file = current_file
|
|
1378
|
+
self.setup_ui()
|
|
1379
|
+
|
|
1380
|
+
def setup_ui(self):
|
|
1381
|
+
"""Set up the dialog UI with a two-column layout."""
|
|
1382
|
+
main_layout = QVBoxLayout(self)
|
|
1383
|
+
main_layout.setSpacing(15)
|
|
1384
|
+
main_layout.setContentsMargins(15, 15, 15, 15)
|
|
1385
|
+
|
|
1386
|
+
# Add a description at the top
|
|
1387
|
+
description = QLabel(
|
|
1388
|
+
"This tool converts multiple images to helioprojective coordinates in batch. "
|
|
1389
|
+
"Select a pattern of files to convert and specify the output pattern."
|
|
1390
|
+
)
|
|
1391
|
+
description.setWordWrap(True)
|
|
1392
|
+
description.setStyleSheet("color: #BBB; font-style: italic;")
|
|
1393
|
+
main_layout.addWidget(description)
|
|
1394
|
+
|
|
1395
|
+
# Create two-column layout
|
|
1396
|
+
columns_layout = QHBoxLayout()
|
|
1397
|
+
columns_layout.setSpacing(15)
|
|
1398
|
+
|
|
1399
|
+
# ===== LEFT COLUMN =====
|
|
1400
|
+
left_column = QVBoxLayout()
|
|
1401
|
+
left_column.setSpacing(10)
|
|
1402
|
+
|
|
1403
|
+
# Input section
|
|
1404
|
+
input_group = QGroupBox("Input Settings")
|
|
1405
|
+
input_layout = QVBoxLayout(input_group)
|
|
1406
|
+
input_layout.setSpacing(10)
|
|
1407
|
+
input_layout.setContentsMargins(10, 15, 10, 10)
|
|
1408
|
+
|
|
1409
|
+
# Directory selection
|
|
1410
|
+
dir_layout = QHBoxLayout()
|
|
1411
|
+
self.dir_label = QLabel("Input Directory:")
|
|
1412
|
+
self.dir_edit = QLineEdit()
|
|
1413
|
+
if self.current_file:
|
|
1414
|
+
self.dir_edit.setText(os.path.dirname(self.current_file))
|
|
1415
|
+
self.dir_browse_btn = QPushButton("Browse...")
|
|
1416
|
+
self.dir_browse_btn.clicked.connect(self.browse_directory)
|
|
1417
|
+
dir_layout.addWidget(self.dir_label)
|
|
1418
|
+
dir_layout.addWidget(self.dir_edit, 1)
|
|
1419
|
+
dir_layout.addWidget(self.dir_browse_btn)
|
|
1420
|
+
input_layout.addLayout(dir_layout)
|
|
1421
|
+
|
|
1422
|
+
# File pattern
|
|
1423
|
+
pattern_layout = QHBoxLayout()
|
|
1424
|
+
self.pattern_label = QLabel("File Pattern:")
|
|
1425
|
+
self.pattern_edit = QLineEdit()
|
|
1426
|
+
if self.current_file:
|
|
1427
|
+
file_ext = os.path.splitext(self.current_file)[1]
|
|
1428
|
+
self.pattern_edit.setText(f"*{file_ext}")
|
|
1429
|
+
else:
|
|
1430
|
+
self.pattern_edit.setText("*.fits")
|
|
1431
|
+
self.pattern_edit.setPlaceholderText("e.g., *.fits")
|
|
1432
|
+
pattern_layout.addWidget(self.pattern_label)
|
|
1433
|
+
pattern_layout.addWidget(self.pattern_edit, 1)
|
|
1434
|
+
input_layout.addLayout(pattern_layout)
|
|
1435
|
+
|
|
1436
|
+
# Preview button
|
|
1437
|
+
preview_btn = QPushButton("Preview Files")
|
|
1438
|
+
preview_btn.clicked.connect(self.preview_files)
|
|
1439
|
+
input_layout.addWidget(preview_btn)
|
|
1440
|
+
|
|
1441
|
+
# Files list
|
|
1442
|
+
self.files_label = QLabel("Files to be processed:")
|
|
1443
|
+
input_layout.addWidget(self.files_label)
|
|
1444
|
+
|
|
1445
|
+
self.files_list = QListWidget()
|
|
1446
|
+
self.files_list.setSelectionMode(QListWidget.ExtendedSelection)
|
|
1447
|
+
self.files_list.setMinimumHeight(150)
|
|
1448
|
+
input_layout.addWidget(self.files_list)
|
|
1449
|
+
|
|
1450
|
+
left_column.addWidget(input_group)
|
|
1451
|
+
|
|
1452
|
+
# Stokes and Processing Settings group (combined for better space usage)
|
|
1453
|
+
options_group = QGroupBox("Processing Options")
|
|
1454
|
+
options_layout = QVBoxLayout(options_group)
|
|
1455
|
+
options_layout.setSpacing(10)
|
|
1456
|
+
options_layout.setContentsMargins(10, 15, 10, 10)
|
|
1457
|
+
|
|
1458
|
+
# Stokes parameter selection
|
|
1459
|
+
stokes_form = QFormLayout()
|
|
1460
|
+
stokes_form.setVerticalSpacing(10)
|
|
1461
|
+
stokes_form.setHorizontalSpacing(15)
|
|
1462
|
+
|
|
1463
|
+
# Mode selection layout
|
|
1464
|
+
stokes_mode_layout = QHBoxLayout()
|
|
1465
|
+
self.single_stokes_radio = QRadioButton("Single Stokes")
|
|
1466
|
+
self.full_stokes_radio = QRadioButton("Full Stokes")
|
|
1467
|
+
self.single_stokes_radio.setChecked(True)
|
|
1468
|
+
stokes_mode_layout.addWidget(self.single_stokes_radio)
|
|
1469
|
+
stokes_mode_layout.addWidget(self.full_stokes_radio)
|
|
1470
|
+
stokes_mode_layout.addStretch(1)
|
|
1471
|
+
stokes_form.addRow("Mode:", stokes_mode_layout)
|
|
1472
|
+
|
|
1473
|
+
# Stokes combo
|
|
1474
|
+
self.stokes_combo = QComboBox()
|
|
1475
|
+
self.stokes_combo.addItems(["I", "Q", "U", "V"])
|
|
1476
|
+
stokes_form.addRow("Parameter:", self.stokes_combo)
|
|
1477
|
+
|
|
1478
|
+
# Connect stokes mode radios to update UI
|
|
1479
|
+
self.single_stokes_radio.toggled.connect(self.update_stokes_mode)
|
|
1480
|
+
self.full_stokes_radio.toggled.connect(self.update_stokes_mode)
|
|
1481
|
+
|
|
1482
|
+
options_layout.addLayout(stokes_form)
|
|
1483
|
+
|
|
1484
|
+
# Add a separator line
|
|
1485
|
+
line = QFrame()
|
|
1486
|
+
line.setFrameShape(QFrame.HLine)
|
|
1487
|
+
line.setFrameShadow(QFrame.Sunken)
|
|
1488
|
+
options_layout.addWidget(line)
|
|
1489
|
+
|
|
1490
|
+
# Multiprocessing options
|
|
1491
|
+
self.multiprocessing_check = QCheckBox("Use multiprocessing (faster)")
|
|
1492
|
+
self.multiprocessing_check.setChecked(True)
|
|
1493
|
+
options_layout.addWidget(self.multiprocessing_check)
|
|
1494
|
+
|
|
1495
|
+
# CPU cores selection
|
|
1496
|
+
cores_layout = QHBoxLayout()
|
|
1497
|
+
cores_layout.addWidget(QLabel("CPU cores:"))
|
|
1498
|
+
self.cores_spinbox = QSpinBox()
|
|
1499
|
+
self.cores_spinbox.setRange(1, multiprocessing.cpu_count())
|
|
1500
|
+
self.cores_spinbox.setValue(max(1, multiprocessing.cpu_count() - 1))
|
|
1501
|
+
cores_layout.addWidget(self.cores_spinbox)
|
|
1502
|
+
cores_layout.addStretch()
|
|
1503
|
+
options_layout.addLayout(cores_layout)
|
|
1504
|
+
|
|
1505
|
+
# Connect multiprocessing checkbox to enable/disable cores spinbox
|
|
1506
|
+
self.multiprocessing_check.toggled.connect(self.cores_spinbox.setEnabled)
|
|
1507
|
+
|
|
1508
|
+
left_column.addWidget(options_group)
|
|
1509
|
+
|
|
1510
|
+
# ===== RIGHT COLUMN =====
|
|
1511
|
+
right_column = QVBoxLayout()
|
|
1512
|
+
right_column.setSpacing(10)
|
|
1513
|
+
|
|
1514
|
+
# Output section
|
|
1515
|
+
output_group = QGroupBox("Output Settings")
|
|
1516
|
+
output_layout = QVBoxLayout(output_group)
|
|
1517
|
+
output_layout.setSpacing(10)
|
|
1518
|
+
output_layout.setContentsMargins(10, 15, 10, 10)
|
|
1519
|
+
|
|
1520
|
+
# Output directory and pattern
|
|
1521
|
+
output_dir_layout = QHBoxLayout()
|
|
1522
|
+
self.output_dir_label = QLabel("Output Directory:")
|
|
1523
|
+
self.output_dir_edit = QLineEdit()
|
|
1524
|
+
if self.current_file:
|
|
1525
|
+
self.output_dir_edit.setText(os.path.dirname(self.current_file))
|
|
1526
|
+
self.output_dir_btn = QPushButton("Browse...")
|
|
1527
|
+
self.output_dir_btn.clicked.connect(self.browse_output_directory)
|
|
1528
|
+
output_dir_layout.addWidget(self.output_dir_label)
|
|
1529
|
+
output_dir_layout.addWidget(self.output_dir_edit, 1)
|
|
1530
|
+
output_dir_layout.addWidget(self.output_dir_btn)
|
|
1531
|
+
output_layout.addLayout(output_dir_layout)
|
|
1532
|
+
|
|
1533
|
+
output_pattern_layout = QHBoxLayout()
|
|
1534
|
+
self.output_pattern_label = QLabel("Output Pattern:")
|
|
1535
|
+
self.output_pattern_edit = QLineEdit("hpc_*.fits")
|
|
1536
|
+
self.output_pattern_edit.setPlaceholderText("e.g., hpc_*.fits")
|
|
1537
|
+
output_pattern_layout.addWidget(self.output_pattern_label)
|
|
1538
|
+
output_pattern_layout.addWidget(self.output_pattern_edit, 1)
|
|
1539
|
+
output_layout.addLayout(output_pattern_layout)
|
|
1540
|
+
|
|
1541
|
+
# Add a help text for pattern
|
|
1542
|
+
pattern_help = QLabel(
|
|
1543
|
+
"Use * in the pattern as a placeholder for the original filename."
|
|
1544
|
+
)
|
|
1545
|
+
pattern_help.setStyleSheet("color: #BBB; font-style: italic;")
|
|
1546
|
+
output_layout.addWidget(pattern_help)
|
|
1547
|
+
|
|
1548
|
+
# Add example section
|
|
1549
|
+
example_group = QVBoxLayout()
|
|
1550
|
+
example_title = QLabel("Example:")
|
|
1551
|
+
example_title.setStyleSheet("font-weight: bold;")
|
|
1552
|
+
example_label = QLabel("Input: myimage.fits → Output: hpc_myimage.fits")
|
|
1553
|
+
example_label.setStyleSheet("color: #AAA; font-style: italic;")
|
|
1554
|
+
example_group.addWidget(example_title)
|
|
1555
|
+
example_group.addWidget(example_label)
|
|
1556
|
+
output_layout.addLayout(example_group)
|
|
1557
|
+
|
|
1558
|
+
right_column.addWidget(output_group)
|
|
1559
|
+
|
|
1560
|
+
# Status text area
|
|
1561
|
+
status_group = QGroupBox("Status / Results")
|
|
1562
|
+
status_layout = QVBoxLayout(status_group)
|
|
1563
|
+
status_layout.setContentsMargins(10, 15, 10, 10)
|
|
1564
|
+
|
|
1565
|
+
self.status_text = QPlainTextEdit()
|
|
1566
|
+
self.status_text.setReadOnly(True)
|
|
1567
|
+
self.status_text.setPlaceholderText("Status and results will appear here")
|
|
1568
|
+
self.status_text.setMinimumHeight(250) # Increased height for better visibility
|
|
1569
|
+
status_layout.addWidget(self.status_text)
|
|
1570
|
+
|
|
1571
|
+
right_column.addWidget(status_group)
|
|
1572
|
+
|
|
1573
|
+
# Add columns to the layout
|
|
1574
|
+
columns_layout.addLayout(left_column, 1) # 1 is the stretch factor
|
|
1575
|
+
columns_layout.addLayout(right_column, 1) # 1 is the stretch factor
|
|
1576
|
+
|
|
1577
|
+
main_layout.addLayout(columns_layout)
|
|
1578
|
+
|
|
1579
|
+
# Dialog buttons
|
|
1580
|
+
button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
|
1581
|
+
self.ok_button = button_box.button(QDialogButtonBox.Ok)
|
|
1582
|
+
self.ok_button.setText("Convert")
|
|
1583
|
+
button_box.accepted.connect(self.convert_files)
|
|
1584
|
+
button_box.rejected.connect(self.reject)
|
|
1585
|
+
main_layout.addWidget(button_box)
|
|
1586
|
+
|
|
1587
|
+
# Apply consistent styling to the dialog
|
|
1588
|
+
self.setStyleSheet(
|
|
1589
|
+
"""
|
|
1590
|
+
QGroupBox {
|
|
1591
|
+
border: 1px solid #555555;
|
|
1592
|
+
border-radius: 3px;
|
|
1593
|
+
margin-top: 0.5em;
|
|
1594
|
+
padding-top: 0.5em;
|
|
1595
|
+
}
|
|
1596
|
+
QGroupBox::title {
|
|
1597
|
+
subcontrol-origin: margin;
|
|
1598
|
+
left: 10px;
|
|
1599
|
+
padding: 0 3px 0 3px;
|
|
1600
|
+
}
|
|
1601
|
+
QLabel {
|
|
1602
|
+
margin-top: 2px;
|
|
1603
|
+
margin-bottom: 2px;
|
|
1604
|
+
}
|
|
1605
|
+
QRadioButton, QCheckBox {
|
|
1606
|
+
min-height: 20px;
|
|
1607
|
+
}
|
|
1608
|
+
"""
|
|
1609
|
+
)
|
|
1610
|
+
|
|
1611
|
+
def browse_directory(self):
|
|
1612
|
+
"""Browse for input directory"""
|
|
1613
|
+
current_dir = self.dir_edit.text()
|
|
1614
|
+
if not current_dir and self.current_file:
|
|
1615
|
+
current_dir = os.path.dirname(self.current_file)
|
|
1616
|
+
if not current_dir:
|
|
1617
|
+
current_dir = os.path.expanduser("~")
|
|
1618
|
+
|
|
1619
|
+
directory = QFileDialog.getExistingDirectory(
|
|
1620
|
+
self, "Select Input Directory", current_dir
|
|
1621
|
+
)
|
|
1622
|
+
|
|
1623
|
+
if directory:
|
|
1624
|
+
self.dir_edit.setText(directory)
|
|
1625
|
+
|
|
1626
|
+
# Set output directory to match if not already set
|
|
1627
|
+
if not self.output_dir_edit.text():
|
|
1628
|
+
self.output_dir_edit.setText(directory)
|
|
1629
|
+
|
|
1630
|
+
# Preview files if pattern is already set
|
|
1631
|
+
self.preview_files()
|
|
1632
|
+
|
|
1633
|
+
def browse_output_directory(self):
|
|
1634
|
+
"""Browse for output directory"""
|
|
1635
|
+
current_dir = self.output_dir_edit.text()
|
|
1636
|
+
if not current_dir:
|
|
1637
|
+
current_dir = self.dir_edit.text()
|
|
1638
|
+
if not current_dir and self.current_file:
|
|
1639
|
+
current_dir = os.path.dirname(self.current_file)
|
|
1640
|
+
if not current_dir:
|
|
1641
|
+
current_dir = os.path.expanduser("~")
|
|
1642
|
+
|
|
1643
|
+
directory = QFileDialog.getExistingDirectory(
|
|
1644
|
+
self, "Select Output Directory", current_dir
|
|
1645
|
+
)
|
|
1646
|
+
|
|
1647
|
+
if directory:
|
|
1648
|
+
self.output_dir_edit.setText(directory)
|
|
1649
|
+
|
|
1650
|
+
def update_stokes_mode(self):
|
|
1651
|
+
"""Update UI based on selected Stokes mode"""
|
|
1652
|
+
single_stokes = self.single_stokes_radio.isChecked()
|
|
1653
|
+
self.stokes_combo.setEnabled(single_stokes)
|
|
1654
|
+
|
|
1655
|
+
def preview_files(self):
|
|
1656
|
+
"""Show files that match the pattern in the list widget"""
|
|
1657
|
+
self.files_list.clear()
|
|
1658
|
+
|
|
1659
|
+
input_dir = self.dir_edit.text()
|
|
1660
|
+
pattern = self.pattern_edit.text()
|
|
1661
|
+
|
|
1662
|
+
if not input_dir:
|
|
1663
|
+
self.status_text.setPlainText("Please select an input directory.")
|
|
1664
|
+
return
|
|
1665
|
+
|
|
1666
|
+
try:
|
|
1667
|
+
# Get matching files
|
|
1668
|
+
input_pattern = os.path.join(input_dir, pattern)
|
|
1669
|
+
matching_files = glob.glob(input_pattern)
|
|
1670
|
+
|
|
1671
|
+
if not matching_files:
|
|
1672
|
+
self.status_text.setPlainText(
|
|
1673
|
+
f"No files found matching pattern: {input_pattern}"
|
|
1674
|
+
)
|
|
1675
|
+
return
|
|
1676
|
+
|
|
1677
|
+
# Add files to list, showing only basenames but storing full paths as item data
|
|
1678
|
+
for file_path in sorted(matching_files):
|
|
1679
|
+
basename = os.path.basename(file_path)
|
|
1680
|
+
item = QListWidgetItem(basename)
|
|
1681
|
+
item.setToolTip(file_path) # Show full path on hover
|
|
1682
|
+
item.setData(Qt.UserRole, file_path) # Store full path as data
|
|
1683
|
+
self.files_list.addItem(item)
|
|
1684
|
+
|
|
1685
|
+
self.status_text.setPlainText(
|
|
1686
|
+
f"Found {len(matching_files)} files matching the pattern."
|
|
1687
|
+
)
|
|
1688
|
+
except Exception as e:
|
|
1689
|
+
self.status_text.setPlainText(f"Error previewing files: {str(e)}")
|
|
1690
|
+
# Print the full error to console for debugging
|
|
1691
|
+
traceback.print_exc()
|
|
1692
|
+
|
|
1693
|
+
def convert_files(self):
|
|
1694
|
+
"""Convert the selected files to helioprojective coordinates"""
|
|
1695
|
+
# Get input files
|
|
1696
|
+
if self.files_list.count() == 0:
|
|
1697
|
+
QMessageBox.warning(
|
|
1698
|
+
self,
|
|
1699
|
+
"No Files Found",
|
|
1700
|
+
"No files match the pattern. Please check your input settings.",
|
|
1701
|
+
)
|
|
1702
|
+
return
|
|
1703
|
+
|
|
1704
|
+
# Get selected files or use all if none selected
|
|
1705
|
+
selected_items = self.files_list.selectedItems()
|
|
1706
|
+
if selected_items:
|
|
1707
|
+
# Get full paths from item data
|
|
1708
|
+
files_to_process = [item.data(Qt.UserRole) for item in selected_items]
|
|
1709
|
+
self.status_text.appendPlainText(
|
|
1710
|
+
f"Processing {len(files_to_process)} selected files."
|
|
1711
|
+
)
|
|
1712
|
+
else:
|
|
1713
|
+
# Get full paths from item data for all items
|
|
1714
|
+
files_to_process = [
|
|
1715
|
+
self.files_list.item(i).data(Qt.UserRole)
|
|
1716
|
+
for i in range(self.files_list.count())
|
|
1717
|
+
]
|
|
1718
|
+
self.status_text.appendPlainText(
|
|
1719
|
+
f"Processing all {len(files_to_process)} files."
|
|
1720
|
+
)
|
|
1721
|
+
|
|
1722
|
+
# Get output directory and pattern
|
|
1723
|
+
output_dir = self.output_dir_edit.text()
|
|
1724
|
+
output_pattern = self.output_pattern_edit.text()
|
|
1725
|
+
|
|
1726
|
+
if not output_dir:
|
|
1727
|
+
QMessageBox.warning(
|
|
1728
|
+
self,
|
|
1729
|
+
"Output Directory Missing",
|
|
1730
|
+
"Please specify an output directory.",
|
|
1731
|
+
)
|
|
1732
|
+
return
|
|
1733
|
+
|
|
1734
|
+
# Get processing options
|
|
1735
|
+
use_multiprocessing = self.multiprocessing_check.isChecked()
|
|
1736
|
+
max_cores = self.cores_spinbox.value() if use_multiprocessing else 1
|
|
1737
|
+
full_stokes = self.full_stokes_radio.isChecked()
|
|
1738
|
+
stokes_param = self.stokes_combo.currentText() if not full_stokes else None
|
|
1739
|
+
|
|
1740
|
+
# Prepare progress dialog
|
|
1741
|
+
progress_dialog = QProgressDialog(
|
|
1742
|
+
"Converting files to helioprojective coordinates...",
|
|
1743
|
+
"Cancel",
|
|
1744
|
+
0,
|
|
1745
|
+
len(files_to_process),
|
|
1746
|
+
self,
|
|
1747
|
+
)
|
|
1748
|
+
progress_dialog.setWindowTitle("Batch Conversion")
|
|
1749
|
+
progress_dialog.setWindowModality(Qt.WindowModal)
|
|
1750
|
+
progress_dialog.show()
|
|
1751
|
+
|
|
1752
|
+
# Import modules needed for processing
|
|
1753
|
+
import multiprocessing
|
|
1754
|
+
import time
|
|
1755
|
+
from .helioprojective import convert_and_save_hpc
|
|
1756
|
+
|
|
1757
|
+
# Use a worker thread or process for conversion
|
|
1758
|
+
try:
|
|
1759
|
+
self.status_text.appendPlainText("Starting batch conversion...")
|
|
1760
|
+
self.ok_button.setEnabled(False)
|
|
1761
|
+
QApplication.processEvents()
|
|
1762
|
+
|
|
1763
|
+
# Initialize counters
|
|
1764
|
+
success_count = 0
|
|
1765
|
+
error_count = 0
|
|
1766
|
+
completed_count = 0
|
|
1767
|
+
pool = None
|
|
1768
|
+
results = []
|
|
1769
|
+
|
|
1770
|
+
# Multi-stokes requires different handling
|
|
1771
|
+
if full_stokes:
|
|
1772
|
+
stokes_list = ["I", "Q", "U", "V"]
|
|
1773
|
+
|
|
1774
|
+
if use_multiprocessing and len(files_to_process) > 1:
|
|
1775
|
+
# Prepare arguments for multiprocessing
|
|
1776
|
+
self.status_text.appendPlainText(
|
|
1777
|
+
f"Using multiprocessing with {max_cores} cores"
|
|
1778
|
+
)
|
|
1779
|
+
|
|
1780
|
+
# Create task list - each task is (input_file, output_path, stokes, process_id)
|
|
1781
|
+
tasks = []
|
|
1782
|
+
for i, input_file in enumerate(files_to_process):
|
|
1783
|
+
base_filename = os.path.basename(input_file)
|
|
1784
|
+
process_id = i # Use file index as part of process ID
|
|
1785
|
+
|
|
1786
|
+
if "*" in output_pattern:
|
|
1787
|
+
output_filename = output_pattern.replace(
|
|
1788
|
+
"*", os.path.splitext(base_filename)[0]
|
|
1789
|
+
)
|
|
1790
|
+
else:
|
|
1791
|
+
output_filename = (
|
|
1792
|
+
f"{os.path.splitext(base_filename)[0]}_{output_pattern}"
|
|
1793
|
+
)
|
|
1794
|
+
|
|
1795
|
+
output_path = os.path.join(output_dir, output_filename)
|
|
1796
|
+
|
|
1797
|
+
# Create tasks for each stokes parameter
|
|
1798
|
+
for stokes in stokes_list:
|
|
1799
|
+
stokes_output = output_path.replace(
|
|
1800
|
+
".fits", f"_{stokes}.fits"
|
|
1801
|
+
)
|
|
1802
|
+
task = (
|
|
1803
|
+
input_file,
|
|
1804
|
+
stokes_output,
|
|
1805
|
+
stokes,
|
|
1806
|
+
f"{process_id}_{stokes}",
|
|
1807
|
+
)
|
|
1808
|
+
tasks.append(task)
|
|
1809
|
+
|
|
1810
|
+
# Set up progress tracking
|
|
1811
|
+
total_tasks = len(tasks)
|
|
1812
|
+
progress_dialog.setMaximum(total_tasks)
|
|
1813
|
+
|
|
1814
|
+
# Create process pool and start processing
|
|
1815
|
+
pool = multiprocessing.Pool(processes=max_cores)
|
|
1816
|
+
|
|
1817
|
+
# Start asynchronous processing with our standalone function
|
|
1818
|
+
result_objects = pool.map_async(process_single_file_hpc, tasks)
|
|
1819
|
+
pool.close() # No more tasks will be submitted
|
|
1820
|
+
|
|
1821
|
+
# Monitor progress while processing
|
|
1822
|
+
while not result_objects.ready():
|
|
1823
|
+
if progress_dialog.wasCanceled():
|
|
1824
|
+
pool.terminate()
|
|
1825
|
+
self.status_text.appendPlainText(
|
|
1826
|
+
"Operation canceled by user."
|
|
1827
|
+
)
|
|
1828
|
+
break
|
|
1829
|
+
time.sleep(0.1) # Short sleep to prevent UI blocking
|
|
1830
|
+
QApplication.processEvents()
|
|
1831
|
+
|
|
1832
|
+
# Get results if not canceled
|
|
1833
|
+
if not progress_dialog.wasCanceled():
|
|
1834
|
+
results = result_objects.get()
|
|
1835
|
+
|
|
1836
|
+
# Process results
|
|
1837
|
+
file_results = {} # Group results by input file
|
|
1838
|
+
|
|
1839
|
+
for result in results:
|
|
1840
|
+
input_file = result["input_file"]
|
|
1841
|
+
basename = os.path.basename(input_file)
|
|
1842
|
+
|
|
1843
|
+
if basename not in file_results:
|
|
1844
|
+
file_results[basename] = {"success": 0, "errors": []}
|
|
1845
|
+
|
|
1846
|
+
if result["success"]:
|
|
1847
|
+
file_results[basename]["success"] += 1
|
|
1848
|
+
self.status_text.appendPlainText(
|
|
1849
|
+
f" - Stokes {result['stokes']}: Converted successfully"
|
|
1850
|
+
)
|
|
1851
|
+
else:
|
|
1852
|
+
error_msg = result["error"] or "Unknown error"
|
|
1853
|
+
file_results[basename]["errors"].append(
|
|
1854
|
+
f"Stokes {result['stokes']}: {error_msg}"
|
|
1855
|
+
)
|
|
1856
|
+
self.status_text.appendPlainText(
|
|
1857
|
+
f" - Stokes {result['stokes']}: Error: {error_msg}"
|
|
1858
|
+
)
|
|
1859
|
+
|
|
1860
|
+
# Count overall successes
|
|
1861
|
+
for basename, res in file_results.items():
|
|
1862
|
+
if res["success"] == len(stokes_list):
|
|
1863
|
+
success_count += 1
|
|
1864
|
+
elif res["success"] > 0:
|
|
1865
|
+
success_count += 0.5 # Partial success
|
|
1866
|
+
error_count += 0.5
|
|
1867
|
+
else:
|
|
1868
|
+
error_count += 1
|
|
1869
|
+
|
|
1870
|
+
# Log each file's summary
|
|
1871
|
+
self.status_text.appendPlainText(
|
|
1872
|
+
f"File {basename}: {res['success']}/{len(stokes_list)} stokes parameters processed successfully"
|
|
1873
|
+
)
|
|
1874
|
+
if res["errors"]:
|
|
1875
|
+
for err in res["errors"]:
|
|
1876
|
+
self.status_text.appendPlainText(
|
|
1877
|
+
f" - Error: {err}"
|
|
1878
|
+
)
|
|
1879
|
+
|
|
1880
|
+
# Update progress to completion
|
|
1881
|
+
progress_dialog.setValue(total_tasks)
|
|
1882
|
+
else:
|
|
1883
|
+
# Sequential processing for multi-stokes
|
|
1884
|
+
for i, input_file in enumerate(files_to_process):
|
|
1885
|
+
# Check if canceled
|
|
1886
|
+
if progress_dialog.wasCanceled():
|
|
1887
|
+
self.status_text.appendPlainText(
|
|
1888
|
+
"Operation canceled by user."
|
|
1889
|
+
)
|
|
1890
|
+
break
|
|
1891
|
+
|
|
1892
|
+
# Get output filename
|
|
1893
|
+
base_filename = os.path.basename(input_file)
|
|
1894
|
+
if "*" in output_pattern:
|
|
1895
|
+
output_filename = output_pattern.replace(
|
|
1896
|
+
"*", os.path.splitext(base_filename)[0]
|
|
1897
|
+
)
|
|
1898
|
+
else:
|
|
1899
|
+
output_filename = (
|
|
1900
|
+
f"{os.path.splitext(base_filename)[0]}_{output_pattern}"
|
|
1901
|
+
)
|
|
1902
|
+
|
|
1903
|
+
output_path = os.path.join(output_dir, output_filename)
|
|
1904
|
+
|
|
1905
|
+
# Update progress dialog
|
|
1906
|
+
progress_dialog.setValue(i)
|
|
1907
|
+
progress_dialog.setLabelText(f"Converting: {base_filename}")
|
|
1908
|
+
QApplication.processEvents()
|
|
1909
|
+
|
|
1910
|
+
self.status_text.appendPlainText(
|
|
1911
|
+
f"Processing {i+1}/{len(files_to_process)}: {base_filename}"
|
|
1912
|
+
)
|
|
1913
|
+
|
|
1914
|
+
stokes_success = 0
|
|
1915
|
+
for stokes in stokes_list:
|
|
1916
|
+
# Create stokes-specific output filename
|
|
1917
|
+
stokes_output = output_path.replace(
|
|
1918
|
+
".fits", f"_{stokes}.fits"
|
|
1919
|
+
)
|
|
1920
|
+
|
|
1921
|
+
try:
|
|
1922
|
+
# Convert file with a unique temp suffix
|
|
1923
|
+
temp_suffix = f"_seq_{i}_{stokes}"
|
|
1924
|
+
result = process_single_file_hpc(
|
|
1925
|
+
(
|
|
1926
|
+
input_file,
|
|
1927
|
+
stokes_output,
|
|
1928
|
+
stokes,
|
|
1929
|
+
f"_seq_{i}_{stokes}",
|
|
1930
|
+
)
|
|
1931
|
+
)
|
|
1932
|
+
success = result["success"]
|
|
1933
|
+
|
|
1934
|
+
if success:
|
|
1935
|
+
stokes_success += 1
|
|
1936
|
+
self.status_text.appendPlainText(
|
|
1937
|
+
f" - Stokes {stokes}: Converted successfully"
|
|
1938
|
+
)
|
|
1939
|
+
else:
|
|
1940
|
+
self.status_text.appendPlainText(
|
|
1941
|
+
f" - Stokes {stokes}: Conversion failed"
|
|
1942
|
+
)
|
|
1943
|
+
|
|
1944
|
+
except Exception as e:
|
|
1945
|
+
self.status_text.appendPlainText(
|
|
1946
|
+
f" - Stokes {stokes}: Error: {str(e)}"
|
|
1947
|
+
)
|
|
1948
|
+
|
|
1949
|
+
if stokes_success == len(stokes_list):
|
|
1950
|
+
success_count += 1
|
|
1951
|
+
elif stokes_success > 0:
|
|
1952
|
+
success_count += 0.5 # Partial success
|
|
1953
|
+
error_count += 0.5
|
|
1954
|
+
else:
|
|
1955
|
+
error_count += 1
|
|
1956
|
+
else:
|
|
1957
|
+
# Single stokes processing
|
|
1958
|
+
if use_multiprocessing and len(files_to_process) > 1:
|
|
1959
|
+
# Prepare arguments for multiprocessing
|
|
1960
|
+
self.status_text.appendPlainText(
|
|
1961
|
+
f"Using multiprocessing with {max_cores} cores"
|
|
1962
|
+
)
|
|
1963
|
+
|
|
1964
|
+
# Create task list - each task is (input_file, output_path, stokes, process_id)
|
|
1965
|
+
tasks = []
|
|
1966
|
+
for i, input_file in enumerate(files_to_process):
|
|
1967
|
+
base_filename = os.path.basename(input_file)
|
|
1968
|
+
|
|
1969
|
+
if "*" in output_pattern:
|
|
1970
|
+
output_filename = output_pattern.replace(
|
|
1971
|
+
"*", os.path.splitext(base_filename)[0]
|
|
1972
|
+
)
|
|
1973
|
+
else:
|
|
1974
|
+
output_filename = (
|
|
1975
|
+
f"{os.path.splitext(base_filename)[0]}_{output_pattern}"
|
|
1976
|
+
)
|
|
1977
|
+
|
|
1978
|
+
output_path = os.path.join(output_dir, output_filename)
|
|
1979
|
+
task = (input_file, output_path, stokes_param, i)
|
|
1980
|
+
tasks.append(task)
|
|
1981
|
+
|
|
1982
|
+
# Set up progress tracking
|
|
1983
|
+
total_tasks = len(tasks)
|
|
1984
|
+
progress_dialog.setMaximum(total_tasks)
|
|
1985
|
+
|
|
1986
|
+
# Create process pool
|
|
1987
|
+
pool = multiprocessing.Pool(processes=max_cores)
|
|
1988
|
+
|
|
1989
|
+
# Start asynchronous processing
|
|
1990
|
+
result_objects = pool.map_async(process_single_file_hpc, tasks)
|
|
1991
|
+
pool.close() # No more tasks will be submitted
|
|
1992
|
+
|
|
1993
|
+
# Monitor progress while processing
|
|
1994
|
+
while not result_objects.ready():
|
|
1995
|
+
if progress_dialog.wasCanceled():
|
|
1996
|
+
pool.terminate()
|
|
1997
|
+
self.status_text.appendPlainText(
|
|
1998
|
+
"Operation canceled by user."
|
|
1999
|
+
)
|
|
2000
|
+
break
|
|
2001
|
+
time.sleep(0.1) # Short sleep to prevent UI blocking
|
|
2002
|
+
QApplication.processEvents()
|
|
2003
|
+
|
|
2004
|
+
# Process results if not canceled
|
|
2005
|
+
if not progress_dialog.wasCanceled():
|
|
2006
|
+
results = result_objects.get()
|
|
2007
|
+
|
|
2008
|
+
# Process results
|
|
2009
|
+
for result in results:
|
|
2010
|
+
basename = os.path.basename(result["input_file"])
|
|
2011
|
+
|
|
2012
|
+
if result["success"]:
|
|
2013
|
+
success_count += 1
|
|
2014
|
+
self.status_text.appendPlainText(
|
|
2015
|
+
f" - {basename}: Converted successfully"
|
|
2016
|
+
)
|
|
2017
|
+
else:
|
|
2018
|
+
error_count += 1
|
|
2019
|
+
error_msg = result["error"] or "Unknown error"
|
|
2020
|
+
self.status_text.appendPlainText(
|
|
2021
|
+
f" - {basename}: Error: {error_msg}"
|
|
2022
|
+
)
|
|
2023
|
+
|
|
2024
|
+
# Update progress to completion
|
|
2025
|
+
progress_dialog.setValue(total_tasks)
|
|
2026
|
+
else:
|
|
2027
|
+
# Sequential processing for single stokes
|
|
2028
|
+
for i, input_file in enumerate(files_to_process):
|
|
2029
|
+
# Check if canceled
|
|
2030
|
+
if progress_dialog.wasCanceled():
|
|
2031
|
+
self.status_text.appendPlainText(
|
|
2032
|
+
"Operation canceled by user."
|
|
2033
|
+
)
|
|
2034
|
+
break
|
|
2035
|
+
|
|
2036
|
+
# Get output filename
|
|
2037
|
+
base_filename = os.path.basename(input_file)
|
|
2038
|
+
if "*" in output_pattern:
|
|
2039
|
+
output_filename = output_pattern.replace(
|
|
2040
|
+
"*", os.path.splitext(base_filename)[0]
|
|
2041
|
+
)
|
|
2042
|
+
else:
|
|
2043
|
+
output_filename = (
|
|
2044
|
+
f"{os.path.splitext(base_filename)[0]}_{output_pattern}"
|
|
2045
|
+
)
|
|
2046
|
+
|
|
2047
|
+
output_path = os.path.join(output_dir, output_filename)
|
|
2048
|
+
|
|
2049
|
+
# Update progress dialog
|
|
2050
|
+
progress_dialog.setValue(i)
|
|
2051
|
+
progress_dialog.setLabelText(f"Converting: {base_filename}")
|
|
2052
|
+
QApplication.processEvents()
|
|
2053
|
+
|
|
2054
|
+
self.status_text.appendPlainText(
|
|
2055
|
+
f"Processing {i+1}/{len(files_to_process)}: {base_filename}"
|
|
2056
|
+
)
|
|
2057
|
+
|
|
2058
|
+
try:
|
|
2059
|
+
# Convert file with a unique temp suffix
|
|
2060
|
+
temp_suffix = f"_seq_{i}"
|
|
2061
|
+
result = process_single_file_hpc(
|
|
2062
|
+
(input_file, output_path, stokes_param, f"_seq_{i}")
|
|
2063
|
+
)
|
|
2064
|
+
success = result["success"]
|
|
2065
|
+
|
|
2066
|
+
if success:
|
|
2067
|
+
success_count += 1
|
|
2068
|
+
self.status_text.appendPlainText(
|
|
2069
|
+
" - Converted successfully"
|
|
2070
|
+
)
|
|
2071
|
+
else:
|
|
2072
|
+
error_count += 1
|
|
2073
|
+
self.status_text.appendPlainText(
|
|
2074
|
+
" - Conversion failed"
|
|
2075
|
+
)
|
|
2076
|
+
|
|
2077
|
+
except Exception as e:
|
|
2078
|
+
error_count += 1
|
|
2079
|
+
self.status_text.appendPlainText(f" - Error: {str(e)}")
|
|
2080
|
+
|
|
2081
|
+
# Complete the progress
|
|
2082
|
+
progress_dialog.setValue(progress_dialog.maximum())
|
|
2083
|
+
|
|
2084
|
+
# Show completion message
|
|
2085
|
+
summary = (
|
|
2086
|
+
f"Batch conversion completed:\n"
|
|
2087
|
+
f"Total files: {len(files_to_process)}\n"
|
|
2088
|
+
f"Successfully converted: {success_count}\n"
|
|
2089
|
+
f"Failed: {error_count}"
|
|
2090
|
+
)
|
|
2091
|
+
|
|
2092
|
+
self.status_text.appendPlainText("\n" + summary)
|
|
2093
|
+
QMessageBox.information(self, "Conversion Complete", summary)
|
|
2094
|
+
|
|
2095
|
+
except Exception as e:
|
|
2096
|
+
self.status_text.appendPlainText(f"Error in batch processing: {str(e)}")
|
|
2097
|
+
self.status_text.appendPlainText(traceback.format_exc())
|
|
2098
|
+
QMessageBox.critical(self, "Error", f"Error in batch processing: {str(e)}")
|
|
2099
|
+
finally:
|
|
2100
|
+
# Clean up multiprocessing pool if it exists
|
|
2101
|
+
if pool is not None:
|
|
2102
|
+
pool.terminate()
|
|
2103
|
+
pool.join()
|
|
2104
|
+
|
|
2105
|
+
# Close progress dialog and re-enable button
|
|
2106
|
+
progress_dialog.close()
|
|
2107
|
+
self.ok_button.setEnabled(True)
|
|
2108
|
+
|
|
2109
|
+
|
|
2110
|
+
class PlotCustomizationDialog(QDialog):
|
|
2111
|
+
"""Dialog for customizing plot appearance (labels, fonts, colors)."""
|
|
2112
|
+
|
|
2113
|
+
def __init__(self, parent=None, settings=None):
|
|
2114
|
+
super().__init__(parent)
|
|
2115
|
+
self.setWindowTitle("Plot Customization")
|
|
2116
|
+
self.setMinimumWidth(660)
|
|
2117
|
+
self.setMaximumHeight(1280)
|
|
2118
|
+
self.settings = settings.copy() if settings else {}
|
|
2119
|
+
self.setup_ui()
|
|
2120
|
+
|
|
2121
|
+
def setup_ui(self):
|
|
2122
|
+
from PyQt5.QtWidgets import QTabWidget
|
|
2123
|
+
|
|
2124
|
+
outer_layout = QVBoxLayout(self)
|
|
2125
|
+
outer_layout.setSpacing(8)
|
|
2126
|
+
outer_layout.setContentsMargins(10, 10, 10, 10)
|
|
2127
|
+
|
|
2128
|
+
# Create tab widget for organized sections
|
|
2129
|
+
tab_widget = QTabWidget()
|
|
2130
|
+
|
|
2131
|
+
# ===== TAB 1: TEXT & LABELS =====
|
|
2132
|
+
text_tab = QWidget()
|
|
2133
|
+
text_layout = QVBoxLayout(text_tab)
|
|
2134
|
+
text_layout.setSpacing(8)
|
|
2135
|
+
|
|
2136
|
+
# Labels Section
|
|
2137
|
+
labels_group = QGroupBox("Labels")
|
|
2138
|
+
labels_layout = QGridLayout(labels_group)
|
|
2139
|
+
labels_layout.setSpacing(8)
|
|
2140
|
+
|
|
2141
|
+
labels_layout.addWidget(QLabel("X-Axis:"), 0, 0)
|
|
2142
|
+
self.xlabel_edit = QLineEdit(self.settings.get("xlabel", ""))
|
|
2143
|
+
self.xlabel_edit.setPlaceholderText("Auto")
|
|
2144
|
+
labels_layout.addWidget(self.xlabel_edit, 0, 1)
|
|
2145
|
+
|
|
2146
|
+
labels_layout.addWidget(QLabel("Y-Axis:"), 0, 2)
|
|
2147
|
+
self.ylabel_edit = QLineEdit(self.settings.get("ylabel", ""))
|
|
2148
|
+
self.ylabel_edit.setPlaceholderText("Auto")
|
|
2149
|
+
labels_layout.addWidget(self.ylabel_edit, 0, 3)
|
|
2150
|
+
|
|
2151
|
+
labels_layout.addWidget(QLabel("Title:"), 1, 0)
|
|
2152
|
+
self.title_edit = QLineEdit(self.settings.get("title", ""))
|
|
2153
|
+
self.title_edit.setPlaceholderText("Auto")
|
|
2154
|
+
labels_layout.addWidget(self.title_edit, 1, 1)
|
|
2155
|
+
|
|
2156
|
+
labels_layout.addWidget(QLabel("Colorbar:"), 1, 2)
|
|
2157
|
+
self.colorbar_label_edit = QLineEdit(self.settings.get("colorbar_label", ""))
|
|
2158
|
+
self.colorbar_label_edit.setPlaceholderText("e.g., Jy/beam")
|
|
2159
|
+
labels_layout.addWidget(self.colorbar_label_edit, 1, 3)
|
|
2160
|
+
|
|
2161
|
+
text_layout.addWidget(labels_group)
|
|
2162
|
+
|
|
2163
|
+
# Font Sizes Section (compact grid)
|
|
2164
|
+
fonts_group = QGroupBox("Font Sizes")
|
|
2165
|
+
fonts_layout = QGridLayout(fonts_group)
|
|
2166
|
+
fonts_layout.setSpacing(8)
|
|
2167
|
+
|
|
2168
|
+
fonts_layout.addWidget(QLabel("Axis Labels:"), 0, 0)
|
|
2169
|
+
self.axis_label_size = QSpinBox()
|
|
2170
|
+
self.axis_label_size.setRange(1, 50)
|
|
2171
|
+
self.axis_label_size.setValue(self.settings.get("axis_label_fontsize", 12))
|
|
2172
|
+
fonts_layout.addWidget(self.axis_label_size, 0, 1)
|
|
2173
|
+
|
|
2174
|
+
fonts_layout.addWidget(QLabel("Axis Ticks:"), 0, 2)
|
|
2175
|
+
self.axis_tick_size = QSpinBox()
|
|
2176
|
+
self.axis_tick_size.setRange(1, 50)
|
|
2177
|
+
self.axis_tick_size.setValue(self.settings.get("axis_tick_fontsize", 10))
|
|
2178
|
+
fonts_layout.addWidget(self.axis_tick_size, 0, 3)
|
|
2179
|
+
|
|
2180
|
+
fonts_layout.addWidget(QLabel("Title:"), 1, 0)
|
|
2181
|
+
self.title_size = QSpinBox()
|
|
2182
|
+
self.title_size.setRange(1, 50)
|
|
2183
|
+
self.title_size.setValue(self.settings.get("title_fontsize", 12))
|
|
2184
|
+
fonts_layout.addWidget(self.title_size, 1, 1)
|
|
2185
|
+
|
|
2186
|
+
fonts_layout.addWidget(QLabel("Colorbar:"), 1, 2)
|
|
2187
|
+
self.colorbar_label_size = QSpinBox()
|
|
2188
|
+
self.colorbar_label_size.setRange(1, 50)
|
|
2189
|
+
self.colorbar_label_size.setValue(self.settings.get("colorbar_label_fontsize", 10))
|
|
2190
|
+
fonts_layout.addWidget(self.colorbar_label_size, 1, 3)
|
|
2191
|
+
|
|
2192
|
+
fonts_layout.addWidget(QLabel("Colorbar Ticks:"), 2, 0)
|
|
2193
|
+
self.colorbar_tick_size = QSpinBox()
|
|
2194
|
+
self.colorbar_tick_size.setRange(1, 50)
|
|
2195
|
+
self.colorbar_tick_size.setValue(self.settings.get("colorbar_tick_fontsize", 10))
|
|
2196
|
+
fonts_layout.addWidget(self.colorbar_tick_size, 2, 1)
|
|
2197
|
+
|
|
2198
|
+
# Scale buttons
|
|
2199
|
+
scale_layout = QHBoxLayout()
|
|
2200
|
+
scale_layout.addWidget(QLabel("Scale All:"))
|
|
2201
|
+
scale_down_btn = QPushButton("-")
|
|
2202
|
+
scale_down_btn.setFixedWidth(30)
|
|
2203
|
+
scale_down_btn.clicked.connect(self._scale_fonts_down)
|
|
2204
|
+
scale_up_btn = QPushButton("+")
|
|
2205
|
+
scale_up_btn.setFixedWidth(30)
|
|
2206
|
+
scale_up_btn.clicked.connect(self._scale_fonts_up)
|
|
2207
|
+
scale_layout.addWidget(scale_down_btn)
|
|
2208
|
+
scale_layout.addWidget(scale_up_btn)
|
|
2209
|
+
scale_layout.addStretch()
|
|
2210
|
+
fonts_layout.addLayout(scale_layout, 2, 2, 1, 2)
|
|
2211
|
+
|
|
2212
|
+
text_layout.addWidget(fonts_group)
|
|
2213
|
+
text_layout.addStretch()
|
|
2214
|
+
|
|
2215
|
+
tab_widget.addTab(text_tab, "Text")
|
|
2216
|
+
|
|
2217
|
+
# ===== TAB 2: COLORS & STYLE =====
|
|
2218
|
+
style_tab = QWidget()
|
|
2219
|
+
style_layout = QVBoxLayout(style_tab)
|
|
2220
|
+
style_layout.setSpacing(8)
|
|
2221
|
+
|
|
2222
|
+
# Colors Section (compact 2-column grid)
|
|
2223
|
+
colors_group = QGroupBox("Colors")
|
|
2224
|
+
colors_layout = QGridLayout(colors_group)
|
|
2225
|
+
colors_layout.setSpacing(6)
|
|
2226
|
+
|
|
2227
|
+
# Row 0: Plot BG, Figure BG
|
|
2228
|
+
colors_layout.addWidget(QLabel("Plot BG:"), 0, 0)
|
|
2229
|
+
self.plot_bg_color = self.settings.get("plot_bg_color", "auto")
|
|
2230
|
+
self.plot_bg_preview = QLabel()
|
|
2231
|
+
self.plot_bg_preview.setFixedSize(20, 20)
|
|
2232
|
+
self._update_color_preview(self.plot_bg_preview, self.plot_bg_color)
|
|
2233
|
+
self.plot_bg_btn = QPushButton("...")
|
|
2234
|
+
self.plot_bg_btn.setFixedWidth(30)
|
|
2235
|
+
self.plot_bg_btn.clicked.connect(self._pick_plot_bg_color)
|
|
2236
|
+
self.plot_bg_auto_btn = QPushButton("A")
|
|
2237
|
+
self.plot_bg_auto_btn.setFixedWidth(25)
|
|
2238
|
+
self.plot_bg_auto_btn.setToolTip("Auto")
|
|
2239
|
+
self.plot_bg_auto_btn.clicked.connect(lambda: self._set_plot_bg_auto())
|
|
2240
|
+
plot_bg_row = QHBoxLayout()
|
|
2241
|
+
plot_bg_row.addWidget(self.plot_bg_preview)
|
|
2242
|
+
plot_bg_row.addWidget(self.plot_bg_btn)
|
|
2243
|
+
plot_bg_row.addWidget(self.plot_bg_auto_btn)
|
|
2244
|
+
colors_layout.addLayout(plot_bg_row, 0, 1)
|
|
2245
|
+
|
|
2246
|
+
colors_layout.addWidget(QLabel("Figure BG:"), 0, 2)
|
|
2247
|
+
self.figure_bg_color = self.settings.get("figure_bg_color", "auto")
|
|
2248
|
+
self.figure_bg_preview = QLabel()
|
|
2249
|
+
self.figure_bg_preview.setFixedSize(20, 20)
|
|
2250
|
+
self._update_color_preview(self.figure_bg_preview, self.figure_bg_color)
|
|
2251
|
+
self.figure_bg_btn = QPushButton("...")
|
|
2252
|
+
self.figure_bg_btn.setFixedWidth(30)
|
|
2253
|
+
self.figure_bg_btn.clicked.connect(self._pick_figure_bg_color)
|
|
2254
|
+
self.figure_bg_auto_btn = QPushButton("A")
|
|
2255
|
+
self.figure_bg_auto_btn.setFixedWidth(25)
|
|
2256
|
+
self.figure_bg_auto_btn.setToolTip("Auto")
|
|
2257
|
+
self.figure_bg_auto_btn.clicked.connect(lambda: self._set_figure_bg_auto())
|
|
2258
|
+
figure_bg_row = QHBoxLayout()
|
|
2259
|
+
figure_bg_row.addWidget(self.figure_bg_preview)
|
|
2260
|
+
figure_bg_row.addWidget(self.figure_bg_btn)
|
|
2261
|
+
figure_bg_row.addWidget(self.figure_bg_auto_btn)
|
|
2262
|
+
colors_layout.addLayout(figure_bg_row, 0, 3)
|
|
2263
|
+
|
|
2264
|
+
# Row 1: Text, Tick
|
|
2265
|
+
colors_layout.addWidget(QLabel("Text:"), 1, 0)
|
|
2266
|
+
self.text_color = self.settings.get("text_color", "auto")
|
|
2267
|
+
self.text_color_preview = QLabel()
|
|
2268
|
+
self.text_color_preview.setFixedSize(20, 20)
|
|
2269
|
+
self._update_color_preview(self.text_color_preview, self.text_color)
|
|
2270
|
+
self.text_color_btn = QPushButton("...")
|
|
2271
|
+
self.text_color_btn.setFixedWidth(30)
|
|
2272
|
+
self.text_color_btn.clicked.connect(self._pick_text_color)
|
|
2273
|
+
self.text_color_auto_btn = QPushButton("A")
|
|
2274
|
+
self.text_color_auto_btn.setFixedWidth(25)
|
|
2275
|
+
self.text_color_auto_btn.setToolTip("Auto")
|
|
2276
|
+
self.text_color_auto_btn.clicked.connect(self._set_text_color_auto)
|
|
2277
|
+
text_color_row = QHBoxLayout()
|
|
2278
|
+
text_color_row.addWidget(self.text_color_preview)
|
|
2279
|
+
text_color_row.addWidget(self.text_color_btn)
|
|
2280
|
+
text_color_row.addWidget(self.text_color_auto_btn)
|
|
2281
|
+
colors_layout.addLayout(text_color_row, 1, 1)
|
|
2282
|
+
|
|
2283
|
+
colors_layout.addWidget(QLabel("Ticks:"), 1, 2)
|
|
2284
|
+
self.tick_color = self.settings.get("tick_color", "auto")
|
|
2285
|
+
self.tick_color_preview = QLabel()
|
|
2286
|
+
self.tick_color_preview.setFixedSize(20, 20)
|
|
2287
|
+
self._update_color_preview(self.tick_color_preview, self.tick_color)
|
|
2288
|
+
self.tick_color_btn = QPushButton("...")
|
|
2289
|
+
self.tick_color_btn.setFixedWidth(30)
|
|
2290
|
+
self.tick_color_btn.clicked.connect(self._pick_tick_color)
|
|
2291
|
+
self.tick_color_auto_btn = QPushButton("A")
|
|
2292
|
+
self.tick_color_auto_btn.setFixedWidth(25)
|
|
2293
|
+
self.tick_color_auto_btn.setToolTip("Auto")
|
|
2294
|
+
self.tick_color_auto_btn.clicked.connect(self._set_tick_color_auto)
|
|
2295
|
+
tick_color_row = QHBoxLayout()
|
|
2296
|
+
tick_color_row.addWidget(self.tick_color_preview)
|
|
2297
|
+
tick_color_row.addWidget(self.tick_color_btn)
|
|
2298
|
+
tick_color_row.addWidget(self.tick_color_auto_btn)
|
|
2299
|
+
colors_layout.addLayout(tick_color_row, 1, 3)
|
|
2300
|
+
|
|
2301
|
+
# Row 2: Border color + width
|
|
2302
|
+
colors_layout.addWidget(QLabel("Border:"), 2, 0)
|
|
2303
|
+
self.border_color = self.settings.get("border_color", "auto")
|
|
2304
|
+
self.border_color_preview = QLabel()
|
|
2305
|
+
self.border_color_preview.setFixedSize(20, 20)
|
|
2306
|
+
self._update_color_preview(self.border_color_preview, self.border_color)
|
|
2307
|
+
self.border_color_btn = QPushButton("...")
|
|
2308
|
+
self.border_color_btn.setFixedWidth(30)
|
|
2309
|
+
self.border_color_btn.clicked.connect(self._pick_border_color)
|
|
2310
|
+
self.border_color_auto_btn = QPushButton("A")
|
|
2311
|
+
self.border_color_auto_btn.setFixedWidth(25)
|
|
2312
|
+
self.border_color_auto_btn.setToolTip("Auto")
|
|
2313
|
+
self.border_color_auto_btn.clicked.connect(self._set_border_color_auto)
|
|
2314
|
+
border_color_row = QHBoxLayout()
|
|
2315
|
+
border_color_row.addWidget(self.border_color_preview)
|
|
2316
|
+
border_color_row.addWidget(self.border_color_btn)
|
|
2317
|
+
border_color_row.addWidget(self.border_color_auto_btn)
|
|
2318
|
+
colors_layout.addLayout(border_color_row, 2, 1)
|
|
2319
|
+
|
|
2320
|
+
colors_layout.addWidget(QLabel("Border Width:"), 2, 2)
|
|
2321
|
+
self.border_width = QDoubleSpinBox()
|
|
2322
|
+
self.border_width.setRange(0.5, 5.0)
|
|
2323
|
+
self.border_width.setSingleStep(0.5)
|
|
2324
|
+
self.border_width.setValue(self.settings.get("border_width", 1.0))
|
|
2325
|
+
colors_layout.addWidget(self.border_width, 2, 3)
|
|
2326
|
+
|
|
2327
|
+
style_layout.addWidget(colors_group)
|
|
2328
|
+
|
|
2329
|
+
# Tick Marks Section (compact)
|
|
2330
|
+
ticks_group = QGroupBox("Tick Marks")
|
|
2331
|
+
ticks_layout = QGridLayout(ticks_group)
|
|
2332
|
+
ticks_layout.setSpacing(8)
|
|
2333
|
+
|
|
2334
|
+
ticks_layout.addWidget(QLabel("Direction:"), 0, 0)
|
|
2335
|
+
self.tick_direction = QComboBox()
|
|
2336
|
+
self.tick_direction.addItems(["in", "out"])
|
|
2337
|
+
self.tick_direction.setCurrentText(self.settings.get("tick_direction", "out"))
|
|
2338
|
+
ticks_layout.addWidget(self.tick_direction, 0, 1)
|
|
2339
|
+
|
|
2340
|
+
ticks_layout.addWidget(QLabel("Length:"), 0, 2)
|
|
2341
|
+
self.tick_length = QSpinBox()
|
|
2342
|
+
self.tick_length.setRange(1, 20)
|
|
2343
|
+
self.tick_length.setValue(self.settings.get("tick_length", 4))
|
|
2344
|
+
ticks_layout.addWidget(self.tick_length, 0, 3)
|
|
2345
|
+
|
|
2346
|
+
ticks_layout.addWidget(QLabel("Width:"), 1, 0)
|
|
2347
|
+
self.tick_width = QDoubleSpinBox()
|
|
2348
|
+
self.tick_width.setRange(0.5, 5.0)
|
|
2349
|
+
self.tick_width.setSingleStep(0.5)
|
|
2350
|
+
self.tick_width.setValue(self.settings.get("tick_width", 1.0))
|
|
2351
|
+
ticks_layout.addWidget(self.tick_width, 1, 1)
|
|
2352
|
+
|
|
2353
|
+
style_layout.addWidget(ticks_group)
|
|
2354
|
+
style_layout.addStretch()
|
|
2355
|
+
|
|
2356
|
+
tab_widget.addTab(style_tab, "Style")
|
|
2357
|
+
|
|
2358
|
+
# ===== TAB 3: PADDING =====
|
|
2359
|
+
padding_tab = QWidget()
|
|
2360
|
+
padding_layout = QVBoxLayout(padding_tab)
|
|
2361
|
+
padding_layout.setSpacing(8)
|
|
2362
|
+
|
|
2363
|
+
# Subplot Margins Section
|
|
2364
|
+
margins_group = QGroupBox("Plot Margins (0.0 - 1.0)")
|
|
2365
|
+
margins_layout = QGridLayout(margins_group)
|
|
2366
|
+
margins_layout.setSpacing(10)
|
|
2367
|
+
|
|
2368
|
+
# Left
|
|
2369
|
+
margins_layout.addWidget(QLabel("Left:"), 0, 0)
|
|
2370
|
+
self.pad_left = QDoubleSpinBox()
|
|
2371
|
+
self.pad_left.setRange(0.0, 0.5)
|
|
2372
|
+
self.pad_left.setSingleStep(0.01)
|
|
2373
|
+
self.pad_left.setDecimals(2)
|
|
2374
|
+
self.pad_left.setValue(self.settings.get("pad_left", 0.135))
|
|
2375
|
+
margins_layout.addWidget(self.pad_left, 0, 1)
|
|
2376
|
+
|
|
2377
|
+
# Right
|
|
2378
|
+
margins_layout.addWidget(QLabel("Right:"), 0, 2)
|
|
2379
|
+
self.pad_right = QDoubleSpinBox()
|
|
2380
|
+
self.pad_right.setRange(0.5, 1.0)
|
|
2381
|
+
self.pad_right.setSingleStep(0.01)
|
|
2382
|
+
self.pad_right.setDecimals(2)
|
|
2383
|
+
self.pad_right.setValue(self.settings.get("pad_right", 1.0))
|
|
2384
|
+
margins_layout.addWidget(self.pad_right, 0, 3)
|
|
2385
|
+
|
|
2386
|
+
# Top
|
|
2387
|
+
margins_layout.addWidget(QLabel("Top:"), 1, 0)
|
|
2388
|
+
self.pad_top = QDoubleSpinBox()
|
|
2389
|
+
self.pad_top.setRange(0.5, 1.0)
|
|
2390
|
+
self.pad_top.setSingleStep(0.01)
|
|
2391
|
+
self.pad_top.setDecimals(2)
|
|
2392
|
+
self.pad_top.setValue(self.settings.get("pad_top", 0.95))
|
|
2393
|
+
margins_layout.addWidget(self.pad_top, 1, 1)
|
|
2394
|
+
|
|
2395
|
+
# Bottom
|
|
2396
|
+
margins_layout.addWidget(QLabel("Bottom:"), 1, 2)
|
|
2397
|
+
self.pad_bottom = QDoubleSpinBox()
|
|
2398
|
+
self.pad_bottom.setRange(0.0, 0.5)
|
|
2399
|
+
self.pad_bottom.setSingleStep(0.01)
|
|
2400
|
+
self.pad_bottom.setDecimals(2)
|
|
2401
|
+
self.pad_bottom.setValue(self.settings.get("pad_bottom", 0.05))
|
|
2402
|
+
margins_layout.addWidget(self.pad_bottom, 1, 3)
|
|
2403
|
+
|
|
2404
|
+
# Wspace (width space between subplots)
|
|
2405
|
+
margins_layout.addWidget(QLabel("Wspace:"), 2, 0)
|
|
2406
|
+
self.pad_wspace = QDoubleSpinBox()
|
|
2407
|
+
self.pad_wspace.setRange(0.0, 0.5)
|
|
2408
|
+
self.pad_wspace.setSingleStep(0.01)
|
|
2409
|
+
self.pad_wspace.setDecimals(2)
|
|
2410
|
+
self.pad_wspace.setValue(self.settings.get("pad_wspace", 0.2))
|
|
2411
|
+
self.pad_wspace.setToolTip("Width space between subplots")
|
|
2412
|
+
margins_layout.addWidget(self.pad_wspace, 2, 1)
|
|
2413
|
+
|
|
2414
|
+
# Hspace (height space between subplots)
|
|
2415
|
+
margins_layout.addWidget(QLabel("Hspace:"), 2, 2)
|
|
2416
|
+
self.pad_hspace = QDoubleSpinBox()
|
|
2417
|
+
self.pad_hspace.setRange(0.0, 0.5)
|
|
2418
|
+
self.pad_hspace.setSingleStep(0.01)
|
|
2419
|
+
self.pad_hspace.setDecimals(2)
|
|
2420
|
+
self.pad_hspace.setValue(self.settings.get("pad_hspace", 0.2))
|
|
2421
|
+
self.pad_hspace.setToolTip("Height space between subplots")
|
|
2422
|
+
margins_layout.addWidget(self.pad_hspace, 2, 3)
|
|
2423
|
+
|
|
2424
|
+
padding_layout.addWidget(margins_group)
|
|
2425
|
+
|
|
2426
|
+
# Tight Layout Option
|
|
2427
|
+
tight_group = QGroupBox("Layout Options")
|
|
2428
|
+
tight_layout_grid = QGridLayout(tight_group)
|
|
2429
|
+
|
|
2430
|
+
self.use_tight_layout = QCheckBox("Use Tight Layout")
|
|
2431
|
+
self.use_tight_layout.setChecked(self.settings.get("use_tight_layout", False))
|
|
2432
|
+
self.use_tight_layout.setToolTip("Automatically adjust margins for best fit")
|
|
2433
|
+
tight_layout_grid.addWidget(self.use_tight_layout, 0, 0)
|
|
2434
|
+
|
|
2435
|
+
padding_layout.addWidget(tight_group)
|
|
2436
|
+
padding_layout.addStretch()
|
|
2437
|
+
|
|
2438
|
+
tab_widget.addTab(padding_tab, "Padding")
|
|
2439
|
+
|
|
2440
|
+
outer_layout.addWidget(tab_widget)
|
|
2441
|
+
|
|
2442
|
+
# Bottom buttons row
|
|
2443
|
+
buttons_layout = QHBoxLayout()
|
|
2444
|
+
reset_btn = QPushButton("Reset to Defaults")
|
|
2445
|
+
reset_btn.clicked.connect(self.reset_to_defaults)
|
|
2446
|
+
buttons_layout.addWidget(reset_btn)
|
|
2447
|
+
buttons_layout.addStretch()
|
|
2448
|
+
|
|
2449
|
+
button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
|
2450
|
+
button_box.accepted.connect(self.accept)
|
|
2451
|
+
button_box.rejected.connect(self.reject)
|
|
2452
|
+
buttons_layout.addWidget(button_box)
|
|
2453
|
+
|
|
2454
|
+
outer_layout.addLayout(buttons_layout)
|
|
2455
|
+
|
|
2456
|
+
def _update_color_preview(self, label, color):
|
|
2457
|
+
"""Update the color preview label."""
|
|
2458
|
+
if color == "auto" or color == "transparent":
|
|
2459
|
+
label.setStyleSheet("background-color: #888888; border: 1px solid #555555;")
|
|
2460
|
+
label.setText("A" if color == "auto" else "T")
|
|
2461
|
+
label.setAlignment(Qt.AlignCenter)
|
|
2462
|
+
else:
|
|
2463
|
+
label.setStyleSheet(f"background-color: {color}; border: 1px solid #555555;")
|
|
2464
|
+
label.setText("")
|
|
2465
|
+
|
|
2466
|
+
def _pick_plot_bg_color(self):
|
|
2467
|
+
"""Open color picker for plot background."""
|
|
2468
|
+
from PyQt5.QtWidgets import QColorDialog, QPushButton
|
|
2469
|
+
from PyQt5.QtGui import QColor
|
|
2470
|
+
|
|
2471
|
+
initial = QColor(self.plot_bg_color) if self.plot_bg_color not in ("auto", "transparent") else QColor("#ffffff")
|
|
2472
|
+
dialog = QColorDialog(initial, self)
|
|
2473
|
+
dialog.setWindowTitle("Select Plot Background Color")
|
|
2474
|
+
dialog.setOption(QColorDialog.DontUseNativeDialog, True)
|
|
2475
|
+
|
|
2476
|
+
# Hide the "Pick Screen Color" button
|
|
2477
|
+
for btn in dialog.findChildren(QPushButton):
|
|
2478
|
+
if "pick" in btn.text().lower() or "screen" in btn.text().lower():
|
|
2479
|
+
btn.hide()
|
|
2480
|
+
|
|
2481
|
+
if dialog.exec_() == QColorDialog.Accepted:
|
|
2482
|
+
self.plot_bg_color = dialog.selectedColor().name()
|
|
2483
|
+
self._update_color_preview(self.plot_bg_preview, self.plot_bg_color)
|
|
2484
|
+
|
|
2485
|
+
def _pick_figure_bg_color(self):
|
|
2486
|
+
"""Open color picker for figure background."""
|
|
2487
|
+
from PyQt5.QtWidgets import QColorDialog, QPushButton
|
|
2488
|
+
from PyQt5.QtGui import QColor
|
|
2489
|
+
|
|
2490
|
+
initial = QColor(self.figure_bg_color) if self.figure_bg_color not in ("auto", "transparent") else QColor("#ffffff")
|
|
2491
|
+
dialog = QColorDialog(initial, self)
|
|
2492
|
+
dialog.setWindowTitle("Select Figure Background Color")
|
|
2493
|
+
dialog.setOption(QColorDialog.DontUseNativeDialog, True)
|
|
2494
|
+
|
|
2495
|
+
# Hide the "Pick Screen Color" button
|
|
2496
|
+
for btn in dialog.findChildren(QPushButton):
|
|
2497
|
+
if "pick" in btn.text().lower() or "screen" in btn.text().lower():
|
|
2498
|
+
btn.hide()
|
|
2499
|
+
|
|
2500
|
+
if dialog.exec_() == QColorDialog.Accepted:
|
|
2501
|
+
self.figure_bg_color = dialog.selectedColor().name()
|
|
2502
|
+
self._update_color_preview(self.figure_bg_preview, self.figure_bg_color)
|
|
2503
|
+
|
|
2504
|
+
def _set_plot_bg_auto(self):
|
|
2505
|
+
"""Set plot background to auto."""
|
|
2506
|
+
self.plot_bg_color = "auto"
|
|
2507
|
+
self._update_color_preview(self.plot_bg_preview, "auto")
|
|
2508
|
+
|
|
2509
|
+
def _set_figure_bg_auto(self):
|
|
2510
|
+
"""Set figure background to auto."""
|
|
2511
|
+
self.figure_bg_color = "auto"
|
|
2512
|
+
self._update_color_preview(self.figure_bg_preview, "auto")
|
|
2513
|
+
|
|
2514
|
+
def _pick_text_color(self):
|
|
2515
|
+
"""Open color picker for text color."""
|
|
2516
|
+
from PyQt5.QtWidgets import QColorDialog, QPushButton
|
|
2517
|
+
from PyQt5.QtGui import QColor
|
|
2518
|
+
|
|
2519
|
+
initial = QColor(self.text_color) if self.text_color not in ("auto", "transparent") else QColor("#ffffff")
|
|
2520
|
+
dialog = QColorDialog(initial, self)
|
|
2521
|
+
dialog.setWindowTitle("Select Text Color")
|
|
2522
|
+
dialog.setOption(QColorDialog.DontUseNativeDialog, True)
|
|
2523
|
+
|
|
2524
|
+
# Hide the "Pick Screen Color" button
|
|
2525
|
+
for btn in dialog.findChildren(QPushButton):
|
|
2526
|
+
if "pick" in btn.text().lower() or "screen" in btn.text().lower():
|
|
2527
|
+
btn.hide()
|
|
2528
|
+
|
|
2529
|
+
if dialog.exec_() == QColorDialog.Accepted:
|
|
2530
|
+
self.text_color = dialog.selectedColor().name()
|
|
2531
|
+
self._update_color_preview(self.text_color_preview, self.text_color)
|
|
2532
|
+
|
|
2533
|
+
def _set_text_color_auto(self):
|
|
2534
|
+
"""Set text color to auto."""
|
|
2535
|
+
self.text_color = "auto"
|
|
2536
|
+
self._update_color_preview(self.text_color_preview, "auto")
|
|
2537
|
+
|
|
2538
|
+
def _pick_tick_color(self):
|
|
2539
|
+
"""Open color picker for tick color."""
|
|
2540
|
+
from PyQt5.QtWidgets import QColorDialog, QPushButton
|
|
2541
|
+
from PyQt5.QtGui import QColor
|
|
2542
|
+
|
|
2543
|
+
initial = QColor(self.tick_color) if self.tick_color not in ("auto", "transparent") else QColor("#ffffff")
|
|
2544
|
+
dialog = QColorDialog(initial, self)
|
|
2545
|
+
dialog.setWindowTitle("Select Tick Color")
|
|
2546
|
+
dialog.setOption(QColorDialog.DontUseNativeDialog, True)
|
|
2547
|
+
|
|
2548
|
+
# Hide the "Pick Screen Color" button
|
|
2549
|
+
for btn in dialog.findChildren(QPushButton):
|
|
2550
|
+
if "pick" in btn.text().lower() or "screen" in btn.text().lower():
|
|
2551
|
+
btn.hide()
|
|
2552
|
+
|
|
2553
|
+
if dialog.exec_() == QColorDialog.Accepted:
|
|
2554
|
+
self.tick_color = dialog.selectedColor().name()
|
|
2555
|
+
self._update_color_preview(self.tick_color_preview, self.tick_color)
|
|
2556
|
+
|
|
2557
|
+
def _set_tick_color_auto(self):
|
|
2558
|
+
"""Set tick color to auto."""
|
|
2559
|
+
self.tick_color = "auto"
|
|
2560
|
+
self._update_color_preview(self.tick_color_preview, "auto")
|
|
2561
|
+
|
|
2562
|
+
def _pick_border_color(self):
|
|
2563
|
+
"""Open color picker for border color."""
|
|
2564
|
+
from PyQt5.QtWidgets import QColorDialog, QPushButton
|
|
2565
|
+
from PyQt5.QtGui import QColor
|
|
2566
|
+
|
|
2567
|
+
initial = QColor(self.border_color) if self.border_color not in ("auto", "transparent") else QColor("#ffffff")
|
|
2568
|
+
dialog = QColorDialog(initial, self)
|
|
2569
|
+
dialog.setWindowTitle("Select Border Color")
|
|
2570
|
+
dialog.setOption(QColorDialog.DontUseNativeDialog, True)
|
|
2571
|
+
|
|
2572
|
+
# Hide the "Pick Screen Color" button
|
|
2573
|
+
for btn in dialog.findChildren(QPushButton):
|
|
2574
|
+
if "pick" in btn.text().lower() or "screen" in btn.text().lower():
|
|
2575
|
+
btn.hide()
|
|
2576
|
+
|
|
2577
|
+
if dialog.exec_() == QColorDialog.Accepted:
|
|
2578
|
+
self.border_color = dialog.selectedColor().name()
|
|
2579
|
+
self._update_color_preview(self.border_color_preview, self.border_color)
|
|
2580
|
+
|
|
2581
|
+
def _set_border_color_auto(self):
|
|
2582
|
+
"""Set border color to auto."""
|
|
2583
|
+
self.border_color = "auto"
|
|
2584
|
+
self._update_color_preview(self.border_color_preview, "auto")
|
|
2585
|
+
|
|
2586
|
+
def _scale_fonts_up(self):
|
|
2587
|
+
"""Increase all font sizes by 1."""
|
|
2588
|
+
self.axis_label_size.setValue(min(self.axis_label_size.value() + 1, 28))
|
|
2589
|
+
self.axis_tick_size.setValue(min(self.axis_tick_size.value() + 1, 24))
|
|
2590
|
+
self.title_size.setValue(min(self.title_size.value() + 1, 32))
|
|
2591
|
+
self.colorbar_label_size.setValue(min(self.colorbar_label_size.value() + 1, 24))
|
|
2592
|
+
self.colorbar_tick_size.setValue(min(self.colorbar_tick_size.value() + 1, 20))
|
|
2593
|
+
|
|
2594
|
+
def _scale_fonts_down(self):
|
|
2595
|
+
"""Decrease all font sizes by 1."""
|
|
2596
|
+
self.axis_label_size.setValue(max(self.axis_label_size.value() - 1, 6))
|
|
2597
|
+
self.axis_tick_size.setValue(max(self.axis_tick_size.value() - 1, 6))
|
|
2598
|
+
self.title_size.setValue(max(self.title_size.value() - 1, 8))
|
|
2599
|
+
self.colorbar_label_size.setValue(max(self.colorbar_label_size.value() - 1, 6))
|
|
2600
|
+
self.colorbar_tick_size.setValue(max(self.colorbar_tick_size.value() - 1, 6))
|
|
2601
|
+
|
|
2602
|
+
def reset_to_defaults(self):
|
|
2603
|
+
"""Reset all settings to defaults."""
|
|
2604
|
+
self.xlabel_edit.clear()
|
|
2605
|
+
self.ylabel_edit.clear()
|
|
2606
|
+
self.title_edit.clear()
|
|
2607
|
+
self.colorbar_label_edit.clear()
|
|
2608
|
+
self.axis_label_size.setValue(12)
|
|
2609
|
+
self.axis_tick_size.setValue(10)
|
|
2610
|
+
self.title_size.setValue(12)
|
|
2611
|
+
self.colorbar_label_size.setValue(10)
|
|
2612
|
+
self.colorbar_tick_size.setValue(10)
|
|
2613
|
+
self.plot_bg_color = "auto"
|
|
2614
|
+
self.figure_bg_color = "auto"
|
|
2615
|
+
self.text_color = "auto"
|
|
2616
|
+
self.tick_color = "auto"
|
|
2617
|
+
self.border_color = "auto"
|
|
2618
|
+
self._update_color_preview(self.plot_bg_preview, "auto")
|
|
2619
|
+
self._update_color_preview(self.figure_bg_preview, "auto")
|
|
2620
|
+
self._update_color_preview(self.text_color_preview, "auto")
|
|
2621
|
+
self._update_color_preview(self.tick_color_preview, "auto")
|
|
2622
|
+
self._update_color_preview(self.border_color_preview, "auto")
|
|
2623
|
+
self.tick_direction.setCurrentText("out")
|
|
2624
|
+
self.tick_length.setValue(4)
|
|
2625
|
+
self.tick_width.setValue(1.0)
|
|
2626
|
+
self.border_width.setValue(1.0)
|
|
2627
|
+
# Padding defaults
|
|
2628
|
+
self.pad_left.setValue(0.135)
|
|
2629
|
+
self.pad_right.setValue(1.0)
|
|
2630
|
+
self.pad_top.setValue(0.95)
|
|
2631
|
+
self.pad_bottom.setValue(0.05)
|
|
2632
|
+
self.pad_wspace.setValue(0.2)
|
|
2633
|
+
self.pad_hspace.setValue(0.2)
|
|
2634
|
+
self.use_tight_layout.setChecked(False)
|
|
2635
|
+
|
|
2636
|
+
def get_settings(self):
|
|
2637
|
+
"""Return the current settings as a dictionary."""
|
|
2638
|
+
return {
|
|
2639
|
+
"xlabel": self.xlabel_edit.text(),
|
|
2640
|
+
"ylabel": self.ylabel_edit.text(),
|
|
2641
|
+
"title": self.title_edit.text(),
|
|
2642
|
+
"colorbar_label": self.colorbar_label_edit.text(),
|
|
2643
|
+
"axis_label_fontsize": self.axis_label_size.value(),
|
|
2644
|
+
"axis_tick_fontsize": self.axis_tick_size.value(),
|
|
2645
|
+
"title_fontsize": self.title_size.value(),
|
|
2646
|
+
"colorbar_label_fontsize": self.colorbar_label_size.value(),
|
|
2647
|
+
"colorbar_tick_fontsize": self.colorbar_tick_size.value(),
|
|
2648
|
+
"plot_bg_color": self.plot_bg_color,
|
|
2649
|
+
"figure_bg_color": self.figure_bg_color,
|
|
2650
|
+
"text_color": self.text_color,
|
|
2651
|
+
"tick_color": self.tick_color,
|
|
2652
|
+
"border_color": self.border_color,
|
|
2653
|
+
"border_width": self.border_width.value(),
|
|
2654
|
+
"tick_direction": self.tick_direction.currentText(),
|
|
2655
|
+
"tick_length": self.tick_length.value(),
|
|
2656
|
+
"tick_width": self.tick_width.value(),
|
|
2657
|
+
# Padding settings
|
|
2658
|
+
"pad_left": self.pad_left.value(),
|
|
2659
|
+
"pad_right": self.pad_right.value(),
|
|
2660
|
+
"pad_top": self.pad_top.value(),
|
|
2661
|
+
"pad_bottom": self.pad_bottom.value(),
|
|
2662
|
+
"pad_wspace": self.pad_wspace.value(),
|
|
2663
|
+
"pad_hspace": self.pad_hspace.value(),
|
|
2664
|
+
"use_tight_layout": self.use_tight_layout.isChecked(),
|
|
2665
|
+
}
|