diff --git a/mslib/msui/flighttrack.py b/mslib/msui/flighttrack.py index 061d4f830..9cf2d17e6 100644 --- a/mslib/msui/flighttrack.py +++ b/mslib/msui/flighttrack.py @@ -50,7 +50,6 @@ from mslib.utils.coordinate import path_points, get_distance from mslib.utils.find_location import find_location from mslib.utils import thermolib -from mslib.utils.verify_waypoint_data import verify_waypoint_data from mslib.utils.config import config_loader, save_settings_qsettings, load_settings_qsettings from mslib.utils.qt import variant_to_string, variant_to_float from mslib.msui.performance_settings import DEFAULT_PERFORMANCE @@ -63,6 +62,17 @@ TIME_UTC = 9 +class Revision: + """ + Represents a revision of a flight track. + ID is mandatory, name is optional. + """ + + def __init__(self, revision_id, name=None): + self.id = revision_id + self.name = name + + def seconds_to_string(seconds): """ Format a time given in seconds to a string HH:MM:SS. Used for the @@ -132,12 +142,16 @@ class Waypoint: def __init__(self, lat=0., lon=0., flightlevel=0., location="", comments=""): self.location = location + + # Prefer explicitly provided coordinates (e.g. from XML/server) + self.lat = lat + self.lon = lon + + # Only resolve from configured locations if coordinates were not meaningfully provided locations = config_loader(dataset='locations') - if location in locations: + if location in locations and (lat == 0.0 and lon == 0.0): self.lat, self.lon = locations[location] - else: - self.lat = lat - self.lon = lon + self.flightlevel = flightlevel self.pressure = thermolib.flightlevel2pressure(flightlevel * units.hft).magnitude self.distance_to_prev = 0. @@ -186,6 +200,7 @@ def __init__(self, name="", filename=None, waypoints=None, mscolab_mode=False, data_dir=config_loader(dataset="mss_dir"), xml_content=None): super().__init__() + self.revision = None self.name = name # a name for this flight track self.filename = filename # filename for store/load self.data_dir = data_dir @@ -639,12 +654,24 @@ def save_to_ftml(self, filename=None): def get_xml_doc(self): doc = xml.dom.minidom.Document() # nosec, we take care of writing correct XML + ft_el = doc.createElement("FlightTrack") ft_el.setAttribute("version", __version__) doc.appendChild(ft_el) # The list of waypoint elements. + + # Write revision information + if self.revision is not None: + rev_el = doc.createElement("Revision") + rev_el.setAttribute("id", str(self.revision.id)) + if self.revision.name: + rev_el.setAttribute("name", self.revision.name) + ft_el.appendChild(rev_el) + + # List of waypoints wp_el = doc.createElement("ListOfWaypoints") ft_el.appendChild(wp_el) + for wp in self.waypoints: element = doc.createElement("Waypoint") wp_el.appendChild(element) @@ -652,9 +679,11 @@ def get_xml_doc(self): element.setAttribute("lat", str(wp.lat)) element.setAttribute("lon", str(wp.lon)) element.setAttribute("flightlevel", str(wp.flightlevel)) + comments = doc.createElement("Comments") comments.appendChild(doc.createTextNode(str(wp.comments))) element.appendChild(comments) + return doc def get_xml_content(self): @@ -671,16 +700,35 @@ def load_from_ftml(self, filename): def load_from_xml_data(self, xml_content, name="Flight track"): self.name = name - if verify_waypoint_data(xml_content): - _waypoints_list = load_from_xml_data(xml_content, name) - self.replace_waypoints(_waypoints_list) + + try: + doc = defusedxml.minidom.parseString(xml_content) + except DefusedXmlException as ex: + raise SyntaxError(str(ex)) + + ft_el = doc.getElementsByTagName("FlightTrack")[0] + + # Load revision + revision_nodes = ft_el.getElementsByTagName("Revision") + if revision_nodes: + rev_el = revision_nodes[0] + revision_id = int(rev_el.getAttribute("id")) + revision_name = rev_el.getAttribute("name") or None + self.revision = Revision(revision_id, revision_name) else: - raise SyntaxError(f"Invalid flight track filename: {name}") + # Backward compatibility: create deterministic default revision + self.revision = Revision( + revision_id=0, + name=None + ) + + # Validate only waypoint structure, revision is optional metadata + _waypoints_list = load_from_xml_data(xml_content, name) + self.replace_waypoints(_waypoints_list) def get_filename(self): return self.filename - # # CLASS WaypointDelegate # diff --git a/mslib/msui/viewwindows.py b/mslib/msui/viewwindows.py index eb1f936ca..5bbbb612a 100644 --- a/mslib/msui/viewwindows.py +++ b/mslib/msui/viewwindows.py @@ -34,6 +34,23 @@ from mslib.utils.config import save_settings_qsettings +def format_operation_with_revision(model): + """ + Return operation name including revision id and optional revision name. + """ + if model is None: + return "" + + rev = getattr(model, "revision", None) + if rev is None: + return model.name + + label = f"{model.name} [rev: {rev.id}]" + if getattr(rev, "name", None): + label += f" ({rev.name})" + return label + + class MSUIViewWindow(QtWidgets.QMainWindow): """ Derives QMainWindow to provide some common functionality to all @@ -109,7 +126,9 @@ def setFlightTrackModel(self, model): """ # Update title flighttrack name if self.waypoints_model: - self.setWindowTitle(self.windowTitle().replace(self.waypoints_model.name, model.name)) + old_label = format_operation_with_revision(self.waypoints_model) + new_label = format_operation_with_revision(model) + self.setWindowTitle(self.windowTitle().replace(old_label, new_label)) self.waypoints_model = model @@ -274,11 +293,15 @@ def setFlightTrackModel(self, model): # Update Top View flighttrack name if hasattr(self.mpl.canvas, "map"): - self.mpl.canvas.map.ax.figure.suptitle(f"{model.name}", x=0.95, ha='right') + title = format_operation_with_revision(model) + self.mpl.canvas.map.ax.figure.suptitle(title, x=0.95, ha='right') + self.mpl.canvas.map.ax.figure.canvas.draw() elif hasattr(self.mpl.canvas, 'plotter'): - self.mpl.canvas.plotter.fig.suptitle(f"{model.name}", x=0.95, ha='right') + title = format_operation_with_revision(model) + self.mpl.canvas.plotter.fig.suptitle(title, x=0.95, ha='right') + self.mpl.canvas.plotter.fig.canvas.draw() def getView(self): diff --git a/tests/_test_msui/test_flighttrack.py b/tests/_test_msui/test_flighttrack.py index 5a8cc1476..44a6009fc 100644 --- a/tests/_test_msui/test_flighttrack.py +++ b/tests/_test_msui/test_flighttrack.py @@ -114,3 +114,38 @@ def test_isinstance_check_with_various_types(self): "Dict should pass isinstance dict check" assert isinstance({}, dict), \ "Empty dict should pass isinstance dict check" + + def test_load_ftml_with_revision(self): + xml_content = """ + + + + + + + + + """ + + model = WaypointsTableModel(xml_content=xml_content, name="test_track") + + assert model.revision is not None + assert model.revision.id == 42 + assert model.revision.name == "draft" + + def test_load_ftml_without_revision_creates_default_revision(self): + xml_content = """ + + + + + + + + """ + + model = WaypointsTableModel(xml_content=xml_content, name="legacy_track") + + assert model.revision is not None + assert isinstance(model.revision.id, int) + assert model.revision.name is None