class MSSViewWindow(QtWidgets.QMainWindow): """Derives QMainWindow to provide some common functionality to all MSUI view windows. """ name = "Abstract MSS View Window" identifier = None viewCloses = QtCore.pyqtSignal(name="viewCloses") # views for mscolab viewClosesId = QtCore.Signal(int, name="viewClosesId") def __init__(self, parent=None, model=None, _id=None): super(MSSViewWindow, self).__init__(parent) # Object variables: self.waypoints_model = model # pointer to the current flight track. # List that accommodates the dock window instances: Needs to be defined # in proper size in derived classes! self.docks = [] # emit _id if not none logging.debug(_id) self._id = _id # Used to force close window without the dialog popping up self.force_close = False def handle_force_close(self): self.force_close = True self.close() def closeEvent(self, event): """ if force_close is True then close window without dialog else ask user if he/she wants to close the window. Overloads QtGui.QMainWindow.closeEvent(). This method is called if Qt receives a window close request for our application window. """ if self.force_close is True: event.accept() return ret = QtWidgets.QMessageBox.warning( self, self.tr("Mission Support System"), self.tr("Do you want to close this {}?".format(self.name)), QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No) if ret == QtWidgets.QMessageBox.Yes: self.viewCloses.emit() if self._id is not None: self.viewClosesId.emit(self._id) logging.debug(self._id) event.accept() else: event.ignore() def setFlightTrackModel(self, model): """Set the QAbstractItemModel instance that the view displays. """ self.waypoints_model = model def controlToBeCreated(self, index): """Check if the dock widget at index <index> exists. If yes, show the widget and return -1. Otherwise return <index-1>. """ index -= 1 if index >= 0 and self.docks[index] is not None: # The widget has already been created, but is not visible at # the moment. self.docks[index].show() self.docks[index].raise_() index = -1 if hasattr(self, "cbTools"): self.cbTools.setCurrentIndex(0) return index def createDockWidget(self, index, title, widget): """Create a new dock widget. A pointer to the dock widget will be stored in self.docks[index]. The dock will have the title <title> and contain the Qt widget <widget>. """ self.docks[index] = QtWidgets.QDockWidget(title, self) self.docks[index].setAllowedAreas(QtCore.Qt.AllDockWidgetAreas) # setWidget transfers the widget's ownership to Qt -- no setParent() # call is necessary: self.docks[index].setWidget(widget) self.addDockWidget(QtCore.Qt.BottomDockWidgetArea, self.docks[index]) # Check if another dock widget occupies the dock area. If yes, # tabbify the old and the new widget. for dock in self.docks: if dock and not dock == self.docks[index] and not dock.isFloating( ): self.tabifyDockWidget(dock, self.docks[index]) break self.docks[index].show() self.docks[index].raise_() @abstractmethod def getView(self): """Return view object that tools can interact with. ABSTRACT method, needs to be implemented in derived classes. """ return None def setIdentifier(self, identifier): self.identifier = identifier
class ConnectionManager(QtCore.QObject): signal_reload = QtCore.Signal(int, name="reload_wps") signal_message_receive = QtCore.Signal(str, name="message rcv") signal_message_reply_receive = QtCore.Signal(str, name="message reply") signal_message_edited = QtCore.Signal(str, name="message editted") signal_message_deleted = QtCore.Signal(str, name="message deleted") signal_new_permission = QtCore.Signal(int, int, name="new permission") signal_update_permission = QtCore.Signal(int, int, str, name="update permission") signal_revoke_permission = QtCore.Signal(int, int, name="revoke permission") signal_project_permissions_updated = QtCore.Signal( int, name="project permissions updated") signal_project_deleted = QtCore.Signal(int, name="project deleted") def __init__(self, token, user, mscolab_server_url=mss_default.mscolab_server_url): super(ConnectionManager, self).__init__() self.token = token self.user = user self.mscolab_server_url = mscolab_server_url self.sio = socketio.Client(reconnection_attempts=5) self.sio.connect(self.mscolab_server_url) self.sio.on('file-changed', handler=self.handle_file_change) # on chat message recive self.sio.on('chat-message-client', handler=self.handle_incoming_message) self.sio.on('chat-message-reply-client', handler=self.handle_incoming_message_reply) # on message edit self.sio.on('edit-message-client', handler=self.handle_message_edited) # on message delete self.sio.on('delete-message-client', handler=self.handle_message_deleted) # on new permission self.sio.on('new-permission', handler=self.handle_new_permission) # on update of permission self.sio.on('update-permission', handler=self.handle_update_permission) # on revoking project permission self.sio.on('revoke-permission', handler=self.handle_revoke_permission) # on updating project permissions in admin window self.sio.on('project-permissions-updated', handler=self.handle_project_permissions_updated) # On Project Delete self.sio.on('project-deleted', handler=self.handle_project_deleted) self.sio.emit('start', {'token': token}) def handle_update_permission(self, message): """ signal update of permission affected """ message = json.loads(message) p_id = int(message["p_id"]) u_id = int(message["u_id"]) access_level = message["access_level"] self.signal_update_permission.emit(p_id, u_id, access_level) def handle_new_permission(self, message): """ signal updating of newly added permission """ message = json.loads(message) p_id = int(message["p_id"]) u_id = int(message["u_id"]) self.signal_new_permission.emit(p_id, u_id) def handle_revoke_permission(self, message): """ Signal update of revoked permission """ message = json.loads(message) p_id = int(message["p_id"]) u_id = int(message["u_id"]) self.signal_revoke_permission.emit(p_id, u_id) def handle_project_permissions_updated(self, message): message = json.loads(message) u_id = int(message["u_id"]) self.signal_project_permissions_updated.emit(u_id) def handle_incoming_message(self, message): # raise signal to render to view logging.debug(message) # emit signal self.signal_message_receive.emit(message) def handle_incoming_message_reply(self, message): self.signal_message_reply_receive.emit(message) def handle_message_edited(self, message): self.signal_message_edited.emit(message) def handle_message_deleted(self, message): self.signal_message_deleted.emit(message) def handle_file_change(self, message): message = json.loads(message) self.signal_reload.emit(message["p_id"]) def handle_project_deleted(self, message): p_id = int(json.loads(message)["p_id"]) self.signal_project_deleted.emit(p_id) def handle_new_room(self, p_id): logging.debug("adding user to new room") self.sio.emit('add-user-to-room', {"p_id": p_id, "token": self.token}) def send_message(self, message_text, p_id, reply_id): logging.debug("sending message") self.sio.emit( 'chat-message', { "p_id": p_id, "token": self.token, "message_text": message_text, "reply_id": reply_id }) def edit_message(self, message_id, new_message_text, p_id): self.sio.emit( 'edit-message', { "message_id": message_id, "new_message_text": new_message_text, "p_id": p_id, "token": self.token }) def delete_message(self, message_id, p_id): self.sio.emit('delete-message', { 'message_id': message_id, 'p_id': p_id, 'token': self.token }) def save_file(self, token, p_id, content, comment=None): logging.debug("saving file") self.sio.emit( 'file-save', { "p_id": p_id, "token": self.token, "content": content, "comment": comment }) def disconnect(self): self.sio.disconnect()
class VPathInteractor(PathInteractor): """Subclass of PathInteractor that implements an interactively editable vertical profile of the flight track. """ signal_get_vsec = QtCore.Signal(name="get_vsec") def __init__(self, ax, waypoints, redraw_xaxis=None, clear_figure=None, numintpoints=101): """Constructor passes a PathV instance its parent. Arguments: ax -- matplotlib.Axes object into which the path should be drawn. waypoints -- flighttrack.WaypointsModel instance. numintpoints -- number of intermediate interpolation points. The entire flight track will be interpolated to this number of points. redrawXAxis -- callback function to redraw the x-axis on path changes. """ self.numintpoints = numintpoints self.redraw_xaxis = redraw_xaxis self.clear_figure = clear_figure super(VPathInteractor, self).__init__( ax=ax, waypoints=waypoints, mplpath=PathV([[0, 0]], numintpoints=numintpoints)) def get_num_interpolation_points(self): return self.numintpoints def redraw_figure(self): """For the side view, changes in the horizontal position of a waypoint (including moved waypoints, new or deleted waypoints) make a complete redraw of the figure necessary. Calls the callback function 'redrawXAxis()'. """ self.redraw_path() # emit signal to redraw map self.signal_get_vsec.emit() if self.clear_figure() is not None: self.clear_figure() if self.redraw_xaxis is not None: self.redraw_xaxis(self.path.ilats, self.path.ilons, self.path.itimes) self.ax.figure.canvas.draw() def button_release_delete_callback(self, event): """Called whenever a mouse button is released. """ if not self.showverts or event.button != 1: return if self._ind is not None: if self.confirm_delete_waypoint(self._ind): # removeRows() will trigger a signal that will redraw the path. self.waypoints_model.removeRows(self._ind) self._ind = None def button_release_insert_callback(self, event): """Called whenever a mouse button is released. From the click event's coordinates, best_index is calculated as the index of a vertex whose x coordinate > clicked x coordinate. This is the position where the waypoint is to be inserted. 'lat' and 'lon' are calculated as an average of each of the first waypoint in left and right neighbourhood of inserted waypoint. The coordinates are checked against "locations" defined in mss' config. A new waypoint with the coordinates, and name is inserted into the waypoints_model. """ if not self.showverts or event.button != 1 or event.inaxes is None: return y = event.ydata wpm = self.waypoints_model flightlevel = float(pressure2flightlevel(y)) [lat, lon], best_index = self.get_lat_lon(event) loc = find_location(lat, lon) # skipped tolerance which uses appropriate_epsilon_km if loc is not None: (lat, lon), location = loc else: location = "" new_wp = ft.Waypoint(lat, lon, flightlevel, location=location) wpm.insertRows(best_index, rows=1, waypoints=[new_wp]) self.redraw_figure() self._ind = None def get_lat_lon(self, event): x = event.xdata wpm = self.waypoints_model vertices = self.pathpatch.get_path().vertices vertices = np.ndarray.tolist(vertices) for index, vertex in enumerate(vertices): vertices[index].append(datetime.datetime(2012, 7, 1, 10, 30)) best_index = 1 # if x axis has increasing coordinates if vertices[-1][0] > vertices[0][0]: for index, vertex in enumerate(vertices): if x >= vertex[0]: best_index = index + 1 # if x axis has decreasing coordinates else: for index, vertex in enumerate(vertices): if x <= vertex[0]: best_index = index + 1 # number of subcoordinates is determined by difference in x coordinates number_of_intermediate_points = math.floor(vertices[best_index][0] - vertices[best_index - 1][0]) intermediate_vertices_list = path_points([vertices[best_index - 1], vertices[best_index]], number_of_intermediate_points) wp1Array = [wpm.waypoint_data(best_index - 1).lat, wpm.waypoint_data(best_index - 1).lon, datetime.datetime(2012, 7, 1, 10, 30)] wp2Array = [wpm.waypoint_data(best_index).lat, wpm.waypoint_data(best_index).lon, datetime.datetime(2012, 7, 1, 10, 30)] intermediate_waypoints_list = latlon_points(wp1Array, wp2Array, number_of_intermediate_points, connection="greatcircle") # best_index1 is the best index among the intermediate coordinates to fit the hovered point # if x axis has increasing coordinates best_index1 = 1 if vertices[-1][0] > vertices[0][0]: for index, vertex in enumerate(intermediate_vertices_list[0]): if x >= vertex: best_index1 = index + 1 # if x axis has decreasing coordinates else: for index, vertex in enumerate(intermediate_vertices_list[0]): if x <= vertex: best_index1 = index + 1 # depends if best_index1 or best_index1 - 1 on closeness to left or right neighbourhood return [intermediate_waypoints_list[0][best_index1 - 1], intermediate_waypoints_list[1][best_index1 - 1]], best_index def button_release_move_callback(self, event): """Called whenever a mouse button is released. """ if not self.showverts or event.button != 1: return if self._ind is not None: # Submit the new pressure (the only value that can be edited # in the side view) to the data model. vertices = self.pathpatch.get_path().vertices pressure = vertices[self._ind][1] # http://doc.trolltech.com/4.3/qabstractitemmodel.html#createIndex qt_index = self.waypoints_model.createIndex(self._ind, ft.PRESSURE) # NOTE: QVariant cannot handle numpy.float64 types, hence convert # to float(). self.waypoints_model.setData(qt_index, QtCore.QVariant(float(pressure / 100.))) self._ind = None def motion_notify_callback(self, event): """Called on mouse movement. Redraws the path if a vertex has been picked and is being dragged. In the side view, the horizontal position of a waypoint is locked. Hence, points can only be moved in the vertical direction (y position in this view). """ if not self.showverts or self._ind is None or event.inaxes is None or event.button != 1: return vertices = self.pathpatch.get_path().vertices # Set the new y position of the vertex to event.ydata. Keep the # x coordinate. vertices[self._ind] = vertices[self._ind][0], event.ydata self.redraw_path(vertices) def qt_data_changed_listener(self, index1, index2): """Listens to dataChanged() signals emitted by the flight track data model. The side view can thus react to data changes induced by another view (table, top view). """ # If the altitude of a point has changed, only the plotted flight # profile needs to be redrawn (redraw_path()). If the horizontal # position of a waypoint has changed, the entire figure needs to be # redrawn, as this affects the x-position of all points. self.pathpatch.get_path().update_from_WaypointsTableModel(self.waypoints_model) if index1.column() in [ft.FLIGHTLEVEL, ft.PRESSURE, ft.LOCATION]: self.redraw_path(self.pathpatch.get_path().vertices) elif index1.column() in [ft.LAT, ft.LON]: self.redraw_figure() elif index1.column() in [ft.TIME_UTC]: if self.redraw_xaxis is not None: self.redraw_xaxis(self.path.ilats, self.path.ilons, self.path.itimes)