RCAIDE-GUI 1.0.0__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.
- common_widgets/__init__.py +6 -0
- common_widgets/animated_toggle.py +141 -0
- common_widgets/color.py +17 -0
- common_widgets/data_entry_widget.py +337 -0
- common_widgets/image_widget.py +21 -0
- common_widgets/unit_picker_widget.py +48 -0
- main.py +168 -0
- rcaide_gui-1.0.0.dist-info/METADATA +96 -0
- rcaide_gui-1.0.0.dist-info/RECORD +170 -0
- rcaide_gui-1.0.0.dist-info/WHEEL +5 -0
- rcaide_gui-1.0.0.dist-info/entry_points.txt +2 -0
- rcaide_gui-1.0.0.dist-info/top_level.txt +5 -0
- rcaide_io.py +818 -0
- tabs/__init__.py +10 -0
- tabs/aircraft_configs/__init__.py +3 -0
- tabs/aircraft_configs/aircraft_configs.py +301 -0
- tabs/analysis/__init__.py +4 -0
- tabs/analysis/analysis.py +92 -0
- tabs/analysis/widgets/__init__.py +13 -0
- tabs/analysis/widgets/aeroacoustics_widget.py +158 -0
- tabs/analysis/widgets/aerodynamics_widget.py +108 -0
- tabs/analysis/widgets/analysis_data_widget.py +29 -0
- tabs/analysis/widgets/atmosphere_widget.py +49 -0
- tabs/analysis/widgets/costs_widget.py +35 -0
- tabs/analysis/widgets/energy_widget.py +32 -0
- tabs/analysis/widgets/geometry_widget.py +55 -0
- tabs/analysis/widgets/planets_widget.py +34 -0
- tabs/analysis/widgets/propulsion_widget.py +53 -0
- tabs/analysis/widgets/stability_widget.py +34 -0
- tabs/analysis/widgets/weights_widget.py +94 -0
- tabs/geometry/__init__.py +6 -0
- tabs/geometry/aircraft_configs.py +232 -0
- tabs/geometry/frames/__init__.py +15 -0
- tabs/geometry/frames/booms/__init__.py +5 -0
- tabs/geometry/frames/booms/boom_frame.py +238 -0
- tabs/geometry/frames/cargo_bays/__init__.py +5 -0
- tabs/geometry/frames/cargo_bays/cargo_bay_frame.py +159 -0
- tabs/geometry/frames/default_frame.py +23 -0
- tabs/geometry/frames/energy_network/__init__.py +2 -0
- tabs/geometry/frames/energy_network/turbofan_network/__init__.py +3 -0
- tabs/geometry/frames/energy_network/turbofan_network/frames/__init__.py +1 -0
- tabs/geometry/frames/energy_network/turbofan_network/widgets/__init__.py +1 -0
- tabs/geometry/frames/fuselages/__init__.py +5 -0
- tabs/geometry/frames/fuselages/fuselage_frame.py +294 -0
- tabs/geometry/frames/geometry_frame.py +91 -0
- tabs/geometry/frames/landing_gears/__init__.py +5 -0
- tabs/geometry/frames/landing_gears/landing_gear_frame.py +189 -0
- tabs/geometry/frames/nacelles/__init__.py +1 -0
- tabs/geometry/frames/powertrain/__init__.py +6 -0
- tabs/geometry/frames/powertrain/converters/__init__.py +5 -0
- tabs/geometry/frames/powertrain/converters/converter_frame.py +137 -0
- tabs/geometry/frames/powertrain/distributors/__init__.py +5 -0
- tabs/geometry/frames/powertrain/distributors/distributor_frame.py +143 -0
- tabs/geometry/frames/powertrain/nacelles/__init__.py +5 -0
- tabs/geometry/frames/powertrain/nacelles/nacelle_frame.py +283 -0
- tabs/geometry/frames/powertrain/powertrain_frame.py +259 -0
- tabs/geometry/frames/powertrain/propulsors/__init__.py +5 -0
- tabs/geometry/frames/powertrain/propulsors/propulsor_frame.py +146 -0
- tabs/geometry/frames/powertrain/sources/__init__.py +6 -0
- tabs/geometry/frames/powertrain/sources/energy_source_frame.py +147 -0
- tabs/geometry/frames/vehicle_frame.py +171 -0
- tabs/geometry/frames/wings/__init__.py +5 -0
- tabs/geometry/frames/wings/wings_frame.py +457 -0
- tabs/geometry/geometry.py +513 -0
- tabs/geometry/widgets/__init__.py +12 -0
- tabs/geometry/widgets/booms/__init__.py +6 -0
- tabs/geometry/widgets/booms/boom_section_widget.py +109 -0
- tabs/geometry/widgets/cargo_bays/__init__.py +1 -0
- tabs/geometry/widgets/fuselages/__init__.py +8 -0
- tabs/geometry/widgets/fuselages/cabin_class_widget.py +188 -0
- tabs/geometry/widgets/fuselages/cabin_widget.py +217 -0
- tabs/geometry/widgets/fuselages/fuselage_section_widget.py +190 -0
- tabs/geometry/widgets/geometry_data_widget.py +25 -0
- tabs/geometry/widgets/landing_gears/__init__.py +6 -0
- tabs/geometry/widgets/nacelles/__init__.py +1 -0
- tabs/geometry/widgets/powertrain/__init__.py +9 -0
- tabs/geometry/widgets/powertrain/converters/__init__.py +5 -0
- tabs/geometry/widgets/powertrain/converters/turboelectric_generator_widget.py +74 -0
- tabs/geometry/widgets/powertrain/distributors/__init__.py +5 -0
- tabs/geometry/widgets/powertrain/distributors/fuel_line_widget.py +81 -0
- tabs/geometry/widgets/powertrain/modulators/__init__.py +5 -0
- tabs/geometry/widgets/powertrain/modulators/esc_widget.py +58 -0
- tabs/geometry/widgets/powertrain/nacelles/__init__.py +5 -0
- tabs/geometry/widgets/powertrain/nacelles/nacelle_section_widget.py +115 -0
- tabs/geometry/widgets/powertrain/powertrain_connector_widget.py +136 -0
- tabs/geometry/widgets/powertrain/powertrain_widget.py +206 -0
- tabs/geometry/widgets/powertrain/propulsors/__init__.py +5 -0
- tabs/geometry/widgets/powertrain/propulsors/turbofan_widget.py +462 -0
- tabs/geometry/widgets/powertrain/sources/__init__.py +6 -0
- tabs/geometry/widgets/powertrain/sources/fuel_tank_widget.py +103 -0
- tabs/geometry/widgets/powertrain/sources/source_selector_widget.py +35 -0
- tabs/geometry/widgets/wings/__init__.py +7 -0
- tabs/geometry/widgets/wings/wing_cs_widget.py +198 -0
- tabs/geometry/widgets/wings/wing_section_widget.py +298 -0
- tabs/home/__init__.py +3 -0
- tabs/home/home.py +585 -0
- tabs/mission/__init__.py +4 -0
- tabs/mission/mission.py +1124 -0
- tabs/mission/widgets/__init__.py +3 -0
- tabs/mission/widgets/flight_controls_widget.py +186 -0
- tabs/mission/widgets/mission_analysis_widget.py +166 -0
- tabs/mission/widgets/mission_segment_helper.py +358 -0
- tabs/mission/widgets/mission_segment_widget.py +358 -0
- tabs/solve/__init__.py +4 -0
- tabs/solve/plots/__init__.py +12 -0
- tabs/solve/plots/aeroacoustics/__init__.py +6 -0
- tabs/solve/plots/aeroacoustics/plot_2D_noise_contour.py +184 -0
- tabs/solve/plots/aeroacoustics/plot_3D_noise_contour.py +219 -0
- tabs/solve/plots/aeroacoustics/plot_noise_certification_contour.py +115 -0
- tabs/solve/plots/aeroacoustics/plot_noise_level.py +118 -0
- tabs/solve/plots/aerodynamics/__init__.py +10 -0
- tabs/solve/plots/aerodynamics/plot_aerodynamic_coefficients.py +156 -0
- tabs/solve/plots/aerodynamics/plot_aerodynamic_forces.py +155 -0
- tabs/solve/plots/aerodynamics/plot_aircraft_aerodynamics.py +136 -0
- tabs/solve/plots/aerodynamics/plot_drag_components.py +157 -0
- tabs/solve/plots/aerodynamics/plot_lift_distribution.py +107 -0
- tabs/solve/plots/aerodynamics/plot_rotor_conditions.py +255 -0
- tabs/solve/plots/aerodynamics/plot_rotor_disc_performance.py +144 -0
- tabs/solve/plots/aerodynamics/plot_rotor_performance.py +124 -0
- tabs/solve/plots/common/__init__.py +4 -0
- tabs/solve/plots/common/plot_style.py +71 -0
- tabs/solve/plots/common/set_axes.py +55 -0
- tabs/solve/plots/create_plot_widgets.py +34 -0
- tabs/solve/plots/emissions/__init__.py +3 -0
- tabs/solve/plots/emissions/plot_emissions.py +167 -0
- tabs/solve/plots/energy/__init__.py +12 -0
- tabs/solve/plots/energy/plot_altitude_sfc_weight.py +134 -0
- tabs/solve/plots/energy/plot_battery_cell_conditions.py +154 -0
- tabs/solve/plots/energy/plot_battery_degradation.py +157 -0
- tabs/solve/plots/energy/plot_battery_module_C_rates.py +139 -0
- tabs/solve/plots/energy/plot_battery_module_conditions.py +157 -0
- tabs/solve/plots/energy/plot_battery_pack_conditions.py +164 -0
- tabs/solve/plots/energy/plot_battery_ragone_diagram.py +97 -0
- tabs/solve/plots/energy/plot_battery_temperature.py +136 -0
- tabs/solve/plots/energy/plot_electric_propulsor_efficiencies.py +136 -0
- tabs/solve/plots/energy/plot_propulsor_throttles.py +119 -0
- tabs/solve/plots/mission/__init__.py +5 -0
- tabs/solve/plots/mission/plot_aircraft_velocities.py +145 -0
- tabs/solve/plots/mission/plot_flight_conditions.py +236 -0
- tabs/solve/plots/mission/plot_flight_trajectory.py +172 -0
- tabs/solve/plots/stability/__init__.py +5 -0
- tabs/solve/plots/stability/plot_flight_forces_and_moments.py +163 -0
- tabs/solve/plots/stability/plot_lateral_stability.py +151 -0
- tabs/solve/plots/stability/plot_longitudinal_stability.py +173 -0
- tabs/solve/plots/thermal_management/__init__.py +7 -0
- tabs/solve/plots/thermal_management/plot_air_cooled_conditions.py +152 -0
- tabs/solve/plots/thermal_management/plot_cross_flow_heat_exchanger_conditions.py +185 -0
- tabs/solve/plots/thermal_management/plot_reservoir_conditions.py +141 -0
- tabs/solve/plots/thermal_management/plot_thermal_management_performance.py +105 -0
- tabs/solve/plots/thermal_management/plot_wavy_channel_conditions.py +161 -0
- tabs/solve/plots/weights/__init__.py +4 -0
- tabs/solve/plots/weights/plot_load_diagram.py +163 -0
- tabs/solve/plots/weights/plot_weight_breakdown.py +164 -0
- tabs/solve/solve.py +1498 -0
- tabs/style_sheet.py +114 -0
- tabs/tab_widget.py +14 -0
- tabs/visualize_geometry/__init__.py +4 -0
- tabs/visualize_geometry/core_3d_viewer.py +253 -0
- tabs/visualize_geometry/features/__init__.py +10 -0
- tabs/visualize_geometry/features/axes_gizmo.py +173 -0
- tabs/visualize_geometry/features/background.py +161 -0
- tabs/visualize_geometry/features/blueprint.py +685 -0
- tabs/visualize_geometry/features/camera.py +364 -0
- tabs/visualize_geometry/features/drag_aircraft.py +219 -0
- tabs/visualize_geometry/features/grid.py +194 -0
- tabs/visualize_geometry/features/measurement.py +367 -0
- tabs/visualize_geometry/features/screenshot.py +91 -0
- tabs/visualize_geometry/geometry_helper_functions.py +3 -0
- tabs/visualize_geometry/visualize_geometry.py +731 -0
- utilities.py +257 -0
tabs/mission/mission.py
ADDED
|
@@ -0,0 +1,1124 @@
|
|
|
1
|
+
# RCAIDE_GUI/tabs/mission/mission.py
|
|
2
|
+
|
|
3
|
+
# RCAIDE imports
|
|
4
|
+
import RCAIDE
|
|
5
|
+
|
|
6
|
+
# RCAIDE-GUI imports
|
|
7
|
+
from tabs.mission.widgets import MissionSegmentWidget
|
|
8
|
+
from tabs.mission.widgets import MissionAnalysisWidget
|
|
9
|
+
from tabs import TabWidget
|
|
10
|
+
from tabs.aircraft_configs.aircraft_configs import AircraftConfigsWidget
|
|
11
|
+
import rcaide_io
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# ------------------------------------------------------------------------------
|
|
15
|
+
# RCAIDE-format → GUI-format conversion helpers
|
|
16
|
+
# ------------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
def _get_nested_raw(d, path):
|
|
19
|
+
"""Navigate a dotted attribute path in a nested dict.
|
|
20
|
+
|
|
21
|
+
Returns the raw value (which may still be a [value, units_index] pair),
|
|
22
|
+
or [False, 0] when the path cannot be resolved.
|
|
23
|
+
"""
|
|
24
|
+
for part in path.split('.'):
|
|
25
|
+
if isinstance(d, dict):
|
|
26
|
+
d = d.get(part, None)
|
|
27
|
+
else:
|
|
28
|
+
return [False, 0]
|
|
29
|
+
if d is None:
|
|
30
|
+
return [False, 0]
|
|
31
|
+
return d if isinstance(d, (list, tuple)) else [d, 0]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _rcaide_seg_to_gui(seg_dict):
|
|
35
|
+
"""Convert one RCAIDE native segment dict (with unit-arg pairs) to GUI form data.
|
|
36
|
+
|
|
37
|
+
The RCAIDE format uses [value, units_index] for every leaf; the GUI's
|
|
38
|
+
DataEntryWidget.load_data expects the same pairs keyed by display label.
|
|
39
|
+
"""
|
|
40
|
+
from tabs.mission.widgets.mission_segment_helper import (
|
|
41
|
+
segment_rcaide_classes, segment_data_fields,
|
|
42
|
+
)
|
|
43
|
+
from tabs.mission.widgets.flight_controls_widget import FlightControlsWidget
|
|
44
|
+
|
|
45
|
+
# Reverse-map __type__ string → (top_dropdown_index, sub_name)
|
|
46
|
+
type_str = seg_dict.get("__type__", "")
|
|
47
|
+
top_idx, sub_name = 0, ""
|
|
48
|
+
for ti, sub_dict in enumerate(segment_rcaide_classes):
|
|
49
|
+
for sn, cls in sub_dict.items():
|
|
50
|
+
if f"{cls.__module__}.{cls.__qualname__}" == type_str:
|
|
51
|
+
top_idx, sub_name = ti, sn
|
|
52
|
+
break
|
|
53
|
+
if sub_name:
|
|
54
|
+
break
|
|
55
|
+
|
|
56
|
+
# control_points is stored as [16, 0] — extract the scalar
|
|
57
|
+
cp_raw = seg_dict.get("control_points", [16, 0])
|
|
58
|
+
cp = int(cp_raw[0]) if isinstance(cp_raw, (list, tuple)) else int(cp_raw)
|
|
59
|
+
|
|
60
|
+
# Degrees-of-freedom flags
|
|
61
|
+
dof_labels = [
|
|
62
|
+
("Forces in X axis", "flight_dynamics.force_x"),
|
|
63
|
+
("Moments about X axis", "flight_dynamics.moment_x"),
|
|
64
|
+
("Forces in Y axis", "flight_dynamics.force_y"),
|
|
65
|
+
("Moments about Y axis", "flight_dynamics.moment_y"),
|
|
66
|
+
("Forces in Z axis", "flight_dynamics.force_z"),
|
|
67
|
+
("Moments about Z axis", "flight_dynamics.moment_z"),
|
|
68
|
+
]
|
|
69
|
+
flight_forces = {
|
|
70
|
+
label: _get_nested_raw(seg_dict, path)
|
|
71
|
+
for label, path in dof_labels
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
# Flight-control active flags
|
|
75
|
+
flight_controls = {}
|
|
76
|
+
for group_fields in FlightControlsWidget.fields.values():
|
|
77
|
+
for label, _, rcaide_path in group_fields:
|
|
78
|
+
flight_controls[label] = _get_nested_raw(seg_dict, rcaide_path)
|
|
79
|
+
|
|
80
|
+
# Assigned propulsors for throttle — stored as [[[name1, name2]], 0] after unit-arg wrapping
|
|
81
|
+
try:
|
|
82
|
+
acv_raw = seg_dict.get("assigned_control_variables", {})
|
|
83
|
+
throttle_raw = acv_raw.get("throttle", {}) if isinstance(acv_raw, dict) else {}
|
|
84
|
+
ap_raw = throttle_raw.get("assigned_propulsors", []) if isinstance(throttle_raw, dict) else []
|
|
85
|
+
if isinstance(ap_raw, list) and len(ap_raw) == 2 and isinstance(ap_raw[1], int) and not isinstance(ap_raw[1], bool):
|
|
86
|
+
ap_raw = ap_raw[0] # strip unit-arg wrapper → [[name1, name2]]
|
|
87
|
+
if ap_raw and isinstance(ap_raw[0], list):
|
|
88
|
+
assigned_propulsors = ap_raw[0]
|
|
89
|
+
elif ap_raw and isinstance(ap_raw[0], str):
|
|
90
|
+
assigned_propulsors = list(ap_raw)
|
|
91
|
+
else:
|
|
92
|
+
assigned_propulsors = []
|
|
93
|
+
except Exception:
|
|
94
|
+
assigned_propulsors = []
|
|
95
|
+
flight_controls["assigned_propulsors"] = assigned_propulsors
|
|
96
|
+
|
|
97
|
+
gui_data = {
|
|
98
|
+
"Segment Name": seg_dict.get("tag", ""),
|
|
99
|
+
"top dropdown": top_idx,
|
|
100
|
+
"nested dropdown": sub_name,
|
|
101
|
+
"config": seg_dict.get("config_tag", ""),
|
|
102
|
+
"Control Points": cp,
|
|
103
|
+
"Solver": "root",
|
|
104
|
+
"flight forces": flight_forces,
|
|
105
|
+
"flight controls": flight_controls,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
# Subsegment-specific parameter values (altitude, speed, etc.)
|
|
109
|
+
if sub_name and top_idx < len(segment_data_fields):
|
|
110
|
+
for label, _, rcaide_label in segment_data_fields[top_idx].get(sub_name, []):
|
|
111
|
+
gui_data[label] = seg_dict.get(rcaide_label, [0, 0])
|
|
112
|
+
|
|
113
|
+
return gui_data
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _extract_gui_segments(mission_data):
|
|
117
|
+
"""Return a flat list of GUI-format segment dicts from mission_data.
|
|
118
|
+
|
|
119
|
+
Handles both formats:
|
|
120
|
+
- Old GUI format: list where each item has a "Segment Name" key
|
|
121
|
+
- New RCAIDE format: list of mission objects each containing a "segments" list
|
|
122
|
+
"""
|
|
123
|
+
if not mission_data:
|
|
124
|
+
return []
|
|
125
|
+
|
|
126
|
+
first = mission_data[0] if mission_data else {}
|
|
127
|
+
|
|
128
|
+
# Old GUI format — items are already GUI dicts
|
|
129
|
+
if isinstance(first, dict) and "Segment Name" in first:
|
|
130
|
+
return list(mission_data)
|
|
131
|
+
|
|
132
|
+
# New RCAIDE format — extract and convert each segment from every mission
|
|
133
|
+
result = []
|
|
134
|
+
for mission_entry in mission_data:
|
|
135
|
+
if not isinstance(mission_entry, dict):
|
|
136
|
+
continue
|
|
137
|
+
for seg_dict in mission_entry.get("segments", []):
|
|
138
|
+
if isinstance(seg_dict, dict):
|
|
139
|
+
result.append(_rcaide_seg_to_gui(seg_dict))
|
|
140
|
+
return result
|
|
141
|
+
|
|
142
|
+
# PyQt imports
|
|
143
|
+
from PyQt6.QtCore import Qt, QTimer, QPointF
|
|
144
|
+
from PyQt6.QtGui import QColor, QPainter, QPen, QBrush, QFont, QPainterPath
|
|
145
|
+
from PyQt6.QtWidgets import (
|
|
146
|
+
QWidget, QVBoxLayout, QHBoxLayout, QSplitter, QGroupBox, QLabel, QLineEdit,
|
|
147
|
+
QPushButton, QTreeWidget, QTreeWidgetItem, QScrollArea, QFrame, QTableWidget,
|
|
148
|
+
QTableWidgetItem, QHeaderView, QSizePolicy, QAbstractItemView
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# ------------------------------------------------------------------------------
|
|
152
|
+
# Mission Profile Widget
|
|
153
|
+
# ------------------------------------------------------------------------------
|
|
154
|
+
class MissionProfileWidget(QWidget):
|
|
155
|
+
def __init__(self, parent=None):
|
|
156
|
+
super().__init__(parent)
|
|
157
|
+
# Constrain the profile card so it remains readable in compact layouts.
|
|
158
|
+
self.setMinimumHeight(220)
|
|
159
|
+
self.setMaximumHeight(320)
|
|
160
|
+
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
|
161
|
+
# Each phase entry is stored as (display_name, normalized_phase_type).
|
|
162
|
+
self.phases = []
|
|
163
|
+
# 0..1 progress used to animate the moving marker along the profile.
|
|
164
|
+
self._progress = 0.0
|
|
165
|
+
self._timer = QTimer(self)
|
|
166
|
+
self._timer.timeout.connect(self._advance_animation)
|
|
167
|
+
# 25 FPS-ish update cadence for lightweight motion.
|
|
168
|
+
self._timer.start(40)
|
|
169
|
+
self.setAutoFillBackground(False)
|
|
170
|
+
|
|
171
|
+
def set_phases(self, names):
|
|
172
|
+
normalized = []
|
|
173
|
+
for item in (names or []):
|
|
174
|
+
if isinstance(item, tuple) and len(item) == 2:
|
|
175
|
+
label, ptype = item
|
|
176
|
+
label_text = str(label).title()
|
|
177
|
+
# Re-classify using both type + label to avoid name-only misclassification.
|
|
178
|
+
normalized.append((label_text, self._phase_type(f"{ptype} {label_text}")))
|
|
179
|
+
else:
|
|
180
|
+
text = str(item).title()
|
|
181
|
+
normalized.append((text, self._phase_type(text)))
|
|
182
|
+
self.phases = normalized
|
|
183
|
+
# Restart marker animation whenever phases are regenerated.
|
|
184
|
+
self._progress = 0.0
|
|
185
|
+
self.update()
|
|
186
|
+
|
|
187
|
+
def _advance_animation(self):
|
|
188
|
+
if not self.phases or len(self.phases) < 2:
|
|
189
|
+
return
|
|
190
|
+
self._progress += 0.005
|
|
191
|
+
if self._progress > 1.0:
|
|
192
|
+
self._progress = 0.0
|
|
193
|
+
self.update()
|
|
194
|
+
|
|
195
|
+
@staticmethod
|
|
196
|
+
def _phase_type(name):
|
|
197
|
+
n = str(name).lower().replace("_", " ")
|
|
198
|
+
# Prefer explicit phase prefix from overview tuples (stable and deterministic).
|
|
199
|
+
if n.startswith("vertical climb"):
|
|
200
|
+
return "vertical_climb"
|
|
201
|
+
if n.startswith("vertical descent"):
|
|
202
|
+
return "vertical_descent"
|
|
203
|
+
if n.startswith("vertical hover"):
|
|
204
|
+
return "vertical_hover"
|
|
205
|
+
if n.startswith("single point"):
|
|
206
|
+
return "single_point"
|
|
207
|
+
if n.startswith("ground"):
|
|
208
|
+
return "ground"
|
|
209
|
+
if n.startswith("descent"):
|
|
210
|
+
return "descent"
|
|
211
|
+
if n.startswith("climb"):
|
|
212
|
+
return "climb"
|
|
213
|
+
if n.startswith("cruise"):
|
|
214
|
+
return "cruise"
|
|
215
|
+
if n.startswith("takeoff") or n.startswith("taxi"):
|
|
216
|
+
return "takeoff"
|
|
217
|
+
if "vertical" in n and "climb" in n:
|
|
218
|
+
return "vertical_climb"
|
|
219
|
+
if "vertical" in n and ("descent" in n or "descend" in n):
|
|
220
|
+
return "vertical_descent"
|
|
221
|
+
if "vertical" in n and "hover" in n:
|
|
222
|
+
return "vertical_hover"
|
|
223
|
+
if "single point" in n:
|
|
224
|
+
return "single_point"
|
|
225
|
+
if "ground" in n:
|
|
226
|
+
return "ground"
|
|
227
|
+
if "descent" in n or n.startswith("des"):
|
|
228
|
+
return "descent"
|
|
229
|
+
if "climb" in n:
|
|
230
|
+
return "climb"
|
|
231
|
+
if "cruise" in n:
|
|
232
|
+
return "cruise"
|
|
233
|
+
if "landing" in n or "land" in n or "approach" in n:
|
|
234
|
+
return "landing"
|
|
235
|
+
return "other"
|
|
236
|
+
|
|
237
|
+
def _phase_color(self, ptype):
|
|
238
|
+
colors = {
|
|
239
|
+
"takeoff": "#4caf50",
|
|
240
|
+
"climb": "#81c784",
|
|
241
|
+
"cruise": "#64b5f6",
|
|
242
|
+
"descent": "#ffb74d",
|
|
243
|
+
"landing": "#e57373",
|
|
244
|
+
"ground": "#8d6e63",
|
|
245
|
+
"single_point": "#ab47bc",
|
|
246
|
+
"vertical_climb": "#26a69a",
|
|
247
|
+
"vertical_descent": "#26a69a",
|
|
248
|
+
"vertical_hover": "#26a69a",
|
|
249
|
+
}
|
|
250
|
+
return QColor(colors.get(ptype, "#b0bec5"))
|
|
251
|
+
|
|
252
|
+
def paintEvent(self, event):
|
|
253
|
+
painter = QPainter(self)
|
|
254
|
+
painter.setRenderHint(QPainter.RenderHint.Antialiasing, True)
|
|
255
|
+
outer = self.rect().adjusted(16, 14, -16, -14)
|
|
256
|
+
# Keep vertical/horizontal proportions consistent for a less flattened profile.
|
|
257
|
+
plot_h = int(outer.height() * 0.72)
|
|
258
|
+
rect = outer.adjusted(0, 0, 0, -(outer.height() - plot_h))
|
|
259
|
+
painter.fillRect(self.rect(), QColor("#050a12"))
|
|
260
|
+
|
|
261
|
+
painter.setPen(QPen(QColor("#1f2933"), 1, Qt.PenStyle.DotLine))
|
|
262
|
+
for i in range(1, 4):
|
|
263
|
+
y = rect.top() + i * rect.height() / 4
|
|
264
|
+
painter.drawLine(int(rect.left()), int(y), int(rect.right()), int(y))
|
|
265
|
+
|
|
266
|
+
if not self.phases:
|
|
267
|
+
painter.setPen(QColor("#8c9aa8"))
|
|
268
|
+
painter.setFont(QFont("Segoe UI", 10))
|
|
269
|
+
painter.drawText(
|
|
270
|
+
self.rect(),
|
|
271
|
+
Qt.AlignmentFlag.AlignCenter,
|
|
272
|
+
"Mission profile will appear here once you add segments.",
|
|
273
|
+
)
|
|
274
|
+
return
|
|
275
|
+
|
|
276
|
+
# Build a normalized altitude trace (0..1-ish in plot space) from phase types.
|
|
277
|
+
n = len(self.phases)
|
|
278
|
+
alt = []
|
|
279
|
+
cur = 0.15
|
|
280
|
+
for _, ptype in self.phases:
|
|
281
|
+
if ptype in ("takeoff", "climb"):
|
|
282
|
+
cur += 0.18
|
|
283
|
+
elif ptype == "vertical_climb":
|
|
284
|
+
cur += 0.24
|
|
285
|
+
elif ptype == "cruise":
|
|
286
|
+
cur += 0.0
|
|
287
|
+
elif ptype in ("descent", "landing"):
|
|
288
|
+
cur -= 0.18
|
|
289
|
+
elif ptype == "vertical_descent":
|
|
290
|
+
cur -= 0.24
|
|
291
|
+
elif ptype == "ground":
|
|
292
|
+
cur = 0.10
|
|
293
|
+
elif ptype in ("single_point", "vertical_hover"):
|
|
294
|
+
cur += 0.0
|
|
295
|
+
else:
|
|
296
|
+
cur += 0.05
|
|
297
|
+
cur = max(0.08, min(0.85, cur))
|
|
298
|
+
alt.append(cur)
|
|
299
|
+
# Seed the first point so the first segment starts from a realistic state.
|
|
300
|
+
first_ptype = self.phases[0][1] if self.phases else "other"
|
|
301
|
+
if first_ptype in ("takeoff", "climb"):
|
|
302
|
+
start_alt = 0.08
|
|
303
|
+
elif first_ptype == "vertical_climb":
|
|
304
|
+
# Ensure the first vertical climb draws as a visible segment, not a point.
|
|
305
|
+
start_alt = 0.10
|
|
306
|
+
elif first_ptype in ("descent", "landing", "vertical_descent"):
|
|
307
|
+
start_alt = 0.78
|
|
308
|
+
elif first_ptype == "ground":
|
|
309
|
+
start_alt = 0.10
|
|
310
|
+
elif first_ptype in ("single_point", "vertical_hover"):
|
|
311
|
+
start_alt = alt[0] if alt else 0.45
|
|
312
|
+
elif alt:
|
|
313
|
+
start_alt = alt[0]
|
|
314
|
+
else:
|
|
315
|
+
start_alt = 0.08
|
|
316
|
+
alt.insert(0, start_alt)
|
|
317
|
+
|
|
318
|
+
# Uniform x-spacing across segments.
|
|
319
|
+
dx = rect.width() / max(n, 1)
|
|
320
|
+
xs = [rect.left() + i * dx for i in range(n + 1)]
|
|
321
|
+
|
|
322
|
+
# Fixed runway-level y used for all ground segments.
|
|
323
|
+
ground_y = rect.bottom() - 0.10 * rect.height()
|
|
324
|
+
painter.setFont(QFont("Segoe UI", 8))
|
|
325
|
+
for i, (name, ptype) in enumerate(self.phases):
|
|
326
|
+
color = self._phase_color(ptype)
|
|
327
|
+
x1, x2 = xs[i], xs[i + 1]
|
|
328
|
+
y1 = rect.bottom() - alt[i] * rect.height()
|
|
329
|
+
y2 = rect.bottom() - alt[i + 1] * rect.height()
|
|
330
|
+
painter.setPen(QPen(color, 2.0))
|
|
331
|
+
cx = 0.5 * (x1 + x2)
|
|
332
|
+
if ptype == "ground":
|
|
333
|
+
# Ground is always shown as a constant-altitude line near runway level.
|
|
334
|
+
y1 = ground_y
|
|
335
|
+
y2 = ground_y
|
|
336
|
+
painter.drawLine(QPointF(x1, y1), QPointF(x2, y2))
|
|
337
|
+
elif ptype in ("vertical_climb", "vertical_descent", "vertical_hover"):
|
|
338
|
+
# Vertical phases are represented by vertical motion in the same x-slot.
|
|
339
|
+
painter.drawLine(QPointF(cx, y1), QPointF(cx, y2))
|
|
340
|
+
elif ptype == "single_point":
|
|
341
|
+
# Single-point phases are event markers instead of trajectory segments.
|
|
342
|
+
py = y2
|
|
343
|
+
painter.setBrush(QBrush(color))
|
|
344
|
+
painter.drawEllipse(QPointF(cx, py), 4.5, 4.5)
|
|
345
|
+
painter.setBrush(Qt.BrushStyle.NoBrush)
|
|
346
|
+
else:
|
|
347
|
+
seg_path = QPainterPath()
|
|
348
|
+
seg_path.moveTo(x1, y1)
|
|
349
|
+
seg_path.lineTo(x2, y2)
|
|
350
|
+
painter.drawPath(seg_path)
|
|
351
|
+
|
|
352
|
+
label_y = max(min(y1, y2) - 16, rect.top() + 4)
|
|
353
|
+
painter.setPen(QColor("#e8f2ff"))
|
|
354
|
+
painter.drawText(
|
|
355
|
+
int(cx - 55),
|
|
356
|
+
int(label_y),
|
|
357
|
+
110,
|
|
358
|
+
18,
|
|
359
|
+
Qt.AlignmentFlag.AlignCenter,
|
|
360
|
+
name,
|
|
361
|
+
)
|
|
362
|
+
painter.setPen(QPen(QColor("#3f5670"), 1.2))
|
|
363
|
+
painter.drawLine(
|
|
364
|
+
int(rect.left()),
|
|
365
|
+
int(rect.bottom()),
|
|
366
|
+
int(rect.right()),
|
|
367
|
+
int(rect.bottom()),
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
# Animate a marker over the currently active segment.
|
|
371
|
+
total_segments = n
|
|
372
|
+
t = self._progress * total_segments
|
|
373
|
+
idx = int(t)
|
|
374
|
+
frac = t - idx
|
|
375
|
+
if idx >= total_segments:
|
|
376
|
+
idx = total_segments - 1
|
|
377
|
+
frac = 1.0
|
|
378
|
+
|
|
379
|
+
x1, x2 = xs[idx], xs[idx + 1]
|
|
380
|
+
seg_ptype = self.phases[idx][1]
|
|
381
|
+
y1 = rect.bottom() - alt[idx] * rect.height()
|
|
382
|
+
y2 = rect.bottom() - alt[idx + 1] * rect.height()
|
|
383
|
+
# Keep the animated dot aligned with each segment's custom drawing rule.
|
|
384
|
+
if seg_ptype == "ground":
|
|
385
|
+
mx = x1 + (x2 - x1) * frac
|
|
386
|
+
my = ground_y
|
|
387
|
+
elif seg_ptype in ("vertical_climb", "vertical_descent", "vertical_hover"):
|
|
388
|
+
mx = 0.5 * (x1 + x2)
|
|
389
|
+
my = y1 + (y2 - y1) * frac
|
|
390
|
+
elif seg_ptype == "single_point":
|
|
391
|
+
mx = 0.5 * (x1 + x2)
|
|
392
|
+
my = y2
|
|
393
|
+
else:
|
|
394
|
+
mx = x1 + (x2 - x1) * frac
|
|
395
|
+
my = y1 + (y2 - y1) * frac
|
|
396
|
+
painter.setBrush(QBrush(QColor("#ffffff")))
|
|
397
|
+
painter.setPen(Qt.PenStyle.NoPen)
|
|
398
|
+
painter.drawEllipse(QPointF(mx, my), 4, 4)
|
|
399
|
+
painter.setPen(QPen(QColor("#64b5f6"), 1.2))
|
|
400
|
+
painter.setBrush(Qt.BrushStyle.NoBrush)
|
|
401
|
+
painter.drawEllipse(QPointF(mx, my), 7, 7)
|
|
402
|
+
|
|
403
|
+
# ============================================================
|
|
404
|
+
# Mission Summary Table
|
|
405
|
+
# ============================================================
|
|
406
|
+
class MissionSummaryTable(QTableWidget):
|
|
407
|
+
def __init__(self, mission_widget):
|
|
408
|
+
super().__init__(0, 6)
|
|
409
|
+
# Parent mission widget reference for reading segment state and UI navigation.
|
|
410
|
+
self.mission_widget = mission_widget
|
|
411
|
+
|
|
412
|
+
# Table columns summarizing each segment's key mission/solver metadata.
|
|
413
|
+
self.horizontalHeader().setMinimumSectionSize(56)
|
|
414
|
+
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
|
415
|
+
self.setHorizontalHeaderLabels(
|
|
416
|
+
[
|
|
417
|
+
"Segment",
|
|
418
|
+
"Type",
|
|
419
|
+
"Config",
|
|
420
|
+
"Ctrl Pts",
|
|
421
|
+
"Unknowns",
|
|
422
|
+
"Residuals",
|
|
423
|
+
]
|
|
424
|
+
)
|
|
425
|
+
self.horizontalHeader().setDefaultAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
426
|
+
self.horizontalHeader().setMinimumSectionSize(72)
|
|
427
|
+
self.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
|
|
428
|
+
self.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
|
|
429
|
+
self.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch)
|
|
430
|
+
self.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeMode.ResizeToContents)
|
|
431
|
+
self.horizontalHeader().setSectionResizeMode(4, QHeaderView.ResizeMode.ResizeToContents)
|
|
432
|
+
self.horizontalHeader().setSectionResizeMode(5, QHeaderView.ResizeMode.ResizeToContents)
|
|
433
|
+
self.verticalHeader().setVisible(False)
|
|
434
|
+
self.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
|
|
435
|
+
self.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)
|
|
436
|
+
self.setWordWrap(False)
|
|
437
|
+
self.setTextElideMode(Qt.TextElideMode.ElideRight)
|
|
438
|
+
self.verticalHeader().setDefaultSectionSize(28)
|
|
439
|
+
self.setHorizontalScrollMode(QTableWidget.ScrollMode.ScrollPerPixel)
|
|
440
|
+
self.setVerticalScrollMode(QTableWidget.ScrollMode.ScrollPerPixel)
|
|
441
|
+
# Keep visual style consistent with the Mission tab theme.
|
|
442
|
+
self.setStyleSheet(
|
|
443
|
+
"""
|
|
444
|
+
QTableWidget {
|
|
445
|
+
background-color:#060b13;
|
|
446
|
+
color:#e5f0ff;
|
|
447
|
+
border:1px solid #2d3a4e;
|
|
448
|
+
font-size:13px;
|
|
449
|
+
gridline-color:#23334b;
|
|
450
|
+
}
|
|
451
|
+
QHeaderView::section {
|
|
452
|
+
background-color:#111b2d;
|
|
453
|
+
color:#a8c6ff;
|
|
454
|
+
font-weight:600;
|
|
455
|
+
padding:3px;
|
|
456
|
+
}
|
|
457
|
+
QTableWidget::item:selected { background-color:#1b2a44; }
|
|
458
|
+
"""
|
|
459
|
+
)
|
|
460
|
+
# Clicking a row focuses/open the corresponding segment detail panel.
|
|
461
|
+
self.cellClicked.connect(self._row_clicked)
|
|
462
|
+
|
|
463
|
+
@staticmethod
|
|
464
|
+
def _count_active(data):
|
|
465
|
+
# Count active boolean-style entries stored as `(enabled, value)` tuples.
|
|
466
|
+
if not isinstance(data, dict):
|
|
467
|
+
return 0
|
|
468
|
+
total = 0
|
|
469
|
+
for value in data.values():
|
|
470
|
+
if isinstance(value, tuple) and value and bool(value[0]):
|
|
471
|
+
total += 1
|
|
472
|
+
return total
|
|
473
|
+
|
|
474
|
+
@staticmethod
|
|
475
|
+
def _norm(name):
|
|
476
|
+
# Normalize config names for robust dictionary/list matching.
|
|
477
|
+
return str(name or "").strip().lower().replace(" ", "_")
|
|
478
|
+
|
|
479
|
+
def update_table(self):
|
|
480
|
+
# Rebuild the full table from current segment widgets.
|
|
481
|
+
segs = self.mission_widget.segment_widgets
|
|
482
|
+
if not segs and self.mission_widget.tree.topLevelItemCount() > 0:
|
|
483
|
+
# Tree exists but detail widgets are not yet available (loading/rebuild state).
|
|
484
|
+
self.clearSpans()
|
|
485
|
+
self.clearContents()
|
|
486
|
+
self.setRowCount(self.mission_widget.tree.topLevelItemCount())
|
|
487
|
+
for r in range(self.mission_widget.tree.topLevelItemCount()):
|
|
488
|
+
name = self.mission_widget.tree.topLevelItem(r).text(0)
|
|
489
|
+
values_row = [name, "-", "-", "-", "-", "-"]
|
|
490
|
+
for c, val in enumerate(values_row):
|
|
491
|
+
item = QTableWidgetItem(str(val))
|
|
492
|
+
item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
493
|
+
self.setItem(r, c, item)
|
|
494
|
+
row_h = self.rowHeight(0) if self.rowCount() else 26
|
|
495
|
+
visible_rows = max(4, min(self.rowCount(), 8))
|
|
496
|
+
self.setMinimumHeight(self.horizontalHeader().height() + (row_h * visible_rows) + 10)
|
|
497
|
+
return
|
|
498
|
+
|
|
499
|
+
if not segs:
|
|
500
|
+
# Explicit empty state before any segments are added.
|
|
501
|
+
self.clearContents()
|
|
502
|
+
self.setRowCount(1)
|
|
503
|
+
item = QTableWidgetItem("No segments added yet.")
|
|
504
|
+
item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
505
|
+
self.setItem(0, 0, item)
|
|
506
|
+
self.setSpan(0, 0, 1, 6)
|
|
507
|
+
row_h = max(self.rowHeight(0), 26)
|
|
508
|
+
self.setMinimumHeight(self.horizontalHeader().height() + (row_h * 4) + 10)
|
|
509
|
+
return
|
|
510
|
+
|
|
511
|
+
rows = []
|
|
512
|
+
for idx, seg in enumerate(segs):
|
|
513
|
+
# Resolve display name with fallback to tree text and generated default.
|
|
514
|
+
name = seg.segment_name_input.text().strip() if hasattr(seg, "segment_name_input") else ""
|
|
515
|
+
if not name and idx < self.mission_widget.tree.topLevelItemCount():
|
|
516
|
+
name = self.mission_widget.tree.topLevelItem(idx).text(0)
|
|
517
|
+
if not name:
|
|
518
|
+
name = f"Segment {idx + 1}"
|
|
519
|
+
sub_type = seg.nested_dropdown.currentText() if hasattr(seg, "nested_dropdown") else "Custom"
|
|
520
|
+
if sub_type == "Constant Speed/Constant Altitude":
|
|
521
|
+
sub_type = "Const Spd/Alt"
|
|
522
|
+
# Resolve config from selector text first, then index-based fallbacks.
|
|
523
|
+
config_name = ""
|
|
524
|
+
if hasattr(seg, "config_selector"):
|
|
525
|
+
config_name = seg.config_selector.currentText().strip()
|
|
526
|
+
if not config_name:
|
|
527
|
+
cfg_idx = seg.config_selector.currentIndex()
|
|
528
|
+
if cfg_idx >= 0:
|
|
529
|
+
config_name = seg.config_selector.itemText(cfg_idx).strip()
|
|
530
|
+
if not config_name:
|
|
531
|
+
cfgs = getattr(values, "rcaide_configs", None)
|
|
532
|
+
if isinstance(cfgs, dict) and cfg_idx >= 0:
|
|
533
|
+
keys = list(cfgs.keys())
|
|
534
|
+
if cfg_idx < len(keys):
|
|
535
|
+
config_name = str(keys[cfg_idx]).strip()
|
|
536
|
+
if not config_name and hasattr(seg, "_default_config_key"):
|
|
537
|
+
try:
|
|
538
|
+
# Final fallback: infer config from segment semantics.
|
|
539
|
+
cfg_key = seg._default_config_key(
|
|
540
|
+
seg.top_dropdown.currentText() if hasattr(seg, "top_dropdown") else "",
|
|
541
|
+
seg.nested_dropdown.currentText() if hasattr(seg, "nested_dropdown") else "",
|
|
542
|
+
name,
|
|
543
|
+
)
|
|
544
|
+
cfgs = getattr(values, "rcaide_configs", None)
|
|
545
|
+
if isinstance(cfgs, dict):
|
|
546
|
+
for key in cfgs.keys():
|
|
547
|
+
if self._norm(key) == self._norm(cfg_key):
|
|
548
|
+
config_name = str(key)
|
|
549
|
+
break
|
|
550
|
+
except Exception:
|
|
551
|
+
pass
|
|
552
|
+
if not config_name:
|
|
553
|
+
config_name = "Base"
|
|
554
|
+
cp = seg.ctrl_points.text().strip() if hasattr(seg, "ctrl_points") else "16"
|
|
555
|
+
# Unknowns/residuals are active-control and active-force counts.
|
|
556
|
+
controls_data = {}
|
|
557
|
+
if hasattr(seg, "flight_controls_widget") and seg.flight_controls_widget:
|
|
558
|
+
try:
|
|
559
|
+
controls_data = seg.flight_controls_widget.get_data()
|
|
560
|
+
except Exception:
|
|
561
|
+
controls_data = {}
|
|
562
|
+
forces_data = {}
|
|
563
|
+
if hasattr(seg, "dof_entry_widget") and seg.dof_entry_widget:
|
|
564
|
+
try:
|
|
565
|
+
forces_data = seg.dof_entry_widget.get_values()
|
|
566
|
+
except Exception:
|
|
567
|
+
forces_data = {}
|
|
568
|
+
unknowns = self._count_active(controls_data)
|
|
569
|
+
residuals = self._count_active(forces_data)
|
|
570
|
+
# Keep takeoff/ground rows grouped first for quick scan.
|
|
571
|
+
is_takeoff = "takeoff" in name.lower() or "ground" in str(sub_type).lower()
|
|
572
|
+
rows.append((is_takeoff, idx, name, sub_type, config_name, cp, unknowns, residuals))
|
|
573
|
+
|
|
574
|
+
rows.sort(key=lambda r: (not r[0], r[1]))
|
|
575
|
+
self.clearSpans()
|
|
576
|
+
self.clearContents()
|
|
577
|
+
self.setRowCount(len(rows))
|
|
578
|
+
|
|
579
|
+
for r, (_, seg_idx, name, sub_type, config_name, cp, unknowns, residuals) in enumerate(rows):
|
|
580
|
+
values_row = [name, sub_type, config_name, cp, unknowns, residuals]
|
|
581
|
+
for c, val in enumerate(values_row):
|
|
582
|
+
item = QTableWidgetItem(str(val))
|
|
583
|
+
item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
584
|
+
item.setToolTip(str(val))
|
|
585
|
+
if c == 0:
|
|
586
|
+
item.setData(Qt.ItemDataRole.UserRole, seg_idx)
|
|
587
|
+
self.setItem(r, c, item)
|
|
588
|
+
|
|
589
|
+
self.resizeRowsToContents()
|
|
590
|
+
row_h = self.rowHeight(0) if rows else 26
|
|
591
|
+
visible_rows = max(4, min(len(rows), 8))
|
|
592
|
+
self.setMinimumHeight(self.horizontalHeader().height() + (row_h * visible_rows) + 10)
|
|
593
|
+
|
|
594
|
+
def _row_clicked(self, row, col):
|
|
595
|
+
# Navigate from summary row to the matching collapsible details panel.
|
|
596
|
+
item = self.item(row, 0)
|
|
597
|
+
if item is None:
|
|
598
|
+
return
|
|
599
|
+
seg_index = item.data(Qt.ItemDataRole.UserRole)
|
|
600
|
+
if seg_index is None:
|
|
601
|
+
return
|
|
602
|
+
|
|
603
|
+
panel_index = int(seg_index) * 2
|
|
604
|
+
layout = self.mission_widget.details_layout
|
|
605
|
+
if panel_index < layout.count():
|
|
606
|
+
widget = layout.itemAt(panel_index).widget()
|
|
607
|
+
if hasattr(widget, "toggle") and not getattr(widget, "is_open", True):
|
|
608
|
+
widget.toggle()
|
|
609
|
+
self.mission_widget.details_scroll.ensureWidgetVisible(widget)
|
|
610
|
+
|
|
611
|
+
# ============================================================
|
|
612
|
+
# Collapsible Panel wrapper
|
|
613
|
+
# ============================================================
|
|
614
|
+
class CollapsiblePanel(QWidget):
|
|
615
|
+
"""A small panel with a header button that toggles visibility of content.
|
|
616
|
+
- title: display title for the header button
|
|
617
|
+
- content_widget: the QWidget that will be shown/hidden when toggled
|
|
618
|
+
"""
|
|
619
|
+
|
|
620
|
+
def __init__(self, title: str, content_widget: QWidget):
|
|
621
|
+
super().__init__()
|
|
622
|
+
|
|
623
|
+
# Track whether the panel is currently expanded
|
|
624
|
+
self.is_open = True
|
|
625
|
+
|
|
626
|
+
# The widget that will be shown or hidden
|
|
627
|
+
self.content = content_widget
|
|
628
|
+
|
|
629
|
+
# Button that acts as the collapsible panel header
|
|
630
|
+
self.toggle_btn = QPushButton(f"▼ {title}")
|
|
631
|
+
self.toggle_btn.setCheckable(True)
|
|
632
|
+
self.toggle_btn.setChecked(True)
|
|
633
|
+
|
|
634
|
+
# Connect button click to expand/collapse behavior
|
|
635
|
+
self.toggle_btn.clicked.connect(self.toggle)
|
|
636
|
+
|
|
637
|
+
# Visual styling for the header button
|
|
638
|
+
# (kept as a raw stylesheet string to preserve look-and-feel)
|
|
639
|
+
self.toggle_btn.setStyleSheet("""
|
|
640
|
+
QPushButton {
|
|
641
|
+
background-color: #0d1522;
|
|
642
|
+
border: 1px solid #2d3a4e;
|
|
643
|
+
border-radius: 6px;
|
|
644
|
+
padding: 6px;
|
|
645
|
+
color: #e5f0ff;
|
|
646
|
+
text-align: left;
|
|
647
|
+
font-weight: 600;
|
|
648
|
+
font-size: 13px;
|
|
649
|
+
}
|
|
650
|
+
QPushButton:checked { background-color: #162234; }
|
|
651
|
+
""")
|
|
652
|
+
|
|
653
|
+
# Vertical layout: header button on top, content below
|
|
654
|
+
layout = QVBoxLayout(self)
|
|
655
|
+
layout.setContentsMargins(0, 0, 0, 0)
|
|
656
|
+
layout.addWidget(self.toggle_btn)
|
|
657
|
+
layout.addWidget(self.content)
|
|
658
|
+
|
|
659
|
+
def toggle(self):
|
|
660
|
+
"""Flip the panel open/closed and update the header arrow."""
|
|
661
|
+
# Toggle open/closed state
|
|
662
|
+
self.is_open = not self.is_open
|
|
663
|
+
|
|
664
|
+
# Show or hide the content widget
|
|
665
|
+
self.content.setVisible(self.is_open)
|
|
666
|
+
|
|
667
|
+
# Update arrow icon in the header text (preserve the existing title)
|
|
668
|
+
self.toggle_btn.setText(
|
|
669
|
+
f"{'▼' if self.is_open else '►'} {self.toggle_btn.text()[2:]}"
|
|
670
|
+
)
|
|
671
|
+
|
|
672
|
+
# ===================================
|
|
673
|
+
# Mission Widget (Main Tab)
|
|
674
|
+
# ===================================
|
|
675
|
+
class MissionWidget(TabWidget):
|
|
676
|
+
"""Main mission tab widget used in the application's tab view.
|
|
677
|
+
Includes:
|
|
678
|
+
- Allowing the user to create and manage mission segments
|
|
679
|
+
- Displaying segment details in collapsible panels
|
|
680
|
+
- Saving/loading mission data to/from the global `values` object
|
|
681
|
+
- Interacting with RCAIDE mission objects when saving
|
|
682
|
+
"""
|
|
683
|
+
|
|
684
|
+
def __init__(self, shared_analysis_widget=None):
|
|
685
|
+
super().__init__()
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
# List of MissionSegmentWidget instances currently in the mission
|
|
689
|
+
self.segment_widgets = []
|
|
690
|
+
# Indices of segments that have been disabled by the user
|
|
691
|
+
self.disabled_segments = set()
|
|
692
|
+
|
|
693
|
+
main_layout = QVBoxLayout(self)
|
|
694
|
+
|
|
695
|
+
left_col = QWidget()
|
|
696
|
+
left_v = QVBoxLayout(left_col)
|
|
697
|
+
left_v.setContentsMargins(0, 0, 0, 0)
|
|
698
|
+
left_v.setSpacing(8)
|
|
699
|
+
|
|
700
|
+
right_col = QWidget()
|
|
701
|
+
right_v = QVBoxLayout(right_col)
|
|
702
|
+
right_v.setContentsMargins(0, 0, 0, 0)
|
|
703
|
+
right_v.setSpacing(8)
|
|
704
|
+
|
|
705
|
+
segments_box = QGroupBox("Mission Setup")
|
|
706
|
+
segments_v = QVBoxLayout(segments_box)
|
|
707
|
+
|
|
708
|
+
# Small notice label
|
|
709
|
+
self.segment_notice = QLabel("")
|
|
710
|
+
self.segment_notice.setVisible(False)
|
|
711
|
+
self.segment_notice.setStyleSheet("""
|
|
712
|
+
QLabel {
|
|
713
|
+
color:#8fd3ff;
|
|
714
|
+
background:#0d223a;
|
|
715
|
+
border:1px solid #1f6feb;
|
|
716
|
+
border-radius:4px;
|
|
717
|
+
padding:4px 8px;
|
|
718
|
+
}
|
|
719
|
+
""")
|
|
720
|
+
segments_v.addWidget(self.segment_notice)
|
|
721
|
+
|
|
722
|
+
# Row: mission name input
|
|
723
|
+
name_row = QHBoxLayout()
|
|
724
|
+
name_label = QLabel("Segment Name:")
|
|
725
|
+
name_label.setStyleSheet("color:#dbe7ff;")
|
|
726
|
+
self.mission_name_input = QLineEdit()
|
|
727
|
+
self.mission_name_input.setPlaceholderText("Enter mission name...")
|
|
728
|
+
name_row.addWidget(name_label)
|
|
729
|
+
name_row.addWidget(self.mission_name_input)
|
|
730
|
+
segments_v.addLayout(name_row)
|
|
731
|
+
|
|
732
|
+
# Row: action buttons (add segment, save data)
|
|
733
|
+
btn_row = QHBoxLayout()
|
|
734
|
+
self.add_segment_btn = QPushButton("➕ Add Segment")
|
|
735
|
+
self.save_btn = QPushButton("💾 Save All Data")
|
|
736
|
+
for b in (self.add_segment_btn, self.save_btn):
|
|
737
|
+
b.setStyleSheet("background-color:#141b29; color:#e5f0ff;")
|
|
738
|
+
|
|
739
|
+
# Wire button clicks to handler methods (no logic change)
|
|
740
|
+
self.add_segment_btn.clicked.connect(self.add_segment)
|
|
741
|
+
self.save_btn.clicked.connect(self.save_all_data)
|
|
742
|
+
|
|
743
|
+
btn_row.addWidget(self.add_segment_btn)
|
|
744
|
+
btn_row.addWidget(self.save_btn)
|
|
745
|
+
segments_v.addLayout(btn_row)
|
|
746
|
+
|
|
747
|
+
# Tree widget to list mission segments and provide checkboxes
|
|
748
|
+
self.tree = QTreeWidget()
|
|
749
|
+
self.tree.setHeaderLabels(["Mission Segments"])
|
|
750
|
+
self.tree.setRootIsDecorated(False)
|
|
751
|
+
self.tree.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
|
|
752
|
+
segments_v.addWidget(self.tree)
|
|
753
|
+
|
|
754
|
+
# Action row: disable/enable segments and clear selected
|
|
755
|
+
action_row = QHBoxLayout()
|
|
756
|
+
self.disable_btn = QPushButton("Disable / Enable")
|
|
757
|
+
self.clear_btn = QPushButton("🗑 Clear Selected")
|
|
758
|
+
self.clear_btn.setStyleSheet(
|
|
759
|
+
"background:#2a1e1e; color:#ffb4b4; border:1px solid #5c2b2b;"
|
|
760
|
+
)
|
|
761
|
+
|
|
762
|
+
action_row.addWidget(self.disable_btn)
|
|
763
|
+
action_row.addStretch()
|
|
764
|
+
action_row.addWidget(self.clear_btn)
|
|
765
|
+
segments_v.addLayout(action_row)
|
|
766
|
+
|
|
767
|
+
overview_box = QGroupBox("Mission Overview")
|
|
768
|
+
overview_v = QVBoxLayout(overview_box)
|
|
769
|
+
self.profile_widget = MissionProfileWidget()
|
|
770
|
+
self.summary_table = MissionSummaryTable(self)
|
|
771
|
+
overview_v.addWidget(self.profile_widget)
|
|
772
|
+
overview_v.addWidget(self.summary_table)
|
|
773
|
+
overview_v.setStretch(0, 3)
|
|
774
|
+
overview_v.setStretch(1, 2)
|
|
775
|
+
left_v.addWidget(overview_box)
|
|
776
|
+
|
|
777
|
+
|
|
778
|
+
details_box = QGroupBox("Segment Details")
|
|
779
|
+
details_v = QVBoxLayout(details_box)
|
|
780
|
+
|
|
781
|
+
self.details_scroll = QScrollArea()
|
|
782
|
+
self.details_scroll.setWidgetResizable(True)
|
|
783
|
+
details_container = QWidget()
|
|
784
|
+
self.details_layout = QVBoxLayout(details_container)
|
|
785
|
+
self.details_scroll.setWidget(details_container)
|
|
786
|
+
|
|
787
|
+
details_v.addWidget(self.details_scroll)
|
|
788
|
+
right_v.addWidget(segments_box, 0)
|
|
789
|
+
right_v.addWidget(details_box, 1)
|
|
790
|
+
|
|
791
|
+
left_col.setMinimumWidth(540)
|
|
792
|
+
left_col.setMaximumWidth(780)
|
|
793
|
+
right_col.setMinimumWidth(760)
|
|
794
|
+
|
|
795
|
+
content_splitter = QSplitter(Qt.Orientation.Horizontal)
|
|
796
|
+
content_splitter.setChildrenCollapsible(False)
|
|
797
|
+
content_splitter.addWidget(left_col)
|
|
798
|
+
content_splitter.addWidget(right_col)
|
|
799
|
+
content_splitter.setStretchFactor(0, 1)
|
|
800
|
+
content_splitter.setStretchFactor(1, 3)
|
|
801
|
+
content_splitter.setHandleWidth(10)
|
|
802
|
+
content_splitter.setSizes([640, 1120])
|
|
803
|
+
|
|
804
|
+
main_layout.addWidget(content_splitter)
|
|
805
|
+
|
|
806
|
+
self.disable_btn.clicked.connect(self.disable_enable_selected)
|
|
807
|
+
self.clear_btn.clicked.connect(self.clear_selected_segments)
|
|
808
|
+
self.refresh_mission_overview()
|
|
809
|
+
|
|
810
|
+
# ====================================================
|
|
811
|
+
# Utilities
|
|
812
|
+
# ====================================================
|
|
813
|
+
def _notify(self, text):
|
|
814
|
+
"""Briefly display a message in the segment notice label"""
|
|
815
|
+
self.segment_notice.setText(text)
|
|
816
|
+
self.segment_notice.setVisible(True)
|
|
817
|
+
QTimer.singleShot(2000, lambda: self.segment_notice.setVisible(False))
|
|
818
|
+
|
|
819
|
+
def _checked_indices(self):
|
|
820
|
+
"""Return a list of indices of checked (selected) segments in the tree."""
|
|
821
|
+
return [
|
|
822
|
+
i for i in range(self.tree.topLevelItemCount())
|
|
823
|
+
if self.tree.topLevelItem(i).checkState(0) == Qt.CheckState.Checked
|
|
824
|
+
]
|
|
825
|
+
|
|
826
|
+
def _selected_indices(self):
|
|
827
|
+
"""Return sorted indices for highlighted rows in the segment tree."""
|
|
828
|
+
indices = []
|
|
829
|
+
for item in self.tree.selectedItems():
|
|
830
|
+
idx = self.tree.indexOfTopLevelItem(item)
|
|
831
|
+
if idx >= 0:
|
|
832
|
+
indices.append(idx)
|
|
833
|
+
return sorted(set(indices))
|
|
834
|
+
|
|
835
|
+
def _panel_style(self, disabled):
|
|
836
|
+
"""Return stylesheet string for a segment panel; visually indicates disabled state."""
|
|
837
|
+
return f"""
|
|
838
|
+
#segmentPanel {{
|
|
839
|
+
background:{'#1a1f2a' if disabled else '#141b29'};
|
|
840
|
+
border:1px solid #2d3a4e;
|
|
841
|
+
border-radius:6px;
|
|
842
|
+
padding:6px;
|
|
843
|
+
margin:4px 0;
|
|
844
|
+
opacity:{'0.45' if disabled else '1.0'};
|
|
845
|
+
}}
|
|
846
|
+
"""
|
|
847
|
+
|
|
848
|
+
def _wire_segment_summary_signals(self, seg):
|
|
849
|
+
if hasattr(seg, "segment_name_input"):
|
|
850
|
+
seg.segment_name_input.textChanged.connect(self.refresh_mission_overview)
|
|
851
|
+
if hasattr(seg, "nested_dropdown"):
|
|
852
|
+
seg.nested_dropdown.currentIndexChanged.connect(self.refresh_mission_overview)
|
|
853
|
+
if hasattr(seg, "top_dropdown"):
|
|
854
|
+
seg.top_dropdown.currentIndexChanged.connect(self.refresh_mission_overview)
|
|
855
|
+
if hasattr(seg, "config_selector"):
|
|
856
|
+
seg.config_selector.currentIndexChanged.connect(self.refresh_mission_overview)
|
|
857
|
+
if hasattr(seg, "ctrl_points"):
|
|
858
|
+
seg.ctrl_points.textChanged.connect(self.refresh_mission_overview)
|
|
859
|
+
|
|
860
|
+
def refresh_mission_overview(self):
|
|
861
|
+
phases = []
|
|
862
|
+
for idx, seg in enumerate(self.segment_widgets):
|
|
863
|
+
if idx in self.disabled_segments:
|
|
864
|
+
continue
|
|
865
|
+
|
|
866
|
+
name = ""
|
|
867
|
+
if hasattr(seg, "segment_name_input"):
|
|
868
|
+
name = seg.segment_name_input.text().strip()
|
|
869
|
+
if not name and idx < self.tree.topLevelItemCount():
|
|
870
|
+
name = self.tree.topLevelItem(idx).text(0).strip()
|
|
871
|
+
if not name and hasattr(seg, "nested_dropdown"):
|
|
872
|
+
name = seg.nested_dropdown.currentText().strip()
|
|
873
|
+
ptype = ""
|
|
874
|
+
if hasattr(seg, "top_dropdown"):
|
|
875
|
+
ptype = seg.top_dropdown.currentText().strip().lower()
|
|
876
|
+
sub_text = ""
|
|
877
|
+
if hasattr(seg, "nested_dropdown"):
|
|
878
|
+
sub_text = seg.nested_dropdown.currentText().strip().lower()
|
|
879
|
+
if ptype == "vertical flight" and sub_text:
|
|
880
|
+
ptype = f"vertical {sub_text}"
|
|
881
|
+
elif ptype == "ground" and sub_text:
|
|
882
|
+
ptype = f"ground {sub_text}"
|
|
883
|
+
elif ptype == "single_point":
|
|
884
|
+
ptype = "single_point"
|
|
885
|
+
elif not ptype and sub_text:
|
|
886
|
+
ptype = sub_text
|
|
887
|
+
phases.append((name or f"Segment {idx + 1}", ptype or "other"))
|
|
888
|
+
|
|
889
|
+
self.profile_widget.set_phases(phases)
|
|
890
|
+
self.summary_table.update_table()
|
|
891
|
+
# ====================================================
|
|
892
|
+
# Disable / Enable
|
|
893
|
+
# ====================================================
|
|
894
|
+
|
|
895
|
+
def disable_enable_selected(self):
|
|
896
|
+
"""Toggle enabled/disabled state for all checked segments."""
|
|
897
|
+
indices = self._checked_indices()
|
|
898
|
+
# If user selects nothing, show a warning and return
|
|
899
|
+
if not indices:
|
|
900
|
+
self._notify("⚠ No segments selected")
|
|
901
|
+
return
|
|
902
|
+
|
|
903
|
+
messages = []
|
|
904
|
+
|
|
905
|
+
for idx in indices:
|
|
906
|
+
seg = self.segment_widgets[idx]
|
|
907
|
+
item = self.tree.topLevelItem(idx)
|
|
908
|
+
name = item.text(0)
|
|
909
|
+
# Each segment has a corresponding panel + separator line; panel is at idx*2
|
|
910
|
+
panel = self.details_layout.itemAt(idx * 2).widget()
|
|
911
|
+
|
|
912
|
+
if idx in self.disabled_segments:
|
|
913
|
+
# Enable
|
|
914
|
+
seg.setDisabled(False)
|
|
915
|
+
self.disabled_segments.remove(idx)
|
|
916
|
+
panel.setStyleSheet(self._panel_style(False))
|
|
917
|
+
messages.append(f"✅ '{name}' enabled")
|
|
918
|
+
else:
|
|
919
|
+
# Disable
|
|
920
|
+
seg.setDisabled(True)
|
|
921
|
+
self.disabled_segments.add(idx)
|
|
922
|
+
panel.setStyleSheet(self._panel_style(True))
|
|
923
|
+
messages.append(f"⛔ '{name}' disabled")
|
|
924
|
+
|
|
925
|
+
# Show last action message (enable/disable)
|
|
926
|
+
self._notify(messages[-1])
|
|
927
|
+
self.refresh_mission_overview()
|
|
928
|
+
|
|
929
|
+
# ===============================
|
|
930
|
+
# Clear Selected Segments
|
|
931
|
+
# ===============================
|
|
932
|
+
def clear_selected_segments(self):
|
|
933
|
+
"""Remove highlighted segments from UI and internal lists."""
|
|
934
|
+
# Remove from highest index to avoid reindex issues
|
|
935
|
+
indices = sorted(self._selected_indices(), reverse=True)
|
|
936
|
+
if not indices:
|
|
937
|
+
self._notify("⚠ No segments selected")
|
|
938
|
+
return
|
|
939
|
+
|
|
940
|
+
messages = []
|
|
941
|
+
|
|
942
|
+
for idx in indices:
|
|
943
|
+
item = self.tree.topLevelItem(idx)
|
|
944
|
+
name = item.text(0)
|
|
945
|
+
|
|
946
|
+
# Remove from tree and internal segment list
|
|
947
|
+
self.tree.takeTopLevelItem(idx)
|
|
948
|
+
self.segment_widgets.pop(idx)
|
|
949
|
+
self.disabled_segments = {
|
|
950
|
+
i - 1 if i > idx else i
|
|
951
|
+
for i in self.disabled_segments
|
|
952
|
+
if i != idx
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
# Each segment consists of a panel and a horizontal line => remove both
|
|
956
|
+
for _ in range(2):
|
|
957
|
+
it = self.details_layout.takeAt(idx * 2)
|
|
958
|
+
if it and it.widget():
|
|
959
|
+
it.widget().deleteLater()
|
|
960
|
+
|
|
961
|
+
messages.append(f"🗑 '{name}' cleared")
|
|
962
|
+
|
|
963
|
+
# Show last cleared segment
|
|
964
|
+
self._notify(messages[-1])
|
|
965
|
+
self.refresh_mission_overview()
|
|
966
|
+
|
|
967
|
+
# ====================================================
|
|
968
|
+
# Add Segment
|
|
969
|
+
# ====================================================
|
|
970
|
+
def add_segment(self):
|
|
971
|
+
"""Create a new MissionSegmentWidget and add it to the UI and internal state."""
|
|
972
|
+
seg = MissionSegmentWidget()
|
|
973
|
+
# Default to a simple cruise segment for quick GUI validation
|
|
974
|
+
#try:
|
|
975
|
+
#seg.top_dropdown.setCurrentIndex(1) # Cruise
|
|
976
|
+
#seg.populate_nested_dropdown(seg.top_dropdown.currentIndex())
|
|
977
|
+
#seg.nested_dropdown.setCurrentText("Constant Speed/Constant Altitude")
|
|
978
|
+
#seg.segment_name_input.setText("cruise")
|
|
979
|
+
##if hasattr(seg, "_apply_defaults"):
|
|
980
|
+
##seg._apply_defaults()
|
|
981
|
+
#except Exception:
|
|
982
|
+
#pass
|
|
983
|
+
|
|
984
|
+
self.segment_widgets.append(seg)
|
|
985
|
+
self._wire_segment_summary_signals(seg)
|
|
986
|
+
|
|
987
|
+
# If user provided a mission name in the input, use it; otherwise fall back
|
|
988
|
+
name = self.mission_name_input.text().strip()
|
|
989
|
+
if not name:
|
|
990
|
+
name = f"Segment {len(self.segment_widgets)}"
|
|
991
|
+
|
|
992
|
+
# Wrap the segment in a collapsible panel and add to details layout
|
|
993
|
+
panel = CollapsiblePanel(name.title(), seg)
|
|
994
|
+
panel.setObjectName("segmentPanel")
|
|
995
|
+
panel.setStyleSheet(self._panel_style(False))
|
|
996
|
+
self.details_layout.addWidget(panel)
|
|
997
|
+
|
|
998
|
+
# Add a visually separating horizontal line
|
|
999
|
+
line = QFrame()
|
|
1000
|
+
line.setFrameShape(QFrame.Shape.HLine)
|
|
1001
|
+
line.setStyleSheet("color:#2b3648; margin:2px 0;")
|
|
1002
|
+
self.details_layout.addWidget(line)
|
|
1003
|
+
|
|
1004
|
+
# Add the segment to the list tree (with an unchecked checkbox)
|
|
1005
|
+
item = QTreeWidgetItem([name.title()])
|
|
1006
|
+
item.setFlags(item.flags() | Qt.ItemFlag.ItemIsUserCheckable)
|
|
1007
|
+
item.setCheckState(0, Qt.CheckState.Checked)
|
|
1008
|
+
self.tree.addTopLevelItem(item)
|
|
1009
|
+
|
|
1010
|
+
# Clear the input and notify the user
|
|
1011
|
+
self.mission_name_input.clear()
|
|
1012
|
+
self.refresh_mission_overview()
|
|
1013
|
+
self._notify(f"✓ Added '{name.title()}'")
|
|
1014
|
+
|
|
1015
|
+
def save_all_data(self):
|
|
1016
|
+
"""
|
|
1017
|
+
Save mission data and build the RCAIDE mission object.
|
|
1018
|
+
This function must not build or modify aircraft configurations.
|
|
1019
|
+
"""
|
|
1020
|
+
import rcaide_io
|
|
1021
|
+
|
|
1022
|
+
# Reset stored mission data before saving
|
|
1023
|
+
rcaide_io.mission_data = []
|
|
1024
|
+
|
|
1025
|
+
# Save analyses that were already defined by the user
|
|
1026
|
+
|
|
1027
|
+
# Collect data from each enabled mission segment
|
|
1028
|
+
for idx, seg in enumerate(self.segment_widgets):
|
|
1029
|
+
if idx in self.disabled_segments:
|
|
1030
|
+
continue
|
|
1031
|
+
|
|
1032
|
+
# Save only the form state here; building RCAIDE segments requires analyses and happens later.
|
|
1033
|
+
seg_data = seg.get_form_data()
|
|
1034
|
+
seg_data["Segment Name"] = self.tree.topLevelItem(idx).text(0)
|
|
1035
|
+
rcaide_io.mission_data.append(seg_data)
|
|
1036
|
+
|
|
1037
|
+
# Create the RCAIDE mission using existing data only
|
|
1038
|
+
# Defer mission build until analyses are saved
|
|
1039
|
+
|
|
1040
|
+
segment_names = [
|
|
1041
|
+
segment.get("Segment Name", f"Segment {index + 1}")
|
|
1042
|
+
for index, segment in enumerate(rcaide_io.mission_data)
|
|
1043
|
+
]
|
|
1044
|
+
print("\n[Mission Data Saved]")
|
|
1045
|
+
print(f"Active segments: {len(rcaide_io.mission_data)}")
|
|
1046
|
+
print(f"Names: {', '.join(segment_names) if segment_names else 'None'}")
|
|
1047
|
+
|
|
1048
|
+
# Notify the user that the mission was saved
|
|
1049
|
+
self._notify("Mission data saved")
|
|
1050
|
+
self.refresh_mission_overview()
|
|
1051
|
+
|
|
1052
|
+
|
|
1053
|
+
def create_rcaide_mission(self):
|
|
1054
|
+
# Create an empty sequential mission container
|
|
1055
|
+
rcaide_mission = RCAIDE.Framework.Mission.Sequential_Segments()
|
|
1056
|
+
rcaide_mission.tag = self.mission_name_input.text()
|
|
1057
|
+
|
|
1058
|
+
# Add each enabled segment to the mission
|
|
1059
|
+
for idx, seg in enumerate(self.segment_widgets):
|
|
1060
|
+
if idx in self.disabled_segments:
|
|
1061
|
+
continue
|
|
1062
|
+
|
|
1063
|
+
_, rcaide_segment = seg.get_data()
|
|
1064
|
+
|
|
1065
|
+
# Ensure analyses exist before assigning them to a segment
|
|
1066
|
+
if not rcaide_io.rcaide_analyses:
|
|
1067
|
+
raise RuntimeError("No RCAIDE analyses available")
|
|
1068
|
+
|
|
1069
|
+
# If the segment has no analyses (should be rare), assign a fallback
|
|
1070
|
+
if not getattr(rcaide_segment, "analyses", None):
|
|
1071
|
+
rcaide_segment.analyses = next(iter(rcaide_io.rcaide_analyses.values()))
|
|
1072
|
+
|
|
1073
|
+
# Append the segment to the mission sequence
|
|
1074
|
+
rcaide_mission.append_segment(rcaide_segment)
|
|
1075
|
+
|
|
1076
|
+
return rcaide_mission
|
|
1077
|
+
|
|
1078
|
+
# ====================================================
|
|
1079
|
+
# Load Mission
|
|
1080
|
+
# ====================================================
|
|
1081
|
+
|
|
1082
|
+
def load_from_values(self):
|
|
1083
|
+
"""Populate the UI from `rcaide_io.mission_data` previously saved."""
|
|
1084
|
+
# Reset UI and internal lists
|
|
1085
|
+
self.tree.clear()
|
|
1086
|
+
self.segment_widgets = []
|
|
1087
|
+
self.disabled_segments.clear()
|
|
1088
|
+
|
|
1089
|
+
# Clear existing widgets in the details layout
|
|
1090
|
+
for i in reversed(range(self.details_layout.count())):
|
|
1091
|
+
w = self.details_layout.itemAt(i).widget()
|
|
1092
|
+
if w:
|
|
1093
|
+
w.deleteLater()
|
|
1094
|
+
|
|
1095
|
+
# Recreate segments from saved data (handles both old GUI and new RCAIDE formats)
|
|
1096
|
+
for seg_data in _extract_gui_segments(rcaide_io.mission_data):
|
|
1097
|
+
seg = MissionSegmentWidget()
|
|
1098
|
+
seg.load_data(seg_data)
|
|
1099
|
+
self.segment_widgets.append(seg)
|
|
1100
|
+
self._wire_segment_summary_signals(seg)
|
|
1101
|
+
|
|
1102
|
+
name = seg_data.get("Segment Name", "Segment")
|
|
1103
|
+
panel = CollapsiblePanel(name.title(), seg)
|
|
1104
|
+
panel.setObjectName("segmentPanel")
|
|
1105
|
+
panel.setStyleSheet(self._panel_style(False))
|
|
1106
|
+
self.details_layout.addWidget(panel)
|
|
1107
|
+
|
|
1108
|
+
line = QFrame()
|
|
1109
|
+
line.setFrameShape(QFrame.Shape.HLine)
|
|
1110
|
+
line.setStyleSheet("color:#2b3648; margin:2px 0;")
|
|
1111
|
+
self.details_layout.addWidget(line)
|
|
1112
|
+
|
|
1113
|
+
item = QTreeWidgetItem([name.title()])
|
|
1114
|
+
item.setFlags(item.flags() | Qt.ItemFlag.ItemIsUserCheckable)
|
|
1115
|
+
item.setCheckState(0, Qt.CheckState.Checked)
|
|
1116
|
+
self.tree.addTopLevelItem(item)
|
|
1117
|
+
|
|
1118
|
+
# Defer mission build until analyses are saved to avoid load-time errors.
|
|
1119
|
+
self.refresh_mission_overview()
|
|
1120
|
+
|
|
1121
|
+
|
|
1122
|
+
def get_widget() -> QWidget:
|
|
1123
|
+
"""Factory helper used to create a MissionWidget instance for the tab system."""
|
|
1124
|
+
return MissionWidget()
|