Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 61 additions & 10 deletions mslib/msui/flighttrack.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -132,12 +142,19 @@ class Waypoint:

def __init__(self, lat=0., lon=0., flightlevel=0., location="", comments=""):
self.location = location
locations = config_loader(dataset='locations')
if location in locations:
self.lat, self.lon = locations[location]
else:

# Prefer explicitly provided coordinates (e.g., from server/XML)
if lat != 0.0 or lon != 0.0:
self.lat = lat
self.lon = lon
else:
locations = config_loader(dataset='locations')
if location in locations:
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.
Expand Down Expand Up @@ -186,6 +203,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
Expand Down Expand Up @@ -639,22 +657,36 @@ 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)
element.setAttribute("location", str(wp.location))
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):
Expand All @@ -671,16 +703,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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why was this removed?



#
# CLASS WaypointDelegate
#
Expand Down
29 changes: 26 additions & 3 deletions mslib/msui/viewwindows.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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):
Expand Down
35 changes: 35 additions & 0 deletions tests/_test_msui/test_flighttrack.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = """
<FlightTrack version="test">
<Revision id="42" name="draft"/>
<ListOfWaypoints>
<Waypoint location="" lat="0" lon="0" flightlevel="100">
<Comments></Comments>
</Waypoint>
</ListOfWaypoints>
</FlightTrack>
"""

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 = """
<FlightTrack version="test">
<ListOfWaypoints>
<Waypoint location="" lat="0" lon="0" flightlevel="100">
<Comments></Comments>
</Waypoint>
</ListOfWaypoints>
</FlightTrack>
"""

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