From be29314aa43e944e6d08d9ae26af29270a2d03be Mon Sep 17 00:00:00 2001 From: "J.Ironman" Date: Thu, 3 Sep 2026 07:05:00 +0000 Subject: [PATCH 1/9] flighttrack: hold linear view data per waypoint Prepares the table view for showing the exact data values at a waypoint. The values of a linear section are computed for interpolated points along the flight path, of which only a few coincide with a waypoint; values_at_waypoints() picks those, linear_data_columns() turns the WMS responses of the linear view into columns. The values are stored on the Waypoint objects, so that they stay at their waypoint when waypoints are inserted or deleted, and appended as read only columns behind the columns of TABLE_FULL. They are hidden until set_linear_data_visible(True) is called, hence the table view looks unchanged for now. Changes of the data are announced with the new linearDataChanged signal instead of dataChanged, as the data is not part of the flight plan: dataChanged triggers a redraw of the flight path (and thus a new WMS request) and an update of an MSColab operation. Co-Authored-By: Claude Opus 5 (1M context) --- mslib/msui/flighttrack.py | 207 ++++++++++++++++++++++++++- tests/_test_msui/test_flighttrack.py | 89 +++++++++++- tests/utils.py | 18 +++ 3 files changed, 311 insertions(+), 3 deletions(-) diff --git a/mslib/msui/flighttrack.py b/mslib/msui/flighttrack.py index 45f327031..8ecb233f4 100644 --- a/mslib/msui/flighttrack.py +++ b/mslib/msui/flighttrack.py @@ -36,6 +36,7 @@ import datetime import logging +import math import os from pathlib import Path @@ -93,6 +94,90 @@ def seconds_to_string(seconds): TABLE_SHORT = [TABLE_FULL[_i] for _i in range(7)] + [TABLE_FULL[-1]] + [("", lambda _: "", False)] * 8 +# Number of the first table column that is filled with data retrieved by the +# linear view. All columns left of it are described by TABLE_FULL/TABLE_SHORT. +LINEAR_DATA_COLUMN = len(TABLE_FULL) + + +def values_at_waypoints(path_lats, path_lons, values, waypoints, tolerance=1e-6): + """ + Pick those values of a linear section that belong to the given waypoints. + + A linear section is computed for interpolated points along the flight + path, of which only a few coincide with a waypoint. The waypoints are + matched in the order in which they appear along the path. + + Arguments: + path_lats, path_lons -- coordinates of the interpolated points of the + linear section + values -- one data value per interpolated point + waypoints -- list of Waypoint objects the values are mapped onto + tolerance -- maximum deviation of a coordinate that is still considered + to be the same point + + Returns a list with one entry per waypoint. Waypoints that are not part + of the section, and waypoints where the data field is undefined (NaN), + get None assigned. + """ + result = [None] * len(waypoints) + index = 0 + for i, (lat, lon) in enumerate(zip(path_lats, path_lons)): + if index >= len(waypoints): + break + waypoint = waypoints[index] + # The path is sent to the WMS server rounded to two decimals, hence + # the returned coordinates have to be compared to rounded waypoints. + if abs(lat - float(f"{waypoint.lat:.2f}")) < tolerance and \ + abs(lon - float(f"{waypoint.lon:.2f}")) < tolerance: + if i < len(values) and not math.isnan(values[i]): + result[index] = values[i] + index += 1 + return result + + +def linear_data_columns(xmls, waypoints): + """ + Convert the linear section responses of the WMS server into data columns + at the positions of the given waypoints. + + Arguments: + xmls -- list of XML elements as returned for a "LINE:1" request, see + mslib.mswms.mpl_lsec + waypoints -- list of Waypoint objects the values are mapped onto + + Returns a list of dictionaries with the keys "name", "unit" and "values", + ready to be passed to WaypointsTableModel.set_linear_data(). + """ + columns = [] + names = set() + for element in xmls: + data = element.find("Data") + lats = element.find("Latitude") + lons = element.find("Longitude") + if any(node is None or node.text is None for node in (data, lats, lons)): + logging.debug("incomplete linear section data, skipping") + continue + title = element.find("Title") + name = title.text if title is not None and title.text else "Data" + # The name identifies the column, therefore it has to be unique. + unique_name = name + suffix = 1 + while unique_name in names: + suffix += 1 + unique_name = f"{name} ({suffix})" + names.add(unique_name) + + columns.append({ + "name": unique_name, + "unit": data.attrib.get("unit", ""), + "values": values_at_waypoints( + [float(value) for value in lats.text.split(",")], + [float(value) for value in lons.text.split(",")], + [float(value) for value in data.text.split(",")], + waypoints), + }) + return columns + def load_from_xml_data(xml_content, name="Flight track"): try: @@ -156,6 +241,10 @@ def __init__(self, lat=0., lon=0., flightlevel=0., location="", comments=""): self.wpnumber_major = None self.wpnumber_minor = None + # Data values retrieved by the linear view for the position of this + # waypoint (column name -> value). + self.linear_data = {} + def __str__(self): """ String representation of the waypoint (e.g., when used with the print @@ -179,6 +268,11 @@ class WaypointsTableModel(QtCore.QAbstractTableModel): # Signal emitted when a waypoint is moved, inserted or deleted changeMessageSignal = QtCore.pyqtSignal(str) + # Signal emitted when the data columns filled by the linear view have + # changed. This is deliberately not the dataChanged() signal, as the data + # does not belong to the flight plan itself and must not trigger a redraw + # of the flight path or an update of an MSColab operation. + linearDataChanged = QtCore.pyqtSignal() def __init__(self, name="", filename=None, waypoints=None, mscolab_mode=False, data_dir=config_loader(dataset="mss_dir"), @@ -192,6 +286,11 @@ def __init__(self, name="", filename=None, waypoints=None, mscolab_mode=False, # file-save events are handled in a different manner self.mscolab_mode = mscolab_mode + # Data columns filled by the linear view, a list of (name, unit) + # tuples. The values themselves are stored per waypoint. + self.linear_data_columns = [] + self.linear_data_visible = False + # self.aircraft.setErrorHandling("permissive") self.settings_tag = "performance" self.load_settings() @@ -235,7 +334,8 @@ def flags(self, index): table = TABLE_SHORT if self.performance_settings["visible"]: table = TABLE_FULL - if table[column][2]: + # The data columns of the linear view are read only. + if column < LINEAR_DATA_COLUMN and table[column][2]: return QtCore.Qt.ItemFlags( int(QtCore.QAbstractTableModel.flags(self, index) | QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsDragEnabled | @@ -263,6 +363,8 @@ def data(self, index, role=QtCore.Qt.DisplayRole): waypoint = waypoints[index.row()] column = index.column() if role == QtCore.Qt.DisplayRole: + if column >= LINEAR_DATA_COLUMN: + return QtCore.QVariant(self.linear_data_value(waypoint, column)) if self.performance_settings["visible"]: return QtCore.QVariant(TABLE_FULL[column][1](waypoint)) else: @@ -310,6 +412,12 @@ def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole): return QtCore.QVariant() # Return the names of the table columns. if orientation == QtCore.Qt.Horizontal: + if section >= LINEAR_DATA_COLUMN: + columns = self.visible_linear_data_columns() + if not 0 <= section - LINEAR_DATA_COLUMN < len(columns): + return QtCore.QVariant() + name, unit = columns[section - LINEAR_DATA_COLUMN] + return QtCore.QVariant(f"{name}\n({unit})" if unit else name) if self.performance_settings["visible"]: return QtCore.QVariant(TABLE_FULL[section][0]) else: @@ -325,7 +433,102 @@ def rowCount(self, index=QtCore.QModelIndex()): return len(self.waypoints) def columnCount(self, index=QtCore.QModelIndex()): - return len(TABLE_FULL) + return LINEAR_DATA_COLUMN + len(self.visible_linear_data_columns()) + + def visible_linear_data_columns(self): + """ + Return the (name, unit) tuples of the data columns of the linear view + that are currently appended to the table. + """ + return self.linear_data_columns if self.linear_data_visible else [] + + def linear_data_value(self, waypoint, column): + """ + Return the value of the linear view data column at the given + waypoint, formatted for display. Waypoints without data (e.g. because + the aircraft is on the ground) give an empty string. + """ + columns = self.visible_linear_data_columns() + if not 0 <= column - LINEAR_DATA_COLUMN < len(columns): + return "" + value = waypoint.linear_data.get(columns[column - LINEAR_DATA_COLUMN][0]) + return "" if value is None else f"{value:.4g}" + + def set_linear_data_visible(self, visible): + """ + Show or hide the data columns filled by the linear view. + """ + visible = bool(visible) + if visible == self.linear_data_visible: + return + count = len(self.linear_data_columns) + if count == 0: + self.linear_data_visible = visible + elif visible: + self.beginInsertColumns(QtCore.QModelIndex(), LINEAR_DATA_COLUMN, LINEAR_DATA_COLUMN + count - 1) + self.linear_data_visible = True + self.endInsertColumns() + else: + self.beginRemoveColumns(QtCore.QModelIndex(), LINEAR_DATA_COLUMN, LINEAR_DATA_COLUMN + count - 1) + self.linear_data_visible = False + self.endRemoveColumns() + self.linearDataChanged.emit() + + def set_linear_data(self, columns): + """ + Store the data retrieved by the linear view. + + Arguments: + columns -- list of dictionaries with the keys "name", "unit" and + "values", where "values" contains one value per waypoint + (None where no data is available). The names have to be + unique, they identify the columns. See + linear_data_columns(). + """ + for i, waypoint in enumerate(self.waypoints): + waypoint.linear_data = { + column["name"]: column["values"][i] for column in columns + if i < len(column["values"]) and column["values"][i] is not None} + self._set_linear_data_columns([(column["name"], column["unit"]) for column in columns]) + + def set_linear_data_from_xml(self, xmls): + """ + Store the linear section data of the given WMS responses, see + linear_data_columns(). + """ + self.set_linear_data(linear_data_columns(xmls, self.waypoints)) + + def clear_linear_data(self): + """ + Drop all data retrieved by the linear view. + """ + for waypoint in self.waypoints: + waypoint.linear_data = {} + self._set_linear_data_columns([]) + + def _set_linear_data_columns(self, columns): + """ + Adapt the table to the given list of (name, unit) data columns and + notify the connected views. + """ + previous = len(self.linear_data_columns) + count = len(columns) + if not self.linear_data_visible or previous == count: + self.linear_data_columns = columns + elif count > previous: + self.beginInsertColumns( + QtCore.QModelIndex(), LINEAR_DATA_COLUMN + previous, LINEAR_DATA_COLUMN + count - 1) + self.linear_data_columns = columns + self.endInsertColumns() + else: + self.beginRemoveColumns( + QtCore.QModelIndex(), LINEAR_DATA_COLUMN + count, LINEAR_DATA_COLUMN + previous - 1) + self.linear_data_columns = columns + self.endRemoveColumns() + if self.linear_data_visible and count > 0: + self.headerDataChanged.emit( + QtCore.Qt.Horizontal, LINEAR_DATA_COLUMN, LINEAR_DATA_COLUMN + count - 1) + self.linearDataChanged.emit() def setData(self, index, value, role=QtCore.Qt.EditRole, update=True): """ diff --git a/tests/_test_msui/test_flighttrack.py b/tests/_test_msui/test_flighttrack.py index 4bea6f8a4..90065b7c9 100644 --- a/tests/_test_msui/test_flighttrack.py +++ b/tests/_test_msui/test_flighttrack.py @@ -24,9 +24,14 @@ limitations under the License. """ import json +import xml.etree.ElementTree as etree -from mslib.msui.flighttrack import WaypointsTableModel +from PyQt5 import QtCore + +from mslib.msui.flighttrack import (LINEAR_DATA_COLUMN, TABLE_FULL, Waypoint, WaypointsTableModel, + linear_data_columns, values_at_waypoints) from mslib.msui.performance_settings import DEFAULT_PERFORMANCE +from tests.utils import lsec_xml class Test_WaypointsTableModel_CorruptedSettings: @@ -101,3 +106,85 @@ def test_isinstance_check_with_various_types(self): "Dict should pass isinstance dict check" assert isinstance({}, dict), \ "Empty dict should pass isinstance dict check" + + +class Test_LinearData: + """ + Tests for the data columns that the linear view fills in the table view. + """ + + def setup_method(self): + self.waypoints = [Waypoint(0., 0., 0.), Waypoint(1., 1., 350.), + Waypoint(2., 2., 350.), Waypoint(3., 3., 0.)] + self.model = WaypointsTableModel("") + self.model.insertRows(0, rows=len(self.waypoints), waypoints=self.waypoints) + + def test_values_at_waypoints(self): + # Only the waypoints, not the interpolated points in between, are + # returned, and NaN values (aircraft on the ground) become None. + assert values_at_waypoints( + [0., 0.5, 1., 1.5, 2., 2.5, 3.], [0., 0.5, 1., 1.5, 2., 2.5, 3.], + [float("nan"), 5., 10., 12., 20., 22., float("nan")], + self.waypoints) == [None, 10., 20., None] + + def test_values_at_waypoints_without_matching_points(self): + assert values_at_waypoints([10., 11.], [10., 11.], [1., 2.], self.waypoints) == [None] * 4 + + def test_linear_data_columns(self): + columns = linear_data_columns([lsec_xml()], self.waypoints) + assert columns == [{"name": "Mole fraction of ozone (Linear)", + "unit": "ppmv", + "values": [None, 10., 20., None]}] + + def test_linear_data_columns_makes_names_unique(self): + columns = linear_data_columns([lsec_xml(), lsec_xml()], self.waypoints) + assert [column["name"] for column in columns] == ["Mole fraction of ozone (Linear)", + "Mole fraction of ozone (Linear) (2)"] + + def test_linear_data_columns_skips_incomplete_data(self): + incomplete = etree.fromstring("Empty") + assert linear_data_columns([incomplete], self.waypoints) == [] + + def test_columns_are_hidden_by_default(self): + self.model.set_linear_data_from_xml([lsec_xml()]) + assert self.model.columnCount() == len(TABLE_FULL) + assert self.model.visible_linear_data_columns() == [] + + def test_columns_are_shown_on_demand(self): + self.model.set_linear_data_from_xml([lsec_xml()]) + self.model.set_linear_data_visible(True) + assert self.model.columnCount() == len(TABLE_FULL) + 1 + assert self.model.headerData( + LINEAR_DATA_COLUMN, QtCore.Qt.Horizontal).value() == "Mole fraction of ozone (Linear)\n(ppmv)" + values = [self.model.data(self.model.index(row, LINEAR_DATA_COLUMN)).value() + for row in range(self.model.rowCount())] + assert values == ["", "10", "20", ""] + # The data columns cannot be edited. + assert not self.model.flags(self.model.index(0, LINEAR_DATA_COLUMN)) & QtCore.Qt.ItemIsEditable + + self.model.set_linear_data_visible(False) + assert self.model.columnCount() == len(TABLE_FULL) + + def test_data_stays_at_its_waypoint_when_waypoints_change(self): + self.model.set_linear_data_from_xml([lsec_xml()]) + self.model.set_linear_data_visible(True) + self.model.insertRows(0, waypoints=[Waypoint(10., 10., 0.)]) + values = [self.model.data(self.model.index(row, LINEAR_DATA_COLUMN)).value() + for row in range(self.model.rowCount())] + assert values == ["", "", "10", "20", ""] + + def test_clear_linear_data(self): + self.model.set_linear_data_from_xml([lsec_xml()]) + self.model.set_linear_data_visible(True) + self.model.clear_linear_data() + assert self.model.columnCount() == len(TABLE_FULL) + assert all(waypoint.linear_data == {} for waypoint in self.model.all_waypoint_data()) + + def test_linear_data_does_not_modify_the_flight_track(self): + changed = [] + self.model.modified = False + self.model.dataChanged.connect(lambda *args: changed.append(args)) + self.model.set_linear_data_visible(True) + self.model.set_linear_data_from_xml([lsec_xml()]) + assert changed == [] + assert not self.model.modified diff --git a/tests/utils.py b/tests/utils.py index a2948a2cb..adf1a785a 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -26,6 +26,7 @@ limitations under the License. """ import requests +import xml.etree.ElementTree as etree from urllib.parse import urljoin @@ -87,6 +88,23 @@ """ +def lsec_xml(title="Mole fraction of ozone (Linear)", unit="ppmv", + lats=(0., 0.5, 1., 1.5, 2., 2.5, 3.), + lons=(0., 0.5, 1., 1.5, 2., 2.5, 3.), + values=("nan", "5", "10", "12", "20", "22", "nan")): + """ + Build a linear section response as it is returned by the WMS server for a + "LINE:1" request, see mslib.mswms.mpl_lsec. + """ + return etree.fromstring( + "" + f"{title}" + f"{','.join(str(lon) for lon in lons)}" + f"{','.join(str(lat) for lat in lats)}" + f"{','.join(values)}" + "") + + def callback_ok_image(status, response_headers): assert status == "200 OK" assert response_headers[0] == ('Content-type', 'image/png') From 250df7ba1a57466d52f5c1236d49ced2f2a5e222 Mon Sep 17 00:00:00 2001 From: "J.Ironman" Date: Thu, 3 Sep 2026 07:51:09 +0000 Subject: [PATCH 2/9] linearview: hand the retrieved values to the flight track model Every plot of a linear section stores its values at the waypoints of the flight track, from where the table view can show them. Closing the linear view drops them again, as it is the source of these values. Co-Authored-By: Claude Opus 5 (1M context) --- mslib/msui/linearview.py | 9 +++++++++ mslib/msui/mpl_qtwidget.py | 4 ++++ tests/_test_msui/test_linearview.py | 29 +++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+) diff --git a/mslib/msui/linearview.py b/mslib/msui/linearview.py index f5b2cf2ef..068f8202d 100644 --- a/mslib/msui/linearview.py +++ b/mslib/msui/linearview.py @@ -129,6 +129,15 @@ def __init__(self, parent=None, mainwindow=None, model=None, _id=None, config_se def __del__(self): del self.mpl.canvas.waypoints_interactor + def closeEvent(self, event): + """ + Drop the data columns of the table view, this view is the source of + their values. + """ + super().closeEvent(event) + if event.isAccepted() and self.waypoints_model is not None: + self.waypoints_model.clear_linear_data() + def update_predefined_maps(self, extra): pass diff --git a/mslib/msui/mpl_qtwidget.py b/mslib/msui/mpl_qtwidget.py index e4c63ec64..20fe61251 100644 --- a/mslib/msui/mpl_qtwidget.py +++ b/mslib/msui/mpl_qtwidget.py @@ -714,6 +714,10 @@ def draw_legend(self, img): def draw_image(self, xmls, colors=None, scales=None): self.plotter.draw_image(xmls, colors, scales) self.redraw_xaxis() + # Hand the retrieved values over to the flight track model, so that + # other views (e.g. the table view) can show the data at a waypoint. + if self.waypoints_model is not None: + self.waypoints_model.set_linear_data_from_xml(xmls) def redraw_xaxis(self): """Redraw the x-axis of the linear view on path changes. diff --git a/tests/_test_msui/test_linearview.py b/tests/_test_msui/test_linearview.py index bd4dfee77..1bc7dd6a7 100644 --- a/tests/_test_msui/test_linearview.py +++ b/tests/_test_msui/test_linearview.py @@ -33,6 +33,7 @@ from mslib.msui.msui import MSUIMainWindow from mslib.msui.viewplotter import _DEFAULT_SETTINGS_LINEARVIEW from mslib.utils.config import config_loader +from tests.utils import lsec_xml WMS_REQUEST_TIMEOUT_MS = (config_loader(dataset="WMS_request_timeout") + 5) * 1000 @@ -77,6 +78,32 @@ def test_mouse_over(self): QtTest.QTest.mouseMove(self.window.mpl.canvas, QtCore.QPoint(782, 266), -1) QtTest.QTest.mouseMove(self.window.mpl.canvas, QtCore.QPoint(100, 100), -1) + def test_draw_image_fills_waypoint_data(self): + """ + The retrieved values are handed over to the flight track model, where + the table view picks them up. + """ + waypoints = self.window.waypoints_model.all_waypoint_data() + self.window.mpl.canvas.draw_image([lsec_xml( + lats=[wp.lat for wp in waypoints], lons=[wp.lon for wp in waypoints], + values=("nan", "42", "13"))]) + + assert self.window.waypoints_model.linear_data_columns == [("Mole fraction of ozone (Linear)", "ppmv")] + assert [wp.linear_data for wp in waypoints] == [ + {}, {"Mole fraction of ozone (Linear)": 42.}, {"Mole fraction of ozone (Linear)": 13.}] + + def test_close_clears_waypoint_data(self): + waypoints = self.window.waypoints_model.all_waypoint_data() + self.window.mpl.canvas.draw_image([lsec_xml( + lats=[wp.lat for wp in waypoints], lons=[wp.lon for wp in waypoints], + values=("nan", "42", "13"))]) + + self.window.force_close = True + self.window.close() + + assert self.window.waypoints_model.linear_data_columns == [] + assert [wp.linear_data for wp in waypoints] == [{}, {}, {}] + @mock.patch("mslib.msui.linearview.MSUI_LV_Options_Dialog") def test_options(self, mockdlg): QtTest.QTest.mouseClick(self.window.lvoptionbtn, QtCore.Qt.LeftButton) @@ -118,3 +145,5 @@ def test_server_getmap(self, qtbot): self.query_server(qtbot, self.url) with qtbot.wait_signal(self.wms_control.image_displayed, timeout=WMS_REQUEST_TIMEOUT_MS): QtTest.QTest.mouseClick(self.wms_control.btGetMap, QtCore.Qt.LeftButton) + # the retrieved data is offered as a column of the table view + assert len(self.window.waypoints_model.linear_data_columns) == 1 From bacba21ec346125695175e1a3987c1809ad6426b Mon Sep 17 00:00:00 2001 From: "J.Ironman" Date: Thu, 3 Sep 2026 07:56:49 +0000 Subject: [PATCH 3/9] tableview: "show data" checkbox for the values of the linear view The checkbox appends one column per layer plotted in the linear view of the same flight track, giving the exact value at each waypoint, e.g. the mole fraction of ozone. Waypoints for which the server has no data, e.g. because the aircraft is on the ground, stay empty. qt5/ui_tableview_window.py was regenerated with pyuic5 5.15.11. Co-Authored-By: Claude Opus 5 (1M context) --- mslib/msui/qt5/ui_tableview_window.py | 12 +++++++++--- mslib/msui/tableview.py | 21 +++++++++++++++++++++ mslib/msui/ui/ui_tableview_window.ui | 10 ++++++++++ tests/_test_msui/test_tableview.py | 27 +++++++++++++++++++++++++++ 4 files changed, 67 insertions(+), 3 deletions(-) diff --git a/mslib/msui/qt5/ui_tableview_window.py b/mslib/msui/qt5/ui_tableview_window.py index 6b22797e7..115ea918f 100644 --- a/mslib/msui/qt5/ui_tableview_window.py +++ b/mslib/msui/qt5/ui_tableview_window.py @@ -2,9 +2,10 @@ # Form implementation generated from reading ui file 'mslib/msui/ui/ui_tableview_window.ui' # -# Created by: PyQt5 UI code generator 5.12.3 +# Created by: PyQt5 UI code generator 5.15.11 # -# WARNING! All changes made in this file will be lost! +# WARNING: Any manual changes made to this file will be lost when pyuic5 is +# run again. Do not edit this file unless you know what you are doing. from PyQt5 import QtCore, QtGui, QtWidgets @@ -44,6 +45,9 @@ def setupUi(self, TableViewWindow): self.cbTools.setObjectName("cbTools") self.cbTools.addItem("") self.horizontalLayout.addWidget(self.cbTools) + self.cbShowLinearData = QtWidgets.QCheckBox(self.centralwidget) + self.cbShowLinearData.setObjectName("cbShowLinearData") + self.horizontalLayout.addWidget(self.cbShowLinearData) spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) self.horizontalLayout.addItem(spacerItem) self.label = QtWidgets.QLabel(self.centralwidget) @@ -72,13 +76,15 @@ def setupUi(self, TableViewWindow): TableViewWindow.addAction(self.actionCloseWindow) self.retranslateUi(TableViewWindow) - self.actionCloseWindow.triggered.connect(TableViewWindow.close) + self.actionCloseWindow.triggered.connect(TableViewWindow.close) # type: ignore QtCore.QMetaObject.connectSlotsByName(TableViewWindow) def retranslateUi(self, TableViewWindow): _translate = QtCore.QCoreApplication.translate TableViewWindow.setWindowTitle(_translate("TableViewWindow", "Table View - Mission Support System")) self.cbTools.setItemText(0, _translate("TableViewWindow", "(select to open control)")) + self.cbShowLinearData.setToolTip(_translate("TableViewWindow", "Show a column with the data values of the linear view at each waypoint")) + self.cbShowLinearData.setText(_translate("TableViewWindow", "show data")) self.label.setText(_translate("TableViewWindow", "Waypoints:")) self.btAddWayPointToFlightTrack.setText(_translate("TableViewWindow", "insert")) self.btCloneWaypoint.setText(_translate("TableViewWindow", "clone")) diff --git a/mslib/msui/tableview.py b/mslib/msui/tableview.py index 199b453b3..6f2d02304 100644 --- a/mslib/msui/tableview.py +++ b/mslib/msui/tableview.py @@ -84,6 +84,7 @@ def __init__(self, parent=None, model=None, _id=None, tutorial_mode=False): self.btDeleteWayPoint.clicked.connect(self.removeWayPoint) self.btInvertDirection.clicked.connect(self.invertDirection) self.btRoundtrip.clicked.connect(self.make_roundtrip) + self.cbShowLinearData.toggled.connect(self.setLinearDataVisible) self.tableWayPoints.selectionModel().selectionChanged.connect(self.on_selection_changed) # Tool opener. @@ -101,6 +102,22 @@ def setPerformance(self, settings): self.resizeColumns() self.tableWayPoints.viewport().repaint() + def setLinearDataVisible(self, visible): + """ + Handler for the checkbox. Shows or hides the + columns with the data values of the linear view. + """ + self.waypoints_model.set_linear_data_visible(visible) + + def update_linear_data(self): + """ + Slot called when the data retrieved by the linear view has changed. + The values are not part of the flight plan, hence the model does not + emit dataChanged() for them and the table has to be repainted here. + """ + self.tableWayPoints.viewport().update() + self.resizeColumns() + def on_selection_changed(self, index): """ Disables insert and clone when multiple rows are selected @@ -280,6 +297,10 @@ def setFlightTrackModel(self, model): self.waypoints_model.dataChanged.connect(self.update_roundtrip_enabled) self.update_roundtrip_enabled() + # Show the data of the linear view as requested for this flight track + self.waypoints_model.linearDataChanged.connect(self.update_linear_data) + self.cbShowLinearData.setChecked(self.waypoints_model.linear_data_visible) + def viewPerformance(self): """ Slot to toggle the view mode of the table between 'USER' and diff --git a/mslib/msui/ui/ui_tableview_window.ui b/mslib/msui/ui/ui_tableview_window.ui index 4c0ddda98..0a84f10cd 100644 --- a/mslib/msui/ui/ui_tableview_window.ui +++ b/mslib/msui/ui/ui_tableview_window.ui @@ -72,6 +72,16 @@ + + + + Show a column with the data values of the linear view at each waypoint + + + show data + + + diff --git a/tests/_test_msui/test_tableview.py b/tests/_test_msui/test_tableview.py index fbf779b85..83cccafd9 100644 --- a/tests/_test_msui/test_tableview.py +++ b/tests/_test_msui/test_tableview.py @@ -33,6 +33,7 @@ from mslib.msui import flighttrack as ft from mslib.msui.performance_settings import DEFAULT_PERFORMANCE import mslib.msui.tableview as tv +from tests.utils import lsec_xml class Test_TableView: @@ -159,6 +160,32 @@ def test_performance(self, mockopen): QtTest.QTest.mouseClick(self.window.docks[1].widget().pbLoadPerformance, QtCore.Qt.LeftButton) assert mockopen.call_count == 1 + def test_show_linear_data(self): + """ + The "show data" checkbox appends the data columns of the linear view. + """ + model = self.window.waypoints_model + waypoints = model.all_waypoint_data() + model.set_linear_data_from_xml([lsec_xml( + lats=[wp.lat for wp in waypoints], lons=[wp.lon for wp in waypoints], + values=("nan", "0.05", "0.06", "0.07", "nan"))]) + # the data is hidden as long as the checkbox is unchecked + assert model.columnCount() == 15 + + QtTest.QTest.mouseClick(self.window.cbShowLinearData, QtCore.Qt.LeftButton) + assert self.window.cbShowLinearData.isChecked() + assert model.columnCount() == 16 + column = ft.LINEAR_DATA_COLUMN + assert model.headerData( + column, QtCore.Qt.Horizontal).value() == "Mole fraction of ozone (Linear)\n(ppmv)" + # no values where the aircraft is on the ground + assert [model.data(model.index(row, column)).value() for row in range(model.rowCount())] == \ + ["", "0.05", "0.06", "0.07", ""] + + QtTest.QTest.mouseClick(self.window.cbShowLinearData, QtCore.Qt.LeftButton) + assert not self.window.cbShowLinearData.isChecked() + assert model.columnCount() == 15 + def test_insert_point(self): """ Check insertion of points From 2d4a3a97c31d16839691cf8cee336625b408b943 Mon Sep 17 00:00:00 2001 From: "J.Ironman" Date: Thu, 3 Sep 2026 07:57:52 +0000 Subject: [PATCH 4/9] docs: describe the data columns of the table view Co-Authored-By: Claude Opus 5 (1M context) --- docs/usage.rst | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/usage.rst b/docs/usage.rst index ae9b79dd4..43f8bbc2c 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -131,6 +131,19 @@ you can use these parameters. If you use shared directories you may have to solv .. literalinclude:: samples/config/msui/snippets/caching.sample +Data values in Table View +------------------------- + +The Linear View shows the values of a data field along the flight path, as retrieved +from a WMS server. Enabling the *show data* checkbox of the Table View appends one +column per layer plotted in the Linear View of the same flight track, giving the exact +value at each waypoint. Waypoints for which the server provides no data, e.g. because +the aircraft is on the ground, stay empty. + +The columns are refreshed with every plot of the Linear View and are removed when the +Linear View is closed. + + Docking Widgets Configurations ------------------------------ From c9ba364c28f564cc1ea2d0ee4e67bd08bf209622 Mon Sep 17 00:00:00 2001 From: "J.Ironman" Date: Fri, 4 Sep 2026 07:18:04 +0000 Subject: [PATCH 5/9] tableview: keep the "show data" checkbox on a flight track switch Activating another flight track hid the data columns of the linear view, even though the linear view had already stored its values for the new track: setFlightTrackModel() pushed the visibility flag of the new flight track into the checkbox, and that flag is off for a track that has not shown the columns yet. The flag has to live on the flight track, as the number of columns depends on it, but the checkbox is the wish of the user for this window. It therefore keeps its state now and is applied to the newly displayed flight track. Only the flight track the window starts with is adopted the other way around, so that a second table view of the same track does not switch its columns off. The change signals of the flight track that is not displayed any more are disconnected as well, they piled up with every switch. Co-Authored-By: Claude Opus 5 (1M context) --- mslib/msui/tableview.py | 23 +++++++++++++-- tests/_test_msui/test_tableview.py | 45 ++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/mslib/msui/tableview.py b/mslib/msui/tableview.py index 6f2d02304..bdb06accd 100644 --- a/mslib/msui/tableview.py +++ b/mslib/msui/tableview.py @@ -290,16 +290,35 @@ def setFlightTrackModel(self, model): """ Set the QAbstractItemModel instance that the table displays. """ + previous = self.waypoints_model super().setFlightTrackModel(model) + if previous is not None and previous is not self.waypoints_model: + # Stop listening to the flight track that is not displayed here + # any more, otherwise the connections pile up on every switch. + for signal, slot in ((previous.dataChanged, self.update_roundtrip_enabled), + (previous.linearDataChanged, self.update_linear_data)): + try: + signal.disconnect(slot) + except (TypeError, RuntimeError): + pass self.tableWayPoints.setModel(self.waypoints_model) # Automatically enable or disable roundtrip when data changes self.waypoints_model.dataChanged.connect(self.update_roundtrip_enabled) self.update_roundtrip_enabled() - # Show the data of the linear view as requested for this flight track self.waypoints_model.linearDataChanged.connect(self.update_linear_data) - self.cbShowLinearData.setChecked(self.waypoints_model.linear_data_visible) + if previous is None or previous is self.waypoints_model: + # Adopt what the flight track already shows, another table view + # of the same track may have switched the data columns on. + self.cbShowLinearData.setChecked(self.waypoints_model.linear_data_visible) + else: + # The checkbox is the wish of the user for this window, hence it + # keeps its state when another flight track becomes the active + # one and applies to the data of that track, which the linear + # view stores per flight track. + self.waypoints_model.set_linear_data_visible(self.cbShowLinearData.isChecked()) + self.resizeColumns() def viewPerformance(self): """ diff --git a/tests/_test_msui/test_tableview.py b/tests/_test_msui/test_tableview.py index 83cccafd9..2e3a7dbbd 100644 --- a/tests/_test_msui/test_tableview.py +++ b/tests/_test_msui/test_tableview.py @@ -186,6 +186,51 @@ def test_show_linear_data(self): assert not self.window.cbShowLinearData.isChecked() assert model.columnCount() == 15 + def test_show_linear_data_of_another_flight_track(self): + """ + Switching to another flight track keeps the "show data" checkbox and + shows the data columns that the linear view retrieved for that track. + """ + model = self.window.waypoints_model + waypoints = model.all_waypoint_data() + model.set_linear_data_from_xml([lsec_xml( + lats=[wp.lat for wp in waypoints], lons=[wp.lon for wp in waypoints], + values=("nan", "0.05", "0.06", "0.07", "nan"))]) + QtTest.QTest.mouseClick(self.window.cbShowLinearData, QtCore.Qt.LeftButton) + assert model.columnCount() == 16 + + # the linear view of the other flight track plots two layers + other = ft.WaypointsTableModel("other") + other.insertRows(0, rows=3, waypoints=[ft.Waypoint(48.10, 10.27, 200), + ft.Waypoint(52.32, 9.21, 200), + ft.Waypoint(52.55, 9.99, 200)]) + other_waypoints = other.all_waypoint_data() + other.set_linear_data_from_xml([ + lsec_xml(title=title, unit="ppmv", + lats=[wp.lat for wp in other_waypoints], + lons=[wp.lon for wp in other_waypoints], + values=values) + for title, values in (("Ozone", ("0.1", "0.2", "0.3")), + ("Water vapour", ("1", "2", "nan")))]) + + self.window.setFlightTrackModel(other) + assert self.window.cbShowLinearData.isChecked() + assert other.columnCount() == 17 + column = ft.LINEAR_DATA_COLUMN + assert [other.headerData(column + i, QtCore.Qt.Horizontal).value() for i in range(2)] == \ + ["Ozone\n(ppmv)", "Water vapour\n(ppmv)"] + assert [other.data(other.index(row, column)).value() for row in range(other.rowCount())] == \ + ["0.1", "0.2", "0.3"] + assert [other.data(other.index(row, column + 1)).value() for row in range(other.rowCount())] == \ + ["1", "2", ""] + + # the data of the other flight track is hidden again on demand + QtTest.QTest.mouseClick(self.window.cbShowLinearData, QtCore.Qt.LeftButton) + assert other.columnCount() == 15 + + # ... and the track shown before does not notify this window any more + assert model.receivers(model.linearDataChanged) == 0 + def test_insert_point(self): """ Check insertion of points From 2e9485447b18a1013bae2ed7bd130d71ce43c4c4 Mon Sep 17 00:00:00 2001 From: "J.Ironman" Date: Fri, 4 Sep 2026 07:18:04 +0000 Subject: [PATCH 6/9] docs: note that the data columns follow the active flight track Co-Authored-By: Claude Opus 5 (1M context) --- docs/usage.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/usage.rst b/docs/usage.rst index 43f8bbc2c..8c8e397ff 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -141,7 +141,8 @@ value at each waypoint. Waypoints for which the server provides no data, e.g. be the aircraft is on the ground, stay empty. The columns are refreshed with every plot of the Linear View and are removed when the -Linear View is closed. +Linear View is closed. The checkbox keeps its state when you activate another flight +track, the columns then show the data of the Linear View of that flight track. Docking Widgets Configurations From 12d0c9ce896f39192763f0447c1f2c172de30e0b Mon Sep 17 00:00:00 2001 From: "J.Ironman" Date: Fri, 4 Sep 2026 10:59:41 +0000 Subject: [PATCH 7/9] tableview: grey the "show data" checkbox out without data The checkbox has no effect as long as no linear view provides data values for the active flight track, so disable and untick it in that case. The wish of the user is kept in show_linear_data_wanted, hence the columns come back on their own once the linear view has data for the flight track, which it retrieves asynchronously after a track switch. Co-Authored-By: Claude Opus 5 (1M context) --- mslib/msui/tableview.py | 48 ++++++++++-- tests/_test_msui/test_tableview.py | 121 +++++++++++++++++++++++++++++ 2 files changed, 163 insertions(+), 6 deletions(-) diff --git a/mslib/msui/tableview.py b/mslib/msui/tableview.py index bdb06accd..abf9fa154 100644 --- a/mslib/msui/tableview.py +++ b/mslib/msui/tableview.py @@ -66,6 +66,17 @@ def __init__(self, parent=None, model=None, _id=None, tutorial_mode=False): self.setWindowIcon(QtGui.QIcon(icons('64x64'))) self.settings_tag = "tableview" + # Tooltip of while it is usable, a different one + # explains the greyed out checkbox. + self.show_linear_data_tooltip = self.cbShowLinearData.toolTip() + # Whether the user wants the data columns. The checkbox itself only + # shows them where a linear view provides data, this remembers the + # wish across flight tracks that have none. + self.show_linear_data_wanted = False + # Guards the checkbox against taking a state set by this window for + # the wish of the user. + self._updating_show_linear_data = False + self.setFlightTrackModel(model) self.tableWayPoints.setItemDelegate(ft.WaypointDelegate(self)) @@ -107,6 +118,8 @@ def setLinearDataVisible(self, visible): Handler for the checkbox. Shows or hides the columns with the data values of the linear view. """ + if not self._updating_show_linear_data: + self.show_linear_data_wanted = visible self.waypoints_model.set_linear_data_visible(visible) def update_linear_data(self): @@ -115,9 +128,31 @@ def update_linear_data(self): The values are not part of the flight plan, hence the model does not emit dataChanged() for them and the table has to be repainted here. """ + self.update_show_linear_data_enabled() self.tableWayPoints.viewport().update() self.resizeColumns() + def update_show_linear_data_enabled(self): + """ + Match the checkbox to the data of the active flight + track. As long as no linear view provides data for it there is nothing + to show, hence the checkbox is unchecked and greyed out; a tick without + data columns would be misleading. The wish of the user is remembered, + so the columns of a flight track reappear as soon as the linear view + has data for it, e.g. after switching back and forth between tracks. + """ + available = len(self.waypoints_model.linear_data_columns) > 0 + # The checkbox passes the change on to the flight track (toggled). + self._updating_show_linear_data = True + try: + self.cbShowLinearData.setChecked(available and self.show_linear_data_wanted) + finally: + self._updating_show_linear_data = False + self.cbShowLinearData.setEnabled(available) + self.cbShowLinearData.setToolTip( + self.show_linear_data_tooltip if available else + "No linear view provides data values for this flight track") + def on_selection_changed(self, index): """ Disables insert and clone when multiple rows are selected @@ -311,13 +346,14 @@ def setFlightTrackModel(self, model): if previous is None or previous is self.waypoints_model: # Adopt what the flight track already shows, another table view # of the same track may have switched the data columns on. - self.cbShowLinearData.setChecked(self.waypoints_model.linear_data_visible) + self.show_linear_data_wanted = self.waypoints_model.linear_data_visible else: - # The checkbox is the wish of the user for this window, hence it - # keeps its state when another flight track becomes the active - # one and applies to the data of that track, which the linear - # view stores per flight track. - self.waypoints_model.set_linear_data_visible(self.cbShowLinearData.isChecked()) + # The wish of the user applies to the flight track that becomes + # the active one, its data is what the linear view retrieved for + # that track. The checkbox follows below, ticked only if there is + # data to show. + self.waypoints_model.set_linear_data_visible(self.show_linear_data_wanted) + self.update_show_linear_data_enabled() self.resizeColumns() def viewPerformance(self): diff --git a/tests/_test_msui/test_tableview.py b/tests/_test_msui/test_tableview.py index 2e3a7dbbd..1ed62f45b 100644 --- a/tests/_test_msui/test_tableview.py +++ b/tests/_test_msui/test_tableview.py @@ -231,6 +231,127 @@ def test_show_linear_data_of_another_flight_track(self): # ... and the track shown before does not notify this window any more assert model.receivers(model.linearDataChanged) == 0 + def test_show_linear_data_without_linear_view(self): + """ + The "show data" checkbox is unchecked and greyed out as long as no + linear view provides data. + """ + model = self.window.waypoints_model + waypoints = model.all_waypoint_data() + # no linear view has plotted this flight track yet + assert not self.window.cbShowLinearData.isEnabled() + assert not self.window.cbShowLinearData.isChecked() + + data = [lsec_xml(lats=[wp.lat for wp in waypoints], lons=[wp.lon for wp in waypoints], + values=("nan", "0.05", "0.06", "0.07", "nan"))] + model.set_linear_data_from_xml(data) + assert self.window.cbShowLinearData.isEnabled() + QtTest.QTest.mouseClick(self.window.cbShowLinearData, QtCore.Qt.LeftButton) + assert model.columnCount() == 16 + + # closing the linear view drops the data columns and the tick + model.clear_linear_data() + assert not self.window.cbShowLinearData.isEnabled() + assert not self.window.cbShowLinearData.isChecked() + assert not model.linear_data_visible + assert model.columnCount() == 15 + + # ... the tick and the columns are back with the next plot + model.set_linear_data_from_xml(data) + assert self.window.cbShowLinearData.isEnabled() + assert self.window.cbShowLinearData.isChecked() + assert model.columnCount() == 16 + + def test_show_linear_data_disabled_for_flight_track_without_data(self): + """ + Switching to a flight track that no linear view plots unchecks and + greys the "show data" checkbox out. + """ + model = self.window.waypoints_model + waypoints = model.all_waypoint_data() + model.set_linear_data_from_xml([lsec_xml( + lats=[wp.lat for wp in waypoints], lons=[wp.lon for wp in waypoints], + values=("nan", "0.05", "0.06", "0.07", "nan"))]) + QtTest.QTest.mouseClick(self.window.cbShowLinearData, QtCore.Qt.LeftButton) + assert self.window.cbShowLinearData.isEnabled() + + other = ft.WaypointsTableModel("other") + other.insertRows(0, rows=2, waypoints=[ft.Waypoint(48.10, 10.27, 200), + ft.Waypoint(52.32, 9.21, 200)]) + self.window.setFlightTrackModel(other) + assert not self.window.cbShowLinearData.isEnabled() + assert not self.window.cbShowLinearData.isChecked() + assert not other.linear_data_visible + assert other.columnCount() == 15 + + def test_show_linear_data_of_flight_tracks_switched_back_and_forth(self): + """ + The data columns follow the active flight track: they are shown as + soon as the linear view has retrieved the values of that track, also + when switching back and forth between flight tracks. + """ + model = self.window.waypoints_model + waypoints = model.all_waypoint_data() + data = [lsec_xml(lats=[wp.lat for wp in waypoints], lons=[wp.lon for wp in waypoints], + values=("nan", "0.05", "0.06", "0.07", "nan"))] + model.set_linear_data_from_xml(data) + QtTest.QTest.mouseClick(self.window.cbShowLinearData, QtCore.Qt.LeftButton) + assert model.columnCount() == 16 + + other = ft.WaypointsTableModel("other") + other.insertRows(0, rows=2, waypoints=[ft.Waypoint(48.10, 10.27, 200), + ft.Waypoint(52.32, 9.21, 200)]) + other_waypoints = other.all_waypoint_data() + other_data = [lsec_xml(lats=[wp.lat for wp in other_waypoints], + lons=[wp.lon for wp in other_waypoints], values=("0.1", "0.2"))] + # the linear view drops the values of the track it does not plot any more + model.clear_linear_data() + self.window.setFlightTrackModel(other) + assert not self.window.cbShowLinearData.isEnabled() + assert not self.window.cbShowLinearData.isChecked() + + # the plot of the new flight track arrives and its data is shown + other.set_linear_data_from_xml(other_data) + assert self.window.cbShowLinearData.isEnabled() + assert self.window.cbShowLinearData.isChecked() + assert other.columnCount() == 16 + assert [other.data(other.index(row, ft.LINEAR_DATA_COLUMN)).value() + for row in range(other.rowCount())] == ["0.1", "0.2"] + + # ... and the same when switching back to the first flight track + other.clear_linear_data() + self.window.setFlightTrackModel(model) + assert not self.window.cbShowLinearData.isChecked() + model.set_linear_data_from_xml(data) + assert self.window.cbShowLinearData.isChecked() + assert model.columnCount() == 16 + assert [model.data(model.index(row, ft.LINEAR_DATA_COLUMN)).value() + for row in range(model.rowCount())] == ["", "0.05", "0.06", "0.07", ""] + + def test_show_linear_data_not_wanted_stays_hidden_on_switch(self): + """ + Data columns that the user has switched off stay off when the linear + view provides data for another flight track. + """ + model = self.window.waypoints_model + waypoints = model.all_waypoint_data() + model.set_linear_data_from_xml([lsec_xml( + lats=[wp.lat for wp in waypoints], lons=[wp.lon for wp in waypoints], + values=("nan", "0.05", "0.06", "0.07", "nan"))]) + assert not self.window.cbShowLinearData.isChecked() + + other = ft.WaypointsTableModel("other") + other.insertRows(0, rows=2, waypoints=[ft.Waypoint(48.10, 10.27, 200), + ft.Waypoint(52.32, 9.21, 200)]) + other_waypoints = other.all_waypoint_data() + self.window.setFlightTrackModel(other) + other.set_linear_data_from_xml([lsec_xml( + lats=[wp.lat for wp in other_waypoints], lons=[wp.lon for wp in other_waypoints], + values=("0.1", "0.2"))]) + assert self.window.cbShowLinearData.isEnabled() + assert not self.window.cbShowLinearData.isChecked() + assert other.columnCount() == 15 + def test_insert_point(self): """ Check insertion of points From c0fe477b43214926fe29711609b915299822858f Mon Sep 17 00:00:00 2001 From: "J.Ironman" Date: Fri, 4 Sep 2026 10:59:41 +0000 Subject: [PATCH 8/9] linearview: refresh the data columns on a flight track switch The values of the flight track that is not plotted here any more were left on its model, so the table view showed them as if they belonged to the current plot -- even after this view was closed, as clearing on close only reaches the active flight track. Drop them on the switch instead. The WMS control also has to know the new flight track before the redraw in the base class asks it for a plot: that request is built from its waypoints model and described the previous track so far. Co-Authored-By: Claude Opus 5 (1M context) --- mslib/msui/linearview.py | 11 ++++++++++- tests/_test_msui/test_linearview.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/mslib/msui/linearview.py b/mslib/msui/linearview.py index 068f8202d..10adb5244 100644 --- a/mslib/msui/linearview.py +++ b/mslib/msui/linearview.py @@ -229,9 +229,18 @@ def setFlightTrackModel(self, model): """ Set the QAbstractItemModel instance that the view displays. """ - super().setFlightTrackModel(model) + previous = self.waypoints_model if self.docks[WMS] is not None: + # The WMS control has to know the new flight track before the + # redraw below asks it for a plot, its request describes the path. self.docks[WMS].widget().setFlightTrackModel(model) + super().setFlightTrackModel(model) + if previous is not None and previous is not model: + # The data values of the flight track that is not plotted here any + # more are outdated, the table view must not show them as if they + # belonged to the current plot. The values of the new flight track + # arrive with the plot that the redraw above requests. + previous.clear_linear_data() def open_settings_dialog(self): settings = self.getView().get_settings() diff --git a/tests/_test_msui/test_linearview.py b/tests/_test_msui/test_linearview.py index 1bc7dd6a7..7fd82d758 100644 --- a/tests/_test_msui/test_linearview.py +++ b/tests/_test_msui/test_linearview.py @@ -104,6 +104,35 @@ def test_close_clears_waypoint_data(self): assert self.window.waypoints_model.linear_data_columns == [] assert [wp.linear_data for wp in waypoints] == [{}, {}, {}] + def test_switch_flight_track_updates_data(self): + """ + On a switch to another flight track the values of the track that is + not plotted here any more are dropped, and the plot of the new track + is requested for its waypoints. + """ + waypoints = self.window.waypoints_model.all_waypoint_data() + self.window.mpl.canvas.draw_image([lsec_xml( + lats=[wp.lat for wp in waypoints], lons=[wp.lon for wp in waypoints], + values=("nan", "42", "13"))]) + previous = self.window.waypoints_model + + other = ft.WaypointsTableModel("other") + other.insertRows(0, rows=2, waypoints=[ft.Waypoint(48.10, 10.27, 200), + ft.Waypoint(52.32, 9.21, 200)]) + + wms_control = self.window.docks[tv.WMS].widget() + requested = [] + self.window.mpl.canvas.waypoints_interactor.signal_get_lsec.connect( + lambda: requested.append(wms_control.waypoints_model)) + + self.window.setFlightTrackModel(other) + + # the WMS control knows the new flight track when it is asked for a plot + assert requested == [other] + # the values of the previous flight track are outdated and gone + assert previous.linear_data_columns == [] + assert [wp.linear_data for wp in waypoints] == [{}, {}, {}] + @mock.patch("mslib.msui.linearview.MSUI_LV_Options_Dialog") def test_options(self, mockdlg): QtTest.QTest.mouseClick(self.window.lvoptionbtn, QtCore.Qt.LeftButton) From 563205b38c34a7f22144873082c1301183b00f4b Mon Sep 17 00:00:00 2001 From: "J.Ironman" Date: Fri, 4 Sep 2026 10:59:59 +0000 Subject: [PATCH 9/9] docs: note when the "show data" checkbox is available Co-Authored-By: Claude Opus 5 (1M context) --- docs/usage.rst | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/usage.rst b/docs/usage.rst index 8c8e397ff..af17547f9 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -141,8 +141,16 @@ value at each waypoint. Waypoints for which the server provides no data, e.g. be the aircraft is on the ground, stay empty. The columns are refreshed with every plot of the Linear View and are removed when the -Linear View is closed. The checkbox keeps its state when you activate another flight -track, the columns then show the data of the Linear View of that flight track. +Linear View is closed. + +As long as no Linear View provides data for the active flight track there is nothing to +show, hence the checkbox is unchecked and greyed out. Activating another flight track +therefore empties the columns first: the values the Linear View retrieved for the flight +track it does not plot any more are dropped, they would be outdated. The columns are +filled again with the plot of the newly activated flight track, which the Linear View +requests right away as long as its *auto update* is enabled. The checkbox remembers your +wish while it is greyed out, so the columns come back on their own, also when you switch +back and forth between flight tracks. Docking Widgets Configurations