Skip to content
Open
22 changes: 22 additions & 0 deletions docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,28 @@ 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.

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
------------------------------

Expand Down
207 changes: 205 additions & 2 deletions mslib/msui/flighttrack.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@

import datetime
import logging
import math
import os
from pathlib import Path

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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"),
Expand All @@ -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()
Expand Down Expand Up @@ -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 |
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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 <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):
"""
Expand Down
20 changes: 19 additions & 1 deletion mslib/msui/linearview.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -220,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()
Expand Down
4 changes: 4 additions & 0 deletions mslib/msui/mpl_qtwidget.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
12 changes: 9 additions & 3 deletions mslib/msui/qt5/ui_tableview_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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"))
Expand Down
Loading
Loading