From 64a60854792a438b886b6e7bc26dbcf2634d47f5 Mon Sep 17 00:00:00 2001 From: Jatin Jain Date: Mon, 21 Nov 2022 13:07:05 +0100 Subject: [PATCH] Merge gsoc2022 jatin jain to develop (#1599) * Filter flight paths (#1488) * filter_flight_paths * fixed_msui * changed endpoints names and fixed typos * inactivated_operation_list_updated * added_tests * fixed_pytest_error * added_mscolab_tests * enable_github_testing * changed_branch name_in_testing-develop.yml * initialized_"last_used"_attribute * refactored_failing_tests * refactored_failing_tests * Multiple Flightpath on Topview (#1510) * multiple_flightpath_dockwidget * updated_multiple_flightpath_dockwidget * connect_slots_and_signals_to_open_ftml_files * Update ui_multiple_flightpath_dockwidget.py * updated_author_and_copyright_year * plot_multiple_flighttracks * remove_flighttrack_added * remove_unchecked_flighttracks_from_canvas * Update mpl_qtwidget.py * reversed_plotting_of_lat_lon * plot_flighttracks_from_MSUIMainWindow * plot_inactive_flighttracks * fixed_naming * sync_listViews * fixed_namespaces * Mscolab Chat Improvements (#1508) * timestamps_below_messages * added_ToDo * updated_multiple_flightpath_dockwidget (#1544) * updated_multiple_flightpath_dockwidget * set_activated_track_as_uncheckable * changed_flags_naming * improved_list_naming * flickering_topview_solved * change_color_of_selected_flightpath * show_color_icon * update_previously_activated_flighttrack_wp_model_in_dict * check_previously_activated_track * change_linewidth * plot_operations_topview * bug_fixes * checkmark_protection_behaviour_updated * show_flighttrack_color_icon * defined_coloring_of_operation_flightpaths * add_todos * add_comments * fixed_coloring_of_flighttracks * clear_operation_list_after_logout * signals_disconnected_after_logout * update_last_used_operation * fixed_test * fixed_color * implemented_operation_permission_revoked * revoked_mscolab_permission * change_operations_linewidth * fixed_uncheck_operations * remove_filter_flightpaths_test * color_change_updated * removed_activation_of_operations_on_addition * Update msui.py * fixed_tracks_coloring * add_todos * Add Documentation for Multiple Flightpath Dockwidget (#1595) * small_code_enhancements * update_documentation * flake8 * Empty-Commit Co-authored-by: Jatin Jain <72596619+Jatin2020-24@users.noreply.github.com> Co-authored-by: Jatin Jain --- .github/workflows/testing-develop.yml | 3 +- docs/usage.rst | 21 + mslib/mscolab/file_manager.py | 10 +- mslib/mscolab/models.py | 13 +- mslib/mscolab/server.py | 41 +- mslib/msui/kmloverlay_dockwidget.py | 2 +- mslib/msui/mpl_qtwidget.py | 9 + mslib/msui/mscolab.py | 109 ++- mslib/msui/mscolab_chat.py | 17 + mslib/msui/msui.py | 53 +- mslib/msui/multiple_flightpath_dockwidget.py | 804 ++++++++++++++++++ mslib/msui/qt5/ui_mainwindow.py | 47 +- .../qt5/ui_multiple_flightpath_dockwidget.py | 122 +++ mslib/msui/socket_control.py | 2 +- mslib/msui/topview.py | 107 ++- mslib/msui/ui/ui_mainwindow.ui | 106 ++- .../ui/ui_multiple_flightpath_dockwidget.ui | 164 ++++ mslib/utils/qt.py | 2 +- tests/_test_mscolab/test_file_manager.py | 2 + tests/_test_mscolab/test_seed.py | 8 +- tests/_test_mscolab/test_server.py | 19 + tests/_test_msui/test_mscolab.py | 54 ++ 22 files changed, 1617 insertions(+), 98 deletions(-) create mode 100644 mslib/msui/multiple_flightpath_dockwidget.py create mode 100644 mslib/msui/qt5/ui_multiple_flightpath_dockwidget.py create mode 100644 mslib/msui/ui/ui_multiple_flightpath_dockwidget.ui diff --git a/.github/workflows/testing-develop.yml b/.github/workflows/testing-develop.yml index 6a1884b93..2d8787364 100644 --- a/.github/workflows/testing-develop.yml +++ b/.github/workflows/testing-develop.yml @@ -4,9 +4,11 @@ on: push: branches: - develop + - GSOC2022-JatinJain pull_request: branches: - develop + - GSOC2022-JatinJain jobs: test-develop: @@ -29,4 +31,3 @@ jobs: secrets: PAT: ${{ secrets.PAT }} - diff --git a/docs/usage.rst b/docs/usage.rst index 5c67d981b..76ab51c22 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -204,6 +204,27 @@ Close the software with ease of mind. Next time you open your software, all your you left it! KML Overlay supports **Saving Open files** so that you can jump back in, anytime! +Multiple Flightpath Dockwidget +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + +The topview has a dockwidget allowing to plot multiple flighttracks/operations on top of map. + +New flightpaths can be added or removed without crashing, and a clear visualization on map, with +relevant geometries and styles. + +The multiple flightpath dockwidget interface supports display of multiple flighttracks on map simultaneously, +with a check box to display/hide individual plots on map. + +Activated flighttrack/operation is shown in bold letters and can't be unchecked. + +"Change Linewidth" and "Change Color" button improves the User experience by allowing user to customize +color & linewidth of each of flightpath displayed, realtime. This allows for better understanding of map and +flightpath. + +For Activated Flightpath, use "options" menu on topview interface. + + Test Samples ------------ diff --git a/mslib/mscolab/file_manager.py b/mslib/mscolab/file_manager.py index f456c1da9..848e488e2 100644 --- a/mslib/mscolab/file_manager.py +++ b/mslib/mscolab/file_manager.py @@ -24,6 +24,7 @@ See the License for the specific language governing permissions and limitations under the License. """ +import datetime import fs import difflib import logging @@ -39,7 +40,7 @@ class FileManager(object): def __init__(self, data_dir): self.data_dir = data_dir - def create_operation(self, path, description, user, content=None, category="default"): + def create_operation(self, path, description, user, last_used=None, content=None, category="default"): """ path: path to the operation description: description of the operation @@ -51,7 +52,9 @@ def create_operation(self, path, description, user, content=None, category="defa proj_available = Operation.query.filter_by(path=path).first() if proj_available is not None: return False - operation = Operation(path, description, category) + if last_used is None: + last_used = datetime.datetime.utcnow() + operation = Operation(path, description, last_used, category) db.session.add(operation) db.session.flush() operation_id = operation.id @@ -100,7 +103,8 @@ def list_operations(self, user): "access_level": permission.access_level, "path": operation.path, "description": operation.description, - "category": operation.category + "category": operation.category, + "active": operation.active }) return operations diff --git a/mslib/mscolab/models.py b/mslib/mscolab/models.py index b6acfc416..5bde744c8 100644 --- a/mslib/mscolab/models.py +++ b/mslib/mscolab/models.py @@ -125,8 +125,10 @@ class Operation(db.Model): path = db.Column(db.String(255), unique=True) category = db.Column(db.String(255)) description = db.Column(db.String(255)) + active = db.Column(db.Boolean) + last_used = db.Column(db.DateTime) - def __init__(self, path, description, category="default"): + def __init__(self, path, description, last_used=None, category="default", active=True): """ path: path to the operation description: small description of operation @@ -135,9 +137,16 @@ def __init__(self, path, description, category="default"): self.path = path self.description = description self.category = category + self.active = active + if self.last_used is None: + self.last_used = datetime.datetime.utcnow() + else: + self.last_used = last_used def __repr__(self): - return f'' + return f' ' class MessageType(enum.IntEnum): diff --git a/mslib/mscolab/server.py b/mslib/mscolab/server.py index af15be010..50644f1cc 100644 --- a/mslib/mscolab/server.py +++ b/mslib/mscolab/server.py @@ -44,7 +44,7 @@ from werkzeug.utils import secure_filename from mslib.mscolab.conf import mscolab_settings -from mslib.mscolab.models import Change, MessageType, User, db +from mslib.mscolab.models import Change, MessageType, User, Operation, db from mslib.mscolab.sockets_manager import setup_managers from mslib.mscolab.utils import create_files, get_message_dict from mslib.utils import conditional_decorator @@ -54,7 +54,6 @@ mail = Mail(APP) CORS(APP, origins=mscolab_settings.CORS_ORIGINS if hasattr(mscolab_settings, "CORS_ORIGINS") else ["*"]) - # set the operation root directory as the static folder # ToDo needs refactoring on a route without using of static folder @@ -235,8 +234,8 @@ def get_auth_token(): if user.confirmed: token = user.generate_auth_token() return json.dumps({ - 'token': token.decode('ascii'), - 'user': {'username': user.username, 'id': user.id}}) + 'token': token.decode('ascii'), + 'user': {'username': user.username, 'id': user.id}}) else: return "False" else: @@ -394,8 +393,9 @@ def create_operation(): content = request.form.get('content', None) description = request.form.get('description', None) category = request.form.get('category', "default") + last_used = datetime.datetime.utcnow() user = g.user - r = str(fm.create_operation(path, description, user, content=content, category=category)) + r = str(fm.create_operation(path, description, user, last_used, content=content, category=category)) if r == "True": token = request.args.get('token', request.form.get('token', False)) json_config = {"token": token} @@ -500,6 +500,37 @@ def get_operation_details(): return json.dumps(fm.get_operation_details(int(op_id), user)) +@APP.route('/set_last_used', methods=["POST"]) +@verify_user +def set_last_used(): + op_id = request.form.get('op_id', None) + operation = Operation.query.filter_by(id=int(op_id)).first() + operation.last_used = datetime.datetime.utcnow() + temp_operation_active = operation.active + operation.active = True + db.session.commit() + # Reload Operation List + if not temp_operation_active: + token = request.args.get('token', request.form.get('token', False)) + json_config = {"token": token} + sockio.sm.update_operation_list(json_config) + return jsonify({"success": True}), 200 + + +@APP.route('/update_last_used', methods=["POST"]) +@verify_user +def update_last_used(): + operations = Operation.query.filter().all() + for operation in operations: + a = (datetime.datetime.utcnow() - operation.last_used).days + if a > 30: + operation.active = False + else: + operation.active = True + db.session.commit() + return jsonify({"success": True}), 200 + + @APP.route('/undo', methods=["POST"]) @verify_user def undo_ftml(): diff --git a/mslib/msui/kmloverlay_dockwidget.py b/mslib/msui/kmloverlay_dockwidget.py index 13275b466..0cf4df8a1 100644 --- a/mslib/msui/kmloverlay_dockwidget.py +++ b/mslib/msui/kmloverlay_dockwidget.py @@ -539,7 +539,7 @@ def load_file(self): Loads multiple KML Files simultaneously and constructs the corresponding patches. """ - for entry in self.dict_files.values(): # removes all patches from map, but not from dict_files + for entry in self.dict_files.values(): # removes all patches from map, but not from dict_flighttrack if entry["patch"] is not None: # since newly initialized files will have patch:None entry["patch"].remove() diff --git a/mslib/msui/mpl_qtwidget.py b/mslib/msui/mpl_qtwidget.py index 2f281ff6d..2b5a478ed 100644 --- a/mslib/msui/mpl_qtwidget.py +++ b/mslib/msui/mpl_qtwidget.py @@ -1163,6 +1163,7 @@ def __init__(self, settings=None): self.waypoints_interactor = None self.satoverpasspatch = [] self.kmloverlay = None + self.multiple_flightpath = None self.map = None self.basename = "topview" @@ -1280,6 +1281,9 @@ def redraw_map(self, kwargs_update=None): if self.kmloverlay: self.kmloverlay.update() + if self.multiple_flightpath: + self.multiple_flightpath.update() + # self.draw_metadata() ; It is not needed here, since below here already plot title is being set. # Setting fontsize for topview plot title when map is redrawn. @@ -1417,6 +1421,11 @@ def plot_kml(self, kmloverlay): """ self.kmloverlay = kmloverlay + def plot_multiple_flightpath(self, multipleflightpath): + """Plots a multiple flightpaths on topview of the map + """ + self.multiple_flightpath = multipleflightpath + def set_map_appearance(self, settings_dict): """Apply settings from dictionary 'settings_dict' to the view. diff --git a/mslib/msui/mscolab.py b/mslib/msui/mscolab.py index a5c95e5a9..7e0a19a80 100644 --- a/mslib/msui/mscolab.py +++ b/mslib/msui/mscolab.py @@ -413,6 +413,15 @@ class MSUIMscolab(QtCore.QObject): """ name = "Mscolab" + signal_activate_operation = QtCore.Signal(int, name="signal_activate_operation") + signal_operation_added = QtCore.Signal(int, str, name="signal_operation_added") + signal_operation_removed = QtCore.Signal(int, name="signal_operation_removed") + signal_login_mscolab = QtCore.Signal(str, str, name="signal_login_mscolab") + signal_logout_mscolab = QtCore.Signal(name="signal_logout_mscolab") + signal_listFlighttrack_doubleClicked = QtCore.Signal() + signal_permission_revoked = QtCore.Signal(int) + signal_render_new_permission = QtCore.Signal(int, str) + def __init__(self, parent=None, data_dir=None): super(MSUIMscolab, self).__init__(parent) self.ui = parent @@ -428,8 +437,7 @@ def __init__(self, parent=None, data_dir=None): self.ui.activeOperationDesc.setHidden(True) # reset operation description label for flight tracks and open views - self.ui.listFlightTracks.itemDoubleClicked.connect( - lambda: self.ui.activeOperationDesc.setText("Select Operation to View Description.")) + self.ui.listFlightTracks.itemDoubleClicked.connect(self.listFlighttrack_itemDoubleClicked) self.ui.listViews.itemDoubleClicked.connect( lambda: self.ui.activeOperationDesc.setText("Select Operation to View Description.")) @@ -442,6 +450,7 @@ def __init__(self, parent=None, data_dir=None): self.ui.actionLeaveOperation.triggered.connect(self.operation_options_handler) self.ui.actionUpdateOperationDesc.triggered.connect(self.update_description_handler) self.ui.actionRenameOperation.triggered.connect(self.rename_operation_handler) + self.ui.actionActivateOperation.triggered.connect(self.activate_operation) self.ui.actionDescription.triggered.connect( lambda: QtWidgets.QMessageBox.information(None, "Operation Description", @@ -466,6 +475,8 @@ def __init__(self, parent=None, data_dir=None): self.token = None # int to store active pid self.active_op_id = None + # int to store selected inactive op_id + self.inactive_op_id = None # storing access_level to save network call self.access_level = None # storing operation_name to save network call @@ -567,6 +578,11 @@ def after_login(self, emailid, url, r): "New Login required!") self.logout() else: + # Update Last Used + data = { + "token": self.token + } + r = requests.post(f"{self.mscolab_server_url}/update_last_used", data=data) self.conn.signal_operation_list_updated.connect(self.reload_operation_list) self.conn.signal_reload.connect(self.reload_window) self.conn.signal_new_permission.connect(self.render_new_permission) @@ -597,8 +613,12 @@ def after_login(self, emailid, url, r): self.ui.actionUpdateOperationDesc.setEnabled(False) # disable delete operation button self.ui.actionDeleteOperation.setEnabled(False) - # enable category change selector + # disable category change selector self.ui.filterCategoryCb.setEnabled(True) + # disable activate operation button + self.ui.actionActivateOperation.setEnabled(False) + + self.signal_login_mscolab.emit(self.mscolab_server_url, self.token) def fetch_gravatar(self, refresh=False): email_hash = hashlib.md5(bytes(self.email.encode('utf-8')).lower()).hexdigest() @@ -826,6 +846,7 @@ def add_operation(self): self.error_dialog.showMessage('Your operation was created successfully') op_id = self.get_recent_op_id() self.conn.handle_new_operation(op_id) + self.signal_operation_added.emit(op_id, path) else: self.error_dialog = QtWidgets.QErrorMessage() self.error_dialog.showMessage('The path already exists') @@ -992,6 +1013,7 @@ def handle_delete_operation(self): res = requests.post(url, data=data) res.raise_for_status() self.reload_operations() + self.signal_operation_removed.emit(self.active_op_id) except requests.exceptions.RequestException as e: logging.debug(e) show_popup(self.ui, "Error", "Some error occurred! Could not delete operation.") @@ -1290,6 +1312,7 @@ def render_new_permission(self, op_id, u_id): widgetItem.access_level = operation["access_level"] widgetItem.active_operation_desc = operation["description"] self.ui.listOperationsMSC.addItem(widgetItem) + self.signal_render_new_permission.emit(operation["op_id"], operation["path"]) if self.chat_window is not None: self.chat_window.load_users() else: @@ -1372,6 +1395,7 @@ def handle_revoke_permission(self, op_id, u_id): # on import permissions revoked name can not taken from the operation list, # because we update the list first by reloading it. show_popup(self.ui, "Permission Revoked", "Access to an operation was revoked") + self.signal_permission_revoked.emit(op_id) @QtCore.Slot(int) def handle_operation_deleted(self, op_id): @@ -1414,22 +1438,72 @@ def add_operations_to_ui(self): logging.debug("adding operations to ui") operations = sorted(self.operations, key=lambda k: k["path"].lower()) self.ui.listOperationsMSC.clear() + self.ui.listInactiveOperationsMSC.clear() selectedOperation = None for operation in operations: operation_desc = f'{operation["path"]} - {operation["access_level"]}' - widgetItem = QtWidgets.QListWidgetItem(operation_desc, parent=self.ui.listOperationsMSC) + widgetItem = QtWidgets.QListWidgetItem(operation_desc) widgetItem.active_operation_desc = operation["description"] widgetItem.op_id = operation["op_id"] widgetItem.access_level = operation["access_level"] widgetItem.operation_path = operation["path"] widgetItem.operation_category = operation["category"] + widgetItem.active = operation["active"] if widgetItem.op_id == self.active_op_id: selectedOperation = widgetItem - self.ui.listOperationsMSC.addItem(widgetItem) + if widgetItem.active: + self.ui.listOperationsMSC.addItem(widgetItem) + else: + self.ui.listInactiveOperationsMSC.addItem(widgetItem) if selectedOperation is not None: self.ui.listOperationsMSC.setCurrentItem(selectedOperation) self.ui.listOperationsMSC.itemActivated.emit(selectedOperation) self.ui.listOperationsMSC.itemActivated.connect(self.set_active_op_id) + self.ui.listInactiveOperationsMSC.itemActivated.connect(self.select_inactive_operation) + else: + show_popup(self.ui, "Error", "Session expired, new login required") + self.logout() + else: + show_popup(self.ui, "Error", "Your Connection is expired. New Login required!") + self.logout() + + def select_inactive_operation(self, item): + self.inactive_op_id = item.op_id + self.active_op_id = None + font = QtGui.QFont() + for i in range(self.ui.listInactiveOperationsMSC.count()): + self.ui.listInactiveOperationsMSC.item(i).setFont(font) + font.setBold(True) + item.setFont(font) + self.show_operation_options_in_inactivated_state(item.access_level) + + def show_operation_options_in_inactivated_state(self, access_level): + self.ui.actionChat.setEnabled(False) + self.ui.actionVersionHistory.setEnabled(False) + self.ui.actionManageUsers.setEnabled(False) + self.ui.menuProperties.setEnabled(False) + self.ui.actionRenameOperation.setEnabled(False) + self.ui.actionLeaveOperation.setEnabled(False) + self.ui.actionDeleteOperation.setEnabled(False) + self.ui.actionUpdateOperationDesc.setEnabled(False) + self.ui.actionActivateOperation.setEnabled(False) + if access_level == "creator": + self.ui.actionActivateOperation.setEnabled(True) + + def activate_operation(self): + if verify_user_token(self.mscolab_server_url, self.token): + # set last used date for operation + data = { + "token": self.token, + "op_id": self.inactive_op_id, + } + res = requests.post(f'{self.mscolab_server_url}/set_last_used', data=data) + if res.text != "False": + res = res.json() + if res["success"]: + self.reload_operations() + else: + show_popup(self.ui, "Error", "Some error occurred! Could not activate operation") else: show_popup(self.ui, "Error", "Session expired, new login required") self.logout() @@ -1452,6 +1526,16 @@ def set_active_op_id(self, item): self.ui.workLocallyCheckbox.setChecked(False) self.ui.workLocallyCheckbox.blockSignals(False) + # Disable Activate Operation Button + self.ui.actionActivateOperation.setEnabled(False) + + # set last used date for operation + data = { + "token": self.token, + "op_id": item.op_id, + } + requests.post(f'{self.mscolab_server_url}/set_last_used', data=data) + # set active_op_id here self.active_op_id = item.op_id self.access_level = item.access_level @@ -1459,6 +1543,14 @@ def set_active_op_id(self, item): self.active_operation_desc = item.active_operation_desc self.waypoints_model = None + self.signal_activate_operation.emit(self.active_op_id) + + self.inactive_op_id = None + font = QtGui.QFont() + for i in range(self.ui.listOperationsMSC.count()): + self.ui.listOperationsMSC.item(i).setFont(font) + font.setBold(False) + # Set active operation description self.set_operation_desc_label(self.active_operation_desc) # set active flightpath here @@ -1690,6 +1782,10 @@ def handle_export_msc(self, extension, function, pickertype): show_popup(self.ui, "Error", "Your Connection is expired. New Login required!") self.logout() + def listFlighttrack_itemDoubleClicked(self): + self.ui.activeOperationDesc.setText("Select Operation to View Description.") + self.signal_listFlighttrack_doubleClicked.emit() + def logout(self): if self.mscolab_server_url is None: return @@ -1710,6 +1806,8 @@ def logout(self): self.local_ftml_file = None # clear operation listing self.ui.listOperationsMSC.clear() + # clear inactive operation listing + self.ui.listInactiveOperationsMSC.clear() # clear mscolab url self.mscolab_server_url = None # clear operations list here @@ -1751,6 +1849,7 @@ def logout(self): # disable category change selector self.ui.filterCategoryCb.setEnabled(False) + self.signal_logout_mscolab.emit() # Don't try to activate local flighttrack while testing if "pytest" not in sys.modules: diff --git a/mslib/msui/mscolab_chat.py b/mslib/msui/mscolab_chat.py index b555b1d8d..fca4ddccd 100644 --- a/mslib/msui/mscolab_chat.py +++ b/mslib/msui/mscolab_chat.py @@ -518,22 +518,36 @@ def setup_message_box(self): self.messageBox.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.messageBox.customContextMenuRequested.connect(self.open_context_menu) + def set_time_label(self): + # ToDo: Translate time in user's timezone + time_label = QtWidgets.QLabel(f"{self.time}") + time_label.setContentsMargins(5, 5, 5, 0) + time_label.setAlignment(QtCore.Qt.AlignRight) + label_font = QtGui.QFont() + label_font.setItalic(True) + time_label.setFont(label_font) + return time_label + def setup_message_box_layout(self): container_layout = QtWidgets.QHBoxLayout() text_area_layout = QtWidgets.QVBoxLayout() if self.chat_window.user["username"] == self.username: text_area_layout.addWidget(self.messageBox) + time_label = self.set_time_label() + text_area_layout.addWidget(time_label) self.textArea.setLayout(text_area_layout) container_layout.addStretch() container_layout.addWidget(self.textArea) else: username_label = QtWidgets.QLabel(f"{self.username}") username_label.setContentsMargins(5, 5, 5, 0) + time_label = self.set_time_label() label_font = QtGui.QFont() label_font.setBold(True) username_label.setFont(label_font) text_area_layout.addWidget(username_label) text_area_layout.addWidget(self.messageBox) + text_area_layout.addWidget(time_label) self.textArea.setLayout(text_area_layout) container_layout.addWidget(self.textArea) container_layout.addStretch() @@ -577,6 +591,9 @@ def insert_reply_area(self): self.replyScroll.setWidget(self.replyArea) self.replyScroll.setContentsMargins(0, 0, 0, 0) self.textArea.layout().addWidget(self.replyScroll) + time_label = self.set_time_label() + self.textArea.layout().addWidget(time_label) + if self.username == self.chat_window.user["username"]: color = "#c3f39e" else: diff --git a/mslib/msui/msui.py b/mslib/msui/msui.py index 48166bdf5..3513c6d26 100644 --- a/mslib/msui/msui.py +++ b/mslib/msui/msui.py @@ -140,6 +140,7 @@ class MSUI_ShortcutsDialog(QtWidgets.QDialog, ui_sh.Ui_ShortcutsDialog): """ Dialog showing shortcuts for all currently open windows """ + def __init__(self): super(MSUI_ShortcutsDialog, self).__init__(QtWidgets.QApplication.activeWindow()) self.setupUi(self) @@ -255,8 +256,8 @@ def get_shortcuts(self): action.toolTip(), action.text().replace("&&", "%%").replace("&", "").replace("%%", "&"), action.objectName(), ",".join([shortcut.toString() for shortcut in action.shortcuts()]), action) - for action in qobject.findChildren(QtWidgets.QAction) if len(action.shortcuts()) > 0 or - self.cbNoShortcut.checkState()]) + for action in qobject.findChildren( + QtWidgets.QAction) if len(action.shortcuts()) > 0 or self.cbNoShortcut.checkState()]) actions.extend([(shortcut.parentWidget().window(), shortcut.whatsThis(), "", shortcut.objectName(), shortcut.key().toString(), shortcut) for shortcut in qobject.findChildren(QtWidgets.QShortcut)]) @@ -280,7 +281,7 @@ def get_shortcuts(self): if not any(action for action in actions if action[3] == "actionShortcuts"): actions.append((qobject.window(), "Show Current Shortcuts", "Show Current Shortcuts", - "Show Current Shortcuts", "Alt+S", None)) + "Show Current Shortcuts", "Alt+S", None)) if not any(action for action in actions if action[3] == "actionSearch"): actions.append((qobject.window(), "Search for interactive text in the UI", "Search for interactive text in the UI", "Search for interactive text in the UI", @@ -340,6 +341,15 @@ class MSUIMainWindow(QtWidgets.QMainWindow, ui.Ui_MSUIMainWindow): """ viewsChanged = QtCore.pyqtSignal(name="viewsChanged") + signal_activate_flighttrack = QtCore.Signal(ft.WaypointsTableModel, name="signal_activate_flighttrack") + signal_activate_operation = QtCore.Signal(int, name="signal_activate_operation") + signal_operation_added = QtCore.Signal(int, str, name="signal_operation_added") + signal_operation_removed = QtCore.Signal(int, name="signal_operation_removed") + signal_login_mscolab = QtCore.Signal(str, str, name="signal_login_mscolab") + signal_logout_mscolab = QtCore.Signal(name="signal_logout_mscolab") + signal_listFlighttrack_doubleClicked = QtCore.Signal() + signal_permission_revoked = QtCore.Signal(int) + signal_render_new_permission = QtCore.Signal(int, str) def __init__(self, mscolab_data_dir=None, *args): super(MSUIMainWindow, self).__init__(*args) @@ -432,6 +442,16 @@ def __init__(self, mscolab_data_dir=None, *args): # disable category until connected/login into mscolab self.filterCategoryCb.setEnabled(False) + self.mscolab.signal_activate_operation.connect(self.activate_operation_slot) + self.mscolab.signal_operation_added.connect(self.add_operation_slot) + self.mscolab.signal_operation_removed.connect(self.remove_operation_slot) + self.mscolab.signal_login_mscolab.connect(lambda d, t: self.signal_login_mscolab.emit(d, t)) + self.mscolab.signal_logout_mscolab.connect(lambda: self.signal_logout_mscolab.emit()) + self.mscolab.signal_listFlighttrack_doubleClicked.connect( + lambda: self.signal_listFlighttrack_doubleClicked.emit()) + self.mscolab.signal_permission_revoked.connect(lambda op_id: self.signal_permission_revoked.emit(op_id)) + self.mscolab.signal_render_new_permission.connect( + lambda op_id, path: self.signal_render_new_permission.emit(op_id, path)) # Don't start the updater during a test run of msui if "pytest" not in sys.modules: @@ -497,6 +517,18 @@ def add_plugins(self): self.add_import_plugins(picker_default) self.add_export_plugins(picker_default) + @QtCore.Slot(int) + def activate_operation_slot(self, active_op_id): + self.signal_activate_operation.emit(active_op_id) + + @QtCore.Slot(int, str) + def add_operation_slot(self, op_id, path): + self.signal_operation_added.emit(op_id, path) + + @QtCore.Slot(int) + def remove_operation_slot(self, op_id): + self.signal_operation_removed.emit(op_id) + def add_plugin_submenu(self, name, extension, function, pickertype, plugin_type="Import"): if plugin_type == "Import": menu = self.menuImportFlightTrack @@ -682,11 +714,11 @@ def create_new_flight_track(self, template=None, filename=None, function=None): waypoints_model.name += " - imported from file" break else: - # Create a new flight track from the waypoints template. + # Create a new flight track from the waypoints' template. self.new_flight_track_counter += 1 waypoints_model = ft.WaypointsTableModel( name=f"new flight track ({self.new_flight_track_counter:d})") - # Make a copy of the template. Otherwise all new flight tracks would + # Make a copy of the template. Otherwise, all new flight tracks would # use the same data structure in memory. template_copy = copy.deepcopy(template) waypoints_model.insertRows(0, rows=len(template_copy), waypoints=template_copy) @@ -714,6 +746,7 @@ def activate_flight_track(self, item): font.setBold(True) item.setFont(font) self.menu_handler() + self.signal_activate_flighttrack.emit(self.active_flight_track) def update_active_flight_track(self, old_flight_track_name=None): for i in range(self.listViews.count()): @@ -723,7 +756,7 @@ def update_active_flight_track(self, old_flight_track_name=None): view_item.window.enable_navbar_action_buttons() if old_flight_track_name is not None: view_item.window.setWindowTitle(view_item.window.windowTitle().replace(old_flight_track_name, - self.active_flight_track.name)) + self.active_flight_track.name)) def activate_selected_flight_track(self): item = self.listFlightTracks.currentItem() @@ -747,7 +780,8 @@ def save_handler(self): self.save_as_handler() def save_as_handler(self): - """Slot for the 'Save Active Flight Track As' menu entry. + """ + Slot for the 'Save Active Flight Track As' menu entry. """ default_filename = os.path.join(self.last_save_directory, self.active_flight_track.name + ".ftml") file_type = ["Flight track (*.ftml)"] @@ -818,7 +852,10 @@ def create_view(self, _type, model): view_window = None if _type == "topview": # Top view. - view_window = topview.MSUITopViewWindow(model=model) + view_window = topview.MSUITopViewWindow(parent=self, model=model, + active_flighttrack=self.active_flight_track, + mscolab_server_url=self.mscolab.mscolab_server_url, + token=self.mscolab.token) view_window.mpl.resize(layout['topview'][0], layout['topview'][1]) if layout["immutable"]: view_window.mpl.setFixedSize(layout['topview'][0], layout['topview'][1]) diff --git a/mslib/msui/multiple_flightpath_dockwidget.py b/mslib/msui/multiple_flightpath_dockwidget.py new file mode 100644 index 000000000..160a08257 --- /dev/null +++ b/mslib/msui/multiple_flightpath_dockwidget.py @@ -0,0 +1,804 @@ +# -*- coding: utf-8 -*- +""" + + mslib.multiple_flightpath_dockwidget + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Control Widget to configure multiple flightpath on topview. + + This file is part of MSS. + + :copyright: Copyright 2022 Jatin Jain + :copyright: Copyright 2022 by the MSS team, see AUTHORS. + :license: APACHE-2.0, see LICENSE for details. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" + +from PyQt5 import QtWidgets, QtGui, QtCore +from mslib.msui.qt5 import ui_multiple_flightpath_dockwidget as ui +from mslib.msui import flighttrack as ft +from mslib.msui import msui +from mslib.utils.verify_user_token import verify_user_token +import threading +import requests +import json + + +class QMscolabOperationsListWidgetItem(QtWidgets.QListWidgetItem): + """ + """ + + def __init__(self, flighttrack_model, op_id: int, parent=None, type=QtWidgets.QListWidgetItem.UserType): + view_name = flighttrack_model.name + super(QMscolabOperationsListWidgetItem, self).__init__( + view_name, parent, type + ) + self.parent = parent + self.flighttrack_model = flighttrack_model + self.op_id = op_id + + +class MultipleFlightpath(object): + """ + Represent a Multiple FLightpath + """ + + def __init__(self, mapcanvas, wp, linewidth=2.0, color='blue'): + self.map = mapcanvas + self.flightlevel = None + self.comments = '' + self.patches = [] + self.waypoints = wp + self.linewidth = linewidth + self.color = color + self.draw() + + def draw_line(self, x, y): + self.patches.append(self.map.plot(x, y, color=self.color, linewidth=self.linewidth)) + + def compute_xy(self, lon, lat): + x, y = self.map.gcpoints_path(lon, lat) + return x, y + + def get_lonlat(self): + lon = [] + lat = [] + for i in range(len(self.waypoints)): + lat.append(self.waypoints[i][0]) + lon.append(self.waypoints[i][1]) + return lat, lon + + def update(self, linewidth=None, color=None): + if linewidth is not None: + self.linewidth = linewidth + if color is not None: + self.color = color + self.remove() + self.draw() + + def draw(self): + lat, lon = self.get_lonlat() + x, y = self.compute_xy(lon, lat) + self.draw_line(x, y) + self.map.ax.figure.canvas.draw() + + def remove(self): + for patch in self.patches: + for elem in patch: + elem.remove() + self.patches = [] + self.map.ax.figure.canvas.draw() + + +class MultipleFlightpathControlWidget(QtWidgets.QWidget, ui.Ui_MultipleViewWidget): + """ + This class provides the interface for plotting Multiple Flighttracks + on the TopView canvas. + """ + + # ToDO: Make a new parent class with all the functions in this class and inherit them + # in MultipleFlightpathControlWidget and MultipleFlightpathOperations classes. + + signal_parent_closes = QtCore.Signal() + + def __init__(self, parent=None, view=None, listFlightTracks=None, + listOperationsMSC=None, activeFlightTrack=None, mscolab_server_url=None, token=None): + super(MultipleFlightpathControlWidget, self).__init__(parent) + # ToDO: Remove all patches, on closing dockwidget. + self.ui = parent + self.setupUi(self) + self.view = view # canvas + self.flight_path = None # flightpath object + self.dict_flighttrack = {} # Dictionary of flighttrack data: patch,color,wp_model + self.active_flight_track = activeFlightTrack + self.listOperationsMSC = listOperationsMSC + self.listFlightTracks = listFlightTracks + self.mscolab_server_url = mscolab_server_url + self.token = token + self.ft_settings_dict = self.ui.get_settings() + self.color = self.ft_settings_dict["colour_ft_vertices"] + self.obb = [] + + self.operation_list = False + self.flighttrack_list = True + + # Set flags + # ToDo: Use invented constants for initialization. + self.flighttrack_added = False + self.flighttrack_activated = False + self.color_change = False + self.change_linewidth = False + self.dsbx_linewidth.setValue(2.0) + + # Connect Signals and Slots + self.listFlightTracks.model().rowsInserted.connect(self.wait) + self.listFlightTracks.model().rowsRemoved.connect(self.flighttrackRemoved) + self.ui.signal_activate_flighttrack1.connect(self.get_active) + self.list_flighttrack.itemChanged.connect(self.flagop) + + self.pushButton_color.clicked.connect(self.select_color) + self.ui.signal_ft_vertices_color_change.connect(self.ft_vertices_color) + self.dsbx_linewidth.valueChanged.connect(self.set_linewidth) + self.ui.signal_login_mscolab.connect(self.login) + + self.colorPixmap.setPixmap(self.show_color_pixmap(self.color)) + + self.list_flighttrack.itemClicked.connect(self.listFlighttrack_itemClicked) + + if self.mscolab_server_url is not None: + self.connect_mscolab_server() + + if parent is not None: + parent.viewCloses.connect(lambda: self.signal_parent_closes.emit()) + + # Load flighttracks + for index in range(self.listFlightTracks.count()): + wp_model = self.listFlightTracks.item(index).flighttrack_model + self.create_list_item(wp_model) + + self.activate_flighttrack() + + @QtCore.Slot() + def logout(self): + self.operations.logout_mscolab() + self.ui.signal_listFlighttrack_doubleClicked.disconnect() + self.ui.signal_permission_revoked.disconnect() + self.ui.signal_render_new_permission.disconnect() + self.operations = None + self.flighttrack_list = True + self.operation_list = False + for idx in range(len(self.obb)): + del self.obb[idx] + + @QtCore.Slot(str, str) + def login(self, url, token): + self.mscolab_server_url = url + self.token = token + self.connect_mscolab_server() + + def connect_mscolab_server(self): + self.operations = MultipleFlightpathOperations(self, self.mscolab_server_url, self.token, + self.list_operation_track, + self.listOperationsMSC, self.view) + self.obb.append(self.operations) + + self.ui.signal_permission_revoked.connect(lambda op_id: self.operations.permission_revoked(op_id)) + self.ui.signal_render_new_permission.connect(lambda op_id, path: self.operations.render_permission(op_id, path)) + # Signal emitted, on activation of operation from MSUI + self.ui.signal_activate_operation.connect(self.update_op_id) + self.ui.signal_operation_added.connect(self.add_operation_slot) + self.ui.signal_operation_removed.connect(self.remove_operation_slot) + + # deactivate vice versa selection of Operation or Flight Track + self.list_operation_track.itemClicked.connect(self.operations.listOperations_itemClicked) + + # deactivate operation or flighttrack + self.listOperationsMSC.itemDoubleClicked.connect(self.deactivate_all_flighttracks) + self.ui.signal_listFlighttrack_doubleClicked.connect(self.operations.deactivate_all_operations) + + # Mscolab Server logout + self.ui.signal_logout_mscolab.connect(self.logout) + + def update(self): + for entry in self.dict_flighttrack.values(): + entry["patch"].update() + + def remove(self): + for entry in self.dict_flighttrack.values(): + entry["patch"].remove() + + def wait(self, parent, start, end): + """ + Adding of flighttrack take time, to avoid emitting of rowInserted signal before that, a delay is inserted in + new thread(it avoid freezing of UI). + """ + # ToDo: Use QThread + self.flighttrack_added = True + t1 = threading.Timer(0.5, self.flighttrackAdded, [parent, start, end]) + t1.start() + + def flagop(self): + if self.flighttrack_added: + self.flighttrack_added = False + elif self.flighttrack_activated: + self.flighttrack_activated = False + elif self.color_change: + self.color_change = False + else: + self.drawInactiveFlighttracks(self.list_flighttrack) + + def flighttrackAdded(self, parent, start, end): + """ + Slot to add flighttrack. + """ + wp_model = self.listFlightTracks.item(start).flighttrack_model + self.create_list_item(wp_model) + if self.mscolab_server_url is not None: + self.operations.deactivate_all_operations() + self.activate_flighttrack() + + @QtCore.Slot(tuple) + def ft_vertices_color(self, color): + self.color = color + self.colorPixmap.setPixmap(self.show_color_pixmap(color)) + + if self.flighttrack_list: + self.dict_flighttrack[self.active_flight_track]["color"] = color + for index in range(self.list_flighttrack.count()): + if self.list_flighttrack.item(index).flighttrack_model == self.active_flight_track: + self.list_flighttrack.item(index).setIcon( + self.show_color_icon(self.get_color(self.active_flight_track))) + break + elif self.operation_list: + self.operations.ft_color_update(color) + + @QtCore.Slot(int, str) + def add_operation_slot(self, op_id, path): + self.operations.operationsAdded(op_id, path) + + @QtCore.Slot(int) + def remove_operation_slot(self, op_id): + self.operations.operationRemoved(op_id) + + @QtCore.Slot(int) + def update_op_id(self, op_id): + self.operations.get_op_id(op_id) + + @QtCore.Slot(ft.WaypointsTableModel) + def get_active(self, active_flighttrack): + self.update_last_flighttrack() + self.active_flight_track = active_flighttrack + self.activate_flighttrack() + + def save_waypoint_model_data(self, wp_model, listWidget): + wp_data = [(wp.lat, wp.lon, wp.flightlevel, wp.location, wp.comments) for wp in wp_model.all_waypoint_data()] + if self.dict_flighttrack[wp_model] is None: + self.create_list_item(wp_model) + self.dict_flighttrack[wp_model]["wp_data"] = wp_data + + def create_list_item(self, wp_model): + """ + PyQt5 method : Add items in list and add checkbox functionality + """ + # Create new key in dict + self.dict_flighttrack[wp_model] = {} + self.dict_flighttrack[wp_model]["patch"] = None + self.dict_flighttrack[wp_model]["color"] = self.color + self.dict_flighttrack[wp_model]["linewidth"] = 2.0 + self.dict_flighttrack[wp_model]["wp_data"] = [] + self.dict_flighttrack[wp_model]["checkState"] = False + + self.save_waypoint_model_data(wp_model, self.list_flighttrack) + + listItem = msui.QFlightTrackListWidgetItem(wp_model, self.list_flighttrack) + listItem.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsUserCheckable) + if not self.flighttrack_added: + self.flighttrack_added = True + listItem.setCheckState(QtCore.Qt.Unchecked) + if not self.flighttrack_added: + self.flighttrack_added = True + + # Show flighttrack color icon + listItem.setIcon(self.show_color_icon(self.get_color(wp_model))) + + return listItem + + def select_color(self): + """ + Sets the color of selected flighttrack when Change Color is clicked. + """ + # ToDO: Use color defined in options for initial color of active flight path. + # afterwards deactivate the color change button in options and it needs also + # the check mark for enabled, but can't be changed (disabled). At the moment + # the dockingwidget is closed the button and checkmark has to become activated again. + + if self.list_flighttrack.currentItem() is not None: + if (hasattr(self.list_flighttrack.currentItem(), "checkState")) and ( + self.list_flighttrack.currentItem().checkState() == QtCore.Qt.Checked): + wp_model = self.list_flighttrack.currentItem().flighttrack_model + if wp_model == self.active_flight_track: + self.error_dialog = QtWidgets.QErrorMessage() + self.error_dialog.showMessage('Use "options" to change color of an activated flighttrack.') + else: + color = QtWidgets.QColorDialog.getColor() + if color.isValid(): + self.dict_flighttrack[wp_model]["color"] = color.getRgbF() + self.color_change = True + self.list_flighttrack.currentItem().setIcon(self.show_color_icon(self.get_color(wp_model))) + self.dict_flighttrack[wp_model]["patch"].update(color=self.dict_flighttrack[wp_model]["color"]) + else: + self.labelStatus.setText("Check Mark the flighttrack to change its color.") + elif self.list_operation_track.currentItem() is not None: + self.operations.select_color() + else: + self.labelStatus.setText("Status: No flight track selected") + + def get_color(self, wp_model): + """ + Returns color of respective flighttrack. + """ + return self.dict_flighttrack[wp_model]["color"] + + def show_color_pixmap(self, clr): + pixmap = QtGui.QPixmap(20, 10) + pixmap.fill(QtGui.QColor(int(clr[0] * 255), int(clr[1] * 255), int(clr[2] * 255))) + return pixmap + + def show_color_icon(self, clr): + """ + Creating object of QPixmap for displaying icon inside the listWidget. + """ + pixmap = self.show_color_pixmap(clr) + return QtGui.QIcon(pixmap) + + def set_linewidth(self): + """ + Change the line width of selected flighttrack. + """ + if self.list_flighttrack.currentItem() is not None: + if (hasattr(self.list_flighttrack.currentItem(), "checkState")) and ( + self.list_flighttrack.currentItem().checkState() == QtCore.Qt.Checked): + wp_model = self.list_flighttrack.currentItem().flighttrack_model + if wp_model != self.active_flight_track: + if self.dict_flighttrack[wp_model]["linewidth"] != self.dsbx_linewidth.value(): + self.dict_flighttrack[wp_model]["linewidth"] = self.dsbx_linewidth.value() + + self.dict_flighttrack[wp_model]["patch"].remove() + self.dict_flighttrack[wp_model]["patch"].update( + self.dict_flighttrack[wp_model]["linewidth"], self.dict_flighttrack[wp_model]["color"] + ) + self.change_linewidth = True + self.dsbx_linewidth.setValue(self.dict_flighttrack[wp_model]["linewidth"]) + else: + self.labelStatus.setText("Status: No flight track selected") + elif self.list_operation_track.currentItem() is not None: + self.operations.set_linewidth() + else: + self.labelStatus.setText("Status: No flight track selected") + + def flighttrackRemoved(self, parent, start, end): + """ + Slot to remove flighttrack. + """ + # ToDo: Add try..catch block + if self.dict_flighttrack[self.list_flighttrack.item(start).flighttrack_model]["patch"] is None: + del self.dict_flighttrack[self.list_flighttrack.item(start).flighttrack_model] + else: + self.dict_flighttrack[self.list_flighttrack.item(start).flighttrack_model]["patch"].remove() + self.list_flighttrack.takeItem(start) + + def update_last_flighttrack(self): + """ + Update waypoint model for most recently activated flighttrack in dict_flighttrack. + """ + if self.active_flight_track is not None: + self.save_waypoint_model_data( + self.active_flight_track, + self.list_flighttrack) # Before activating new flighttrack, update waypoints of previous flighttrack + + def activate_flighttrack(self): + """ + Activate flighttrack + """ + font = QtGui.QFont() + for i in range(self.list_flighttrack.count()): + listItem = self.list_flighttrack.item(i) + if self.active_flight_track == listItem.flighttrack_model: # active flighttrack + listItem.setIcon(self.show_color_icon(self.color)) + font.setBold(True) + if self.dict_flighttrack[listItem.flighttrack_model]["patch"] is not None: + self.dict_flighttrack[listItem.flighttrack_model]["patch"].remove() + if listItem.checkState() == QtCore.Qt.Unchecked: + listItem.setCheckState(QtCore.Qt.Checked) + self.set_activate_flag() + listItem.setFlags(listItem.flags() & ~QtCore.Qt.ItemIsUserCheckable) # make activated track uncheckable + else: + listItem.setIcon(self.show_color_icon(self.get_color(listItem.flighttrack_model))) + font.setBold(False) + listItem.setFlags(listItem.flags() | QtCore.Qt.ItemIsUserCheckable) + self.set_activate_flag() + listItem.setFont(font) + + def drawInactiveFlighttracks(self, list_widget): + """ + Draw inactive flighttracks + """ + for entry in self.dict_flighttrack.values(): + if entry["patch"] is not None: + entry["patch"].remove() + + for index in range(list_widget.count()): + listItem = list_widget.item(index) + if hasattr(list_widget.item(index), "checkState") and ( + list_widget.item(index).checkState() == QtCore.Qt.Checked): + if listItem.flighttrack_model != self.active_flight_track: + patch = MultipleFlightpath(self.view.map, + self.dict_flighttrack[listItem.flighttrack_model][ + "wp_data"], + color=self.dict_flighttrack[listItem.flighttrack_model]['color']) + + self.dict_flighttrack[listItem.flighttrack_model]["patch"] = patch + self.dict_flighttrack[listItem.flighttrack_model]["checkState"] = True + else: + # pass + self.dict_flighttrack[listItem.flighttrack_model]["checkState"] = False + + def set_activate_flag(self): + if not self.flighttrack_added: + self.flighttrack_activated = True + + def deactivate_all_flighttracks(self): + """ + Remove all flighttrack patches from topview canvas and make flighttracks userCheckable. + """ + for index in range(self.list_flighttrack.count()): + listItem = self.list_flighttrack.item(index) + + self.set_listControl(True, False) + + self.set_activate_flag() + listItem.setFlags(listItem.flags() | QtCore.Qt.ItemIsUserCheckable) + + if listItem.flighttrack_model == self.active_flight_track: + font = QtGui.QFont() + font.setBold(False) + listItem.setFont(font) + + self.active_flight_track = None + + def set_listControl(self, operation, flighttrack): + self.operation_list = operation + self.flighttrack_list = flighttrack + + def get_ft_vertices_color(self): + return self.color + + def listFlighttrack_itemClicked(self): + if self.list_operation_track.currentItem() is not None: + self.list_operation_track.setCurrentItem(None) + + if self.list_flighttrack.currentItem() is not None: + wp_model = self.list_flighttrack.currentItem().flighttrack_model + self.dsbx_linewidth.setValue(self.dict_flighttrack[wp_model]["linewidth"]) + + if self.list_flighttrack.currentItem().flighttrack_model == self.active_flight_track: + self.frame.hide() + else: + self.frame.show() + + +class MultipleFlightpathOperations: + """ + This class provides the functions for plotting Multiple Flighttracks from mscolab server + on the TopView canvas. + """ + + def __init__(self, parent, mscolab_server_url, token, list_operation_track, listOperationsMSC, view): + # Variables related to Mscolab Operations + self.parent = parent + self.active_op_id = None + self.mscolab_server_url = mscolab_server_url + self.token = token + self.view = view + self.dict_operations = {} + self.list_operation_track = list_operation_track + self.listOperationsMSC = listOperationsMSC + + self.operation_added = False + self.operation_removed = False + self.operation_activated = False + self.color_change = False + + # Connect signals and slots + self.list_operation_track.itemChanged.connect(self.set_flag) + + # Load operations from wps server + server_operations = self.get_wps_from_server() + sorted_server_operations = sorted(server_operations, key=lambda d: d["path"]) + + for operations in sorted_server_operations: + op_id = operations["op_id"] + xml_content = self.request_wps_from_server(op_id) + wp_model = ft.WaypointsTableModel(xml_content=xml_content) + wp_model.name = operations["path"] + self.create_operation(op_id, wp_model) + + def set_flag(self): + if self.operation_added: + self.operation_added = False + elif self.operation_removed: + self.operation_removed = False + elif self.color_change: + self.color_change = False + else: + self.draw_inactive_operations() + + def get_wps_from_server(self): + operations = {} + data = { + "token": self.token + } + r = requests.get(self.mscolab_server_url + "/operations", data=data) + if r.text != "False": + _json = json.loads(r.text) + operations = _json["operations"] + return operations + + def request_wps_from_server(self, op_id): + if verify_user_token(self.mscolab_server_url, self.token): + data = { + "token": self.token, + "op_id": op_id + } + r = requests.get(self.mscolab_server_url + '/get_operation_by_id', data=data) + if r.text != "False": + xml_content = json.loads(r.text)["content"] + return xml_content + + def load_wps_from_server(self, op_id): + xml_content = self.request_wps_from_server(op_id) + if xml_content is not None: + waypoints_model = ft.WaypointsTableModel(xml_content=xml_content) + return waypoints_model + + def save_operation_data(self, op_id, wp_model): + wp_data = [(wp.lat, wp.lon, wp.flightlevel, wp.location, wp.comments) for wp in wp_model.all_waypoint_data()] + if self.dict_operations[op_id] is None: + self.create_operation(op_id, wp_model) + self.dict_operations[op_id]["wp_data"] = wp_data + + def create_operation(self, op_id, wp_model): + """ + """ + self.dict_operations[op_id] = {} + self.dict_operations[op_id]["patch"] = None + self.dict_operations[op_id]["wp_data"] = None + self.dict_operations[op_id]["linewidth"] = 2.0 + self.dict_operations[op_id]["color"] = self.parent.get_ft_vertices_color() + + self.save_operation_data(op_id, wp_model) + + listItem = QMscolabOperationsListWidgetItem(wp_model, op_id, self.list_operation_track) + listItem.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsUserCheckable) + + if not self.operation_added: + self.operation_added = True + listItem.setCheckState(QtCore.Qt.Unchecked) + if not self.operation_added: + self.operation_added = True + + # Show operations color icon + listItem.setIcon(self.show_color_icon(self.get_color(op_id))) + + return listItem + + def activate_operation(self): + """ + Activate Mscolab Operation + """ + font = QtGui.QFont() + for i in range(self.list_operation_track.count()): + listItem = self.list_operation_track.item(i) + if self.active_op_id == listItem.op_id: # active operation + listItem.setIcon(self.show_color_icon(self.parent.color)) + font.setBold(True) + if self.dict_operations[listItem.op_id]["patch"] is not None: + self.dict_operations[listItem.op_id]["patch"].remove() + if listItem.checkState() == QtCore.Qt.Unchecked: + listItem.setCheckState(QtCore.Qt.Checked) + self.set_activate_flag() + listItem.setFlags(listItem.flags() & ~QtCore.Qt.ItemIsUserCheckable) # make activated track uncheckable + else: + listItem.setIcon(self.show_color_icon(self.get_color(listItem.op_id))) + font.setBold(False) + listItem.setFlags(listItem.flags() | QtCore.Qt.ItemIsUserCheckable) + self.set_activate_flag() + listItem.setFont(font) + + def save_last_used_operation(self, op_id): + if self.active_op_id is not None: + self.save_operation_data(op_id, self.load_wps_from_server(self.active_op_id)) + + def draw_inactive_operations(self): + """ + Draw flighttracks of inactive operations. + """ + for entry in self.dict_operations.values(): + if entry is not None: + if entry["patch"] is not None: + entry["patch"].remove() + + for index in range(self.list_operation_track.count()): + listItem = self.list_operation_track.item(index) + if hasattr(listItem, "checkState") and ( + listItem.checkState() == QtCore.Qt.Checked): + if listItem.op_id != self.active_op_id: + patch = MultipleFlightpath(self.view.map, + self.dict_operations[listItem.op_id][ + "wp_data"], + color=self.dict_operations[listItem.op_id]["color"], + linewidth=self.dict_operations[listItem.op_id]["linewidth"]) + + self.dict_operations[listItem.op_id]["patch"] = patch + + def get_op_id(self, op_id): + if self.active_op_id is not None: + tmp = self.active_op_id + self.save_last_used_operation(tmp) + self.active_op_id = op_id + self.activate_operation() + + def operationsAdded(self, op_id, path): + """ + Slot to add operation. + """ + wp_model = self.load_wps_from_server(op_id) + wp_model.name = path + self.create_operation(op_id, wp_model) + + def operationRemoved(self, op_id): + """ + Slot to remove operation. + """ + self.operation_removed = True + for index in range(self.list_operation_track.count()): + if self.list_operation_track.item(index).op_id == op_id: + if self.dict_operations[self.list_operation_track.item(index).op_id]["patch"] is None: + del self.dict_operations[self.list_operation_track.item(index).op_id] + else: + self.dict_operations[self.list_operation_track.item(index).op_id]["patch"].remove() + self.list_operation_track.takeItem(index) + self.active_op_id = None + break + + def set_activate_flag(self): + if not self.operation_activated: + self.operation_activated = True + + def deactivate_all_operations(self): + """ + Removes all operations patches from topview canvas and make flighttracks userCheckable + """ + for index in range(self.listOperationsMSC.count()): + listItem = self.list_operation_track.item(index) + + self.parent.set_listControl(False, True) + + self.set_activate_flag() + listItem.setFlags(listItem.flags() | QtCore.Qt.ItemIsUserCheckable) + + # if listItem.op_id == self.active_op_id: + self.set_activate_flag() + font = QtGui.QFont() + font.setBold(False) + listItem.setFont(font) + + self.active_op_id = None + + def select_color(self): + """ + Sets the color of selected operation when change Color is clicked. + """ + if self.list_operation_track.currentItem() is not None: + if (hasattr(self.list_operation_track.currentItem(), "checkState")) and ( + self.list_operation_track.currentItem().checkState() == QtCore.Qt.Checked): + op_id = self.list_operation_track.currentItem().op_id + if self.list_operation_track.currentItem().op_id == self.active_op_id: + self.error_dialog = QtWidgets.QErrorMessage() + self.error_dialog.showMessage('Use "options" to change color of an activated operation.') + else: + color = QtWidgets.QColorDialog.getColor() + if color.isValid(): + self.dict_operations[op_id]["color"] = color.getRgbF() + self.color_change = True + self.list_operation_track.currentItem().setIcon(self.show_color_icon(self.get_color(op_id))) + self.dict_operations[op_id]["patch"].update( + color=self.dict_operations[op_id]["color"], + linewidth=self.dict_operations[op_id]["linewidth"]) + else: + self.parent.labelStatus.setText("Check Mark the Operation to change color.") + + def get_color(self, op_id): + """ + Returns color of respective operation. + """ + return self.dict_operations[op_id]["color"] + + def show_color_icon(self, clr): + """ + """ + pixmap = self.parent.show_color_pixmap(clr) + return QtGui.QIcon(pixmap) + + def ft_color_update(self, color): + self.color = color + self.dict_operations[self.active_op_id]["color"] = color + + for index in range(self.list_operation_track.count()): + if self.list_operation_track.item(index).op_id == self.active_op_id: + self.list_operation_track.item(index).setIcon( + self.show_color_icon(self.get_color(self.active_op_id))) + break + + def logout_mscolab(self): + a = self.list_operation_track.count() - 1 + while a >= 0: + if self.dict_operations[self.list_operation_track.item(0).op_id]['patch'] is None: + del self.dict_operations[self.list_operation_track.item(0).op_id] + else: + self.dict_operations[self.list_operation_track.item(0).op_id]['patch'].remove() + self.list_operation_track.takeItem(0) + a -= 1 + + self.list_operation_track.itemChanged.disconnect() + self.mscolab_server_url = None + self.token = None + self.dict_operations = {} + + @QtCore.Slot(int) + def permission_revoked(self, op_id): + self.operationRemoved(op_id) + + @QtCore.Slot(int, str) + def render_permission(self, op_id, path): + self.operationsAdded(op_id, path) + + def set_linewidth(self): + if (hasattr(self.list_operation_track.currentItem(), "checkState")) and ( + self.list_operation_track.currentItem().checkState() == QtCore.Qt.Checked): + op_id = self.list_operation_track.currentItem().op_id + if op_id != self.active_op_id: + self.parent.frame.show() + if self.dict_operations[op_id]["linewidth"] != self.parent.dsbx_linewidth.value(): + self.dict_operations[op_id]["linewidth"] = self.parent.dsbx_linewidth.value() + + self.dict_operations[op_id]["patch"].remove() + self.dict_operations[op_id]["patch"].update( + self.dict_operations[op_id]["linewidth"], self.dict_operations[op_id]["color"] + ) + self.change_linewidth = True + self.parent.dsbx_linewidth.setValue(self.dict_operations[op_id]["linewidth"]) + + def listOperations_itemClicked(self): + if self.parent.list_flighttrack.currentItem() is not None: + self.parent.list_flighttrack.setCurrentItem(None) + + if self.list_operation_track.currentItem() is not None: + op_id = self.list_operation_track.currentItem().op_id + self.parent.dsbx_linewidth.setValue(self.dict_operations[op_id]["linewidth"]) + + if self.list_operation_track.currentItem().op_id == self.active_op_id: + self.parent.frame.hide() + else: + self.parent.frame.show() diff --git a/mslib/msui/qt5/ui_mainwindow.py b/mslib/msui/qt5/ui_mainwindow.py index 36a142a04..913f64d12 100644 --- a/mslib/msui/qt5/ui_mainwindow.py +++ b/mslib/msui/qt5/ui_mainwindow.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file 'ui_mainwindow.ui' +# Form implementation generated from reading ui file 'mslib/msui/ui/ui_mainwindow.ui' # # Created by: PyQt5 UI code generator 5.12.3 # @@ -72,34 +72,40 @@ def setupUi(self, MSUIMainWindow): self.gridLayout_3 = QtWidgets.QGridLayout(self.openOperationsGb) self.gridLayout_3.setContentsMargins(8, 8, 8, 8) self.gridLayout_3.setObjectName("gridLayout_3") + self.categoryLabel = QtWidgets.QLabel(self.openOperationsGb) + self.categoryLabel.setObjectName("categoryLabel") + self.gridLayout_3.addWidget(self.categoryLabel, 7, 0, 1, 1) + self.listInactiveOperationsMSC = QtWidgets.QListWidget(self.openOperationsGb) + self.listInactiveOperationsMSC.setObjectName("listInactiveOperationsMSC") + self.gridLayout_3.addWidget(self.listInactiveOperationsMSC, 3, 0, 1, 2) self.workingStatusLabel = QtWidgets.QLabel(self.openOperationsGb) self.workingStatusLabel.setWordWrap(True) self.workingStatusLabel.setObjectName("workingStatusLabel") - self.gridLayout_3.addWidget(self.workingStatusLabel, 2, 0, 1, 2) - self.listOperationsMSC = QtWidgets.QListWidget(self.openOperationsGb) - self.listOperationsMSC.setObjectName("listOperationsMSC") - self.gridLayout_3.addWidget(self.listOperationsMSC, 1, 0, 1, 2) + self.gridLayout_3.addWidget(self.workingStatusLabel, 4, 0, 1, 2) + self.openOperationsMSCLabel = QtWidgets.QLabel(self.openOperationsGb) + self.openOperationsMSCLabel.setObjectName("openOperationsMSCLabel") + self.gridLayout_3.addWidget(self.openOperationsMSCLabel, 0, 0, 1, 1) self.serverOptionsCb = QtWidgets.QComboBox(self.openOperationsGb) self.serverOptionsCb.setObjectName("serverOptionsCb") self.serverOptionsCb.addItem("") self.serverOptionsCb.addItem("") self.serverOptionsCb.addItem("") - self.gridLayout_3.addWidget(self.serverOptionsCb, 6, 1, 1, 1) - self.openOperationsMSCLabel = QtWidgets.QLabel(self.openOperationsGb) - self.openOperationsMSCLabel.setObjectName("openOperationsMSCLabel") - self.gridLayout_3.addWidget(self.openOperationsMSCLabel, 0, 0, 1, 1) - self.categoryLabel = QtWidgets.QLabel(self.openOperationsGb) - self.categoryLabel.setObjectName("categoryLabel") - self.gridLayout_3.addWidget(self.categoryLabel, 5, 0, 1, 1) + self.gridLayout_3.addWidget(self.serverOptionsCb, 8, 1, 1, 1) self.workLocallyCheckbox = QtWidgets.QCheckBox(self.openOperationsGb) self.workLocallyCheckbox.setObjectName("workLocallyCheckbox") - self.gridLayout_3.addWidget(self.workLocallyCheckbox, 6, 0, 1, 1) + self.gridLayout_3.addWidget(self.workLocallyCheckbox, 8, 0, 1, 1) self.filterCategoryCb = QtWidgets.QComboBox(self.openOperationsGb) self.filterCategoryCb.setAutoFillBackground(False) self.filterCategoryCb.setEditable(False) self.filterCategoryCb.setObjectName("filterCategoryCb") self.filterCategoryCb.addItem("") - self.gridLayout_3.addWidget(self.filterCategoryCb, 5, 1, 1, 1) + self.gridLayout_3.addWidget(self.filterCategoryCb, 7, 1, 1, 1) + self.listOperationsMSC = QtWidgets.QListWidget(self.openOperationsGb) + self.listOperationsMSC.setObjectName("listOperationsMSC") + self.gridLayout_3.addWidget(self.listOperationsMSC, 1, 0, 1, 2) + self.label = QtWidgets.QLabel(self.openOperationsGb) + self.label.setObjectName("label") + self.gridLayout_3.addWidget(self.label, 2, 0, 1, 1) self.gridLayout.addWidget(self.openOperationsGb, 2, 1, 2, 1) self.activeOperationDesc = QtWidgets.QLabel(self.centralwidget) self.activeOperationDesc.setObjectName("activeOperationDesc") @@ -183,6 +189,8 @@ def setupUi(self, MSUIMainWindow): self.actionRenameOperation.setObjectName("actionRenameOperation") self.actionLeaveOperation = QtWidgets.QAction(MSUIMainWindow) self.actionLeaveOperation.setObjectName("actionLeaveOperation") + self.actionActivateOperation = QtWidgets.QAction(MSUIMainWindow) + self.actionActivateOperation.setObjectName("actionActivateOperation") self.menuNew.addAction(self.actionNewFlightTrack) self.menuNew.addAction(self.actionAddOperation) self.menuFile.addAction(self.menuNew.menuAction()) @@ -216,6 +224,7 @@ def setupUi(self, MSUIMainWindow): self.menuOperation.addAction(self.actionChat) self.menuOperation.addAction(self.actionVersionHistory) self.menuOperation.addAction(self.actionManageUsers) + self.menuOperation.addAction(self.actionActivateOperation) self.menuOperation.addSeparator() self.menuOperation.addAction(self.menuProperties.menuAction()) self.menubar.addAction(self.menuFile.menuAction()) @@ -248,20 +257,21 @@ def retranslateUi(self, MSUIMainWindow): self.userOptionsTb.setToolTip(_translate("MSUIMainWindow", "Profile options")) self.connectBtn.setToolTip(_translate("MSUIMainWindow", "Connect to an MSColab Server")) self.connectBtn.setText(_translate("MSUIMainWindow", "Connect to MSColab")) + self.categoryLabel.setText(_translate("MSUIMainWindow", "Category:")) self.workingStatusLabel.setText(_translate("MSUIMainWindow", "No operations selected")) - self.listOperationsMSC.setToolTip(_translate("MSUIMainWindow", "List of mscolab operations.\n" -"Double click a operation to activate and view its description.")) + self.openOperationsMSCLabel.setText(_translate("MSUIMainWindow", "Active Operations:")) self.serverOptionsCb.setToolTip(_translate("MSUIMainWindow", "Fetch/Save Server options")) self.serverOptionsCb.setItemText(0, _translate("MSUIMainWindow", "Server Options")) self.serverOptionsCb.setItemText(1, _translate("MSUIMainWindow", "Fetch From Server")) self.serverOptionsCb.setItemText(2, _translate("MSUIMainWindow", "Save To Server")) - self.openOperationsMSCLabel.setText(_translate("MSUIMainWindow", "Operations:")) - self.categoryLabel.setText(_translate("MSUIMainWindow", "Category:")) self.workLocallyCheckbox.setToolTip(_translate("MSUIMainWindow", "Check to work asynchronously from the server")) self.workLocallyCheckbox.setText(_translate("MSUIMainWindow", "Work Asynchronously")) self.filterCategoryCb.setWhatsThis(_translate("MSUIMainWindow", "filter by operation category")) self.filterCategoryCb.setCurrentText(_translate("MSUIMainWindow", "ANY")) self.filterCategoryCb.setItemText(0, _translate("MSUIMainWindow", "ANY")) + self.listOperationsMSC.setToolTip(_translate("MSUIMainWindow", "List of mscolab operations.\n" +"Double click a operation to activate and view its description.")) + self.label.setText(_translate("MSUIMainWindow", "Inactive Operations:")) self.activeOperationDesc.setText(_translate("MSUIMainWindow", "Select Operation to View Description.")) self.menuFile.setTitle(_translate("MSUIMainWindow", "&File")) self.menuImportFlightTrack.setTitle(_translate("MSUIMainWindow", "Import Flight Track")) @@ -309,3 +319,4 @@ def retranslateUi(self, MSUIMainWindow): self.actionUpdateOperationDesc.setText(_translate("MSUIMainWindow", "Update Description")) self.actionRenameOperation.setText(_translate("MSUIMainWindow", "Rename Operation")) self.actionLeaveOperation.setText(_translate("MSUIMainWindow", "&Leave Operation")) + self.actionActivateOperation.setText(_translate("MSUIMainWindow", "Activate Operation")) diff --git a/mslib/msui/qt5/ui_multiple_flightpath_dockwidget.py b/mslib/msui/qt5/ui_multiple_flightpath_dockwidget.py new file mode 100644 index 000000000..ec9fda2e4 --- /dev/null +++ b/mslib/msui/qt5/ui_multiple_flightpath_dockwidget.py @@ -0,0 +1,122 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'mslib/msui/ui/ui_multiple_flightpath_dockwidget.ui' +# +# Created by: PyQt5 UI code generator 5.12.3 +# +# WARNING! All changes made in this file will be lost! + + +from PyQt5 import QtCore, QtGui, QtWidgets + + +class Ui_MultipleViewWidget(object): + def setupUi(self, MultipleViewWidget): + MultipleViewWidget.setObjectName("MultipleViewWidget") + MultipleViewWidget.resize(778, 235) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Minimum) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(MultipleViewWidget.sizePolicy().hasHeightForWidth()) + MultipleViewWidget.setSizePolicy(sizePolicy) + self.verticalLayout_2 = QtWidgets.QVBoxLayout(MultipleViewWidget) + self.verticalLayout_2.setObjectName("verticalLayout_2") + self.frame_2 = QtWidgets.QFrame(MultipleViewWidget) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.frame_2.sizePolicy().hasHeightForWidth()) + self.frame_2.setSizePolicy(sizePolicy) + self.frame_2.setFrameShape(QtWidgets.QFrame.StyledPanel) + self.frame_2.setFrameShadow(QtWidgets.QFrame.Raised) + self.frame_2.setObjectName("frame_2") + self.horizontalLayout_3 = QtWidgets.QHBoxLayout(self.frame_2) + self.horizontalLayout_3.setObjectName("horizontalLayout_3") + self.ft_color_label = QtWidgets.QLabel(self.frame_2) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.ft_color_label.sizePolicy().hasHeightForWidth()) + self.ft_color_label.setSizePolicy(sizePolicy) + self.ft_color_label.setObjectName("ft_color_label") + self.horizontalLayout_3.addWidget(self.ft_color_label) + self.colorPixmap = QtWidgets.QLabel(self.frame_2) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.colorPixmap.sizePolicy().hasHeightForWidth()) + self.colorPixmap.setSizePolicy(sizePolicy) + self.colorPixmap.setText("") + self.colorPixmap.setObjectName("colorPixmap") + self.horizontalLayout_3.addWidget(self.colorPixmap) + self.verticalLayout_2.addWidget(self.frame_2) + self.horizontalLayout = QtWidgets.QHBoxLayout() + self.horizontalLayout.setObjectName("horizontalLayout") + self.horizontalLayout_2 = QtWidgets.QHBoxLayout() + self.horizontalLayout_2.setObjectName("horizontalLayout_2") + self.list_flighttrack = QtWidgets.QListWidget(MultipleViewWidget) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.MinimumExpanding) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.list_flighttrack.sizePolicy().hasHeightForWidth()) + self.list_flighttrack.setSizePolicy(sizePolicy) + self.list_flighttrack.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContents) + self.list_flighttrack.setObjectName("list_flighttrack") + self.horizontalLayout_2.addWidget(self.list_flighttrack) + self.list_operation_track = QtWidgets.QListWidget(MultipleViewWidget) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.MinimumExpanding) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.list_operation_track.sizePolicy().hasHeightForWidth()) + self.list_operation_track.setSizePolicy(sizePolicy) + self.list_operation_track.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContents) + self.list_operation_track.setObjectName("list_operation_track") + self.horizontalLayout_2.addWidget(self.list_operation_track) + self.frame = QtWidgets.QFrame(MultipleViewWidget) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.frame.sizePolicy().hasHeightForWidth()) + self.frame.setSizePolicy(sizePolicy) + self.frame.setFrameShape(QtWidgets.QFrame.StyledPanel) + self.frame.setFrameShadow(QtWidgets.QFrame.Raised) + self.frame.setObjectName("frame") + self.verticalLayout = QtWidgets.QVBoxLayout(self.frame) + self.verticalLayout.setObjectName("verticalLayout") + self.pushButton_color = QtWidgets.QPushButton(self.frame) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.pushButton_color.sizePolicy().hasHeightForWidth()) + self.pushButton_color.setSizePolicy(sizePolicy) + self.pushButton_color.setObjectName("pushButton_color") + self.verticalLayout.addWidget(self.pushButton_color) + self.dsbx_linewidth = QtWidgets.QDoubleSpinBox(self.frame) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.dsbx_linewidth.sizePolicy().hasHeightForWidth()) + self.dsbx_linewidth.setSizePolicy(sizePolicy) + self.dsbx_linewidth.setAlignment(QtCore.Qt.AlignCenter) + self.dsbx_linewidth.setObjectName("dsbx_linewidth") + self.verticalLayout.addWidget(self.dsbx_linewidth) + self.horizontalLayout_2.addWidget(self.frame) + self.horizontalLayout.addLayout(self.horizontalLayout_2) + self.verticalLayout_2.addLayout(self.horizontalLayout) + self.labelStatus = QtWidgets.QLabel(MultipleViewWidget) + self.labelStatus.setObjectName("labelStatus") + self.verticalLayout_2.addWidget(self.labelStatus) + + self.retranslateUi(MultipleViewWidget) + QtCore.QMetaObject.connectSlotsByName(MultipleViewWidget) + + def retranslateUi(self, MultipleViewWidget): + _translate = QtCore.QCoreApplication.translate + MultipleViewWidget.setWindowTitle(_translate("MultipleViewWidget", "Form")) + self.ft_color_label.setText(_translate("MultipleViewWidget", "Activated Flighttrack/Operation Vertices Color: ")) + self.list_flighttrack.setToolTip(_translate("MultipleViewWidget", "List of Open Flighttracks.\n" +"Check box to activate and display track on topview.")) + self.list_operation_track.setToolTip(_translate("MultipleViewWidget", "List of Mscolab Operations.\n" +"Check box to activate and display track on topview.")) + self.pushButton_color.setText(_translate("MultipleViewWidget", "Change Color")) + self.labelStatus.setText(_translate("MultipleViewWidget", "Status: ")) diff --git a/mslib/msui/socket_control.py b/mslib/msui/socket_control.py index e5a47be92..3988313bb 100644 --- a/mslib/msui/socket_control.py +++ b/mslib/msui/socket_control.py @@ -59,7 +59,7 @@ def __init__(self, token, user, mscolab_server_url=mss_default.mscolab_server_ur logging.debug("Transport Layer: %s", self.sio.transport()) self.sio.on('file-changed', handler=self.handle_file_change) - # on chat message recive + # on chat message receive 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 diff --git a/mslib/msui/topview.py b/mslib/msui/topview.py index dc142c99d..0775e796d 100644 --- a/mslib/msui/topview.py +++ b/mslib/msui/topview.py @@ -41,6 +41,8 @@ from mslib.msui import remotesensing_dockwidget as rs from mslib.msui import kmloverlay_dockwidget as kml from mslib.msui import airdata_dockwidget as ad +from mslib.msui import multiple_flightpath_dockwidget as mf +from mslib.msui import flighttrack as ft from mslib.msui.icons import icons from mslib.msui.flighttrack import Waypoint @@ -50,6 +52,7 @@ REMOTESENSING = 2 KMLOVERLAY = 3 AIRDATA = 4 +MULTIPLEFLIGHTPATH = 5 class MSUI_TV_MapAppearanceDialog(QtWidgets.QDialog, ui_ma.Ui_MapAppearanceDialog): @@ -57,6 +60,7 @@ class MSUI_TV_MapAppearanceDialog(QtWidgets.QDialog, ui_ma.Ui_MapAppearanceDialo Dialog to set map appearance parameters. User interface is defined in "ui_topview_mapappearance.py". """ + signal_ft_vertices_color_change = QtCore.Signal(str, tuple) def __init__(self, parent=None, settings_dict=None, wms_connected=False): """ @@ -173,6 +177,7 @@ def setColour(self, which): colour = palette.color(QtGui.QPalette.Button) colour = QtWidgets.QColorDialog.getColor(colour) if colour.isValid(): + self.signal_ft_vertices_color_change.emit(which, colour.getRgbF()) palette.setColor(QtGui.QPalette.Button, colour) button.setPalette(palette) @@ -184,17 +189,29 @@ class MSUITopViewWindow(MSUIMplViewWindow, ui.Ui_TopViewWindow): """ name = "Top View" - def __init__(self, parent=None, model=None, _id=None): + signal_activate_flighttrack1 = QtCore.Signal(ft.WaypointsTableModel) + signal_activate_operation = QtCore.Signal(int) + signal_ft_vertices_color_change = QtCore.Signal(tuple) + signal_operation_added = QtCore.Signal(int, str) + signal_operation_removed = QtCore.Signal(int) + signal_login_mscolab = QtCore.Signal(str, str) + signal_logout_mscolab = QtCore.Signal() + signal_listFlighttrack_doubleClicked = QtCore.Signal() + signal_permission_revoked = QtCore.Signal(int) + signal_render_new_permission = QtCore.Signal(int, str) + + def __init__(self, parent=None, model=None, _id=None, active_flighttrack=None, mscolab_server_url=None, token=None): """ Set up user interface, connect signal/slots. """ super(MSUITopViewWindow, self).__init__(parent, model, _id) logging.debug(_id) + self.ui = parent self.setupUi(self) self.setWindowIcon(QtGui.QIcon(icons('64x64'))) - # Dock windows [WMS, Satellite, Trajectories, Remote Sensing, KML Overlay]: - self.docks = [None, None, None, None, None] + # Dock windows [WMS, Satellite, Trajectories, Remote Sensing, KML Overlay, Multiple Flightpath]: + self.docks = [None, None, None, None, None, None] self.settings_tag = "topview" self.load_settings() @@ -205,6 +222,16 @@ def __init__(self, parent=None, model=None, _id=None): # Boolean to store active wms connection self.wms_connected = False + # Store active flighttrack waypoint model + self.active_flighttrack = active_flighttrack + + # Stores active mscolab operation id + self.active_op_id = None + + # Mscolab Server Url and token + self.mscolab_server_url = mscolab_server_url + self.token = token + # Connect slots and signals. # ========================== @@ -221,16 +248,53 @@ def __init__(self, parent=None, model=None, _id=None): # Tool opener. self.cbTools.currentIndexChanged.connect(self.openTool) + if parent is not None: + # Update flighttrack + self.ui.signal_activate_flighttrack.connect(self.update_active_flighttrack) + self.ui.signal_activate_operation.connect(self.update_active_operation) + + self.ui.signal_operation_added.connect(self.add_operation_slot) + self.ui.signal_operation_removed.connect(self.remove_operation_slot) + + self.ui.signal_login_mscolab.connect(self.login) + def __del__(self): del self.mpl.canvas.waypoints_interactor + @QtCore.Slot(ft.WaypointsTableModel) + def update_active_flighttrack(self, active_flighttrack): + """ + Slot that handles update of active flighttrack variable. + """ + self.active_flighttrack = active_flighttrack + self.signal_activate_flighttrack1.emit(active_flighttrack) + + @QtCore.Slot(int) + def update_active_operation(self, active_op_id): + self.active_op_id = active_op_id + self.signal_activate_operation.emit(self.active_op_id) + + @QtCore.Slot(int, str) + def add_operation_slot(self, op_id, path): + self.signal_operation_added.emit(op_id, path) + + @QtCore.Slot(int) + def remove_operation_slot(self, op_id): + self.signal_operation_removed.emit(op_id) + + @QtCore.Slot(str, str) + def login(self, mscolab_server_url, token): + self.mscolab_server_url = mscolab_server_url + self.token = token + self.signal_login_mscolab.emit(mscolab_server_url, token) + def setup_top_view(self): """ Initialise GUI elements. (This method is called before signals/slots are connected). """ toolitems = ["(select to open control)", "Web Map Service", "Satellite Tracks", "Remote Sensing", "KML Overlay", - "Airports/Airspaces"] + "Airports/Airspaces", "Multiple FLightpath"] self.cbTools.clear() self.cbTools.addItems(toolitems) @@ -290,12 +354,38 @@ def openTool(self, index): elif index == AIRDATA: title = "Airdata" widget = ad.AirdataDockwidget(parent=self, view=self.mpl.canvas) + elif index == MULTIPLEFLIGHTPATH: + title = "Multiple Flightpath" + widget = mf.MultipleFlightpathControlWidget(parent=self, view=self.mpl.canvas, + listFlightTracks=self.ui.listFlightTracks, + listOperationsMSC=self.ui.listOperationsMSC, + activeFlightTrack=self.active_flighttrack, + mscolab_server_url=self.mscolab_server_url, + token=self.token) + + self.ui.signal_logout_mscolab.connect(lambda: self.signal_logout_mscolab.emit()) + self.ui.signal_listFlighttrack_doubleClicked.connect( + lambda: self.signal_listFlighttrack_doubleClicked.emit()) + self.ui.signal_permission_revoked.connect(lambda op_id: self.signal_permission_revoked.emit(op_id)) + self.ui.signal_render_new_permission.connect( + lambda op_id, path: self.signal_render_new_permission.emit(op_id, path)) + if self.active_op_id is not None: + self.signal_activate_operation.emit(self.active_op_id) + widget.signal_parent_closes.connect(self.closed) else: raise IndexError("invalid control index") # Create the actual dock widget containing . self.createDockWidget(index, title, widget) + def closed(self): + self.ui.signal_login_mscolab.disconnect() + self.ui.signal_logout_mscolab.disconnect() + self.ui.signal_listFlighttrack_doubleClicked.disconnect() + self.ui.signal_activate_operation.disconnect() + self.ui.signal_permission_revoked.disconnect() + self.ui.signal_render_new_permission.disconnect() + @QtCore.Slot() def disable_cbs(self): self.wms_connected = True @@ -341,6 +431,7 @@ def settings_dialogue(self): settings = self.getView().get_map_appearance() dlg = MSUI_TV_MapAppearanceDialog(parent=self, settings_dict=settings, wms_connected=self.wms_connected) dlg.setModal(False) + dlg.signal_ft_vertices_color_change.connect(self.set_ft_vertices_color) if dlg.exec_() == QtWidgets.QDialog.Accepted: settings = dlg.get_settings() self.getView().set_map_appearance(settings) @@ -348,6 +439,11 @@ def settings_dialogue(self): self.mpl.canvas.waypoints_interactor.redraw_path() dlg.destroy() + @QtCore.Slot(str, tuple) + def set_ft_vertices_color(self, which, color): + if which == "ft_vertices": + self.signal_ft_vertices_color_change.emit(color) + def save_settings(self): """ Save the current settings (map appearance) to the file @@ -396,3 +492,6 @@ def is_roundtrip_possible(self): def update_roundtrip_enabled(self): self.btRoundtrip.setEnabled(self.is_roundtrip_possible()) + + def get_settings(self): + return load_settings_qsettings(self.settings_tag, {}) diff --git a/mslib/msui/ui/ui_mainwindow.ui b/mslib/msui/ui/ui_mainwindow.ui index 799e902ea..c1659ddfa 100644 --- a/mslib/msui/ui/ui_mainwindow.ui +++ b/mslib/msui/ui/ui_mainwindow.ui @@ -173,25 +173,7 @@ Save a flight track to name it. 8 - - - - No operations selected - - - true - - - - - - - List of mscolab operations. -Double click a operation to activate and view its description. - - - - + Fetch/Save Server options @@ -213,31 +195,7 @@ Double click a operation to activate and view its description. - - - - Operations: - - - - - - - Category: - - - - - - - Check to work asynchronously from the server - - - Work Asynchronously - - - - + filter by operation category @@ -261,6 +219,58 @@ Double click a operation to activate and view its description. + + + + Check to work asynchronously from the server + + + Work Asynchronously + + + + + + + + + + Category: + + + + + + + No operations selected + + + true + + + + + + + Active Operations: + + + + + + + List of mscolab operations. +Double click a operation to activate and view its description. + + + + + + + Inactive Operations: + + + @@ -279,7 +289,7 @@ Double click a operation to activate and view its description. 0 0 738 - 22 + 26 @@ -357,6 +367,7 @@ Double click a operation to activate and view its description. + @@ -532,6 +543,11 @@ Double click a operation to activate and view its description. &Leave Operation + + + Activate Operation + + connectBtn diff --git a/mslib/msui/ui/ui_multiple_flightpath_dockwidget.ui b/mslib/msui/ui/ui_multiple_flightpath_dockwidget.ui new file mode 100644 index 000000000..037a5b136 --- /dev/null +++ b/mslib/msui/ui/ui_multiple_flightpath_dockwidget.ui @@ -0,0 +1,164 @@ + + + MultipleViewWidget + + + + 0 + 0 + 778 + 235 + + + + + 0 + 0 + + + + Form + + + + + + + 0 + 0 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + 0 + 0 + + + + Activated Flighttrack/Operation Vertices Color: + + + + + + + + 0 + 0 + + + + + + + + + + + + + + + + + + + 0 + 0 + + + + List of Open Flighttracks. +Check box to activate and display track on topview. + + + QAbstractScrollArea::AdjustToContents + + + + + + + + 0 + 0 + + + + List of Mscolab Operations. +Check box to activate and display track on topview. + + + QAbstractScrollArea::AdjustToContents + + + + + + + + 0 + 0 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + 0 + 0 + + + + Change Color + + + + + + + + 0 + 0 + + + + Qt::AlignCenter + + + + + + + + + + + + + + Status: + + + + + + + + diff --git a/mslib/utils/qt.py b/mslib/utils/qt.py index 272cc38f5..a947c84e6 100644 --- a/mslib/utils/qt.py +++ b/mslib/utils/qt.py @@ -372,7 +372,7 @@ class Worker(QtCore.QThread): Can be used to run a function through a QThread without much struggle, and receive the return value or exception through signals. Beware not to modify the parents connections through the function. - You may change the GUI but it may sometimes not update until the Worker is done. + You may change the GUI, but it may sometimes not update until the Worker is done. """ # Static set of all workers to avoid segfaults workers = set() diff --git a/tests/_test_mscolab/test_file_manager.py b/tests/_test_mscolab/test_file_manager.py index 088d16aa9..5fc059095 100644 --- a/tests/_test_mscolab/test_file_manager.py +++ b/tests/_test_mscolab/test_file_manager.py @@ -102,11 +102,13 @@ def test_list_operations(self): self.fm.create_operation("first", "info about first", self.user) self.fm.create_operation("second", "info about second", self.user) expected_result = [{'access_level': 'creator', + "active": True, 'category': 'default', 'description': 'info about first', 'op_id': 1, 'path': 'first'}, {'access_level': 'creator', + "active": True, 'category': 'default', 'description': 'info about second', 'op_id': 2, diff --git a/tests/_test_mscolab/test_seed.py b/tests/_test_mscolab/test_seed.py index d73e6dd48..d790c0190 100644 --- a/tests/_test_mscolab/test_seed.py +++ b/tests/_test_mscolab/test_seed.py @@ -88,7 +88,7 @@ def test_add_all_users_default_operation_viewer(self): # viewer add_all_users_default_operation(path='XYZ', description="Operation to keep all users", access_level='viewer') - expected_result = [{'access_level': 'viewer', 'category': 'default', + expected_result = [{'access_level': 'viewer', 'active': True, 'category': 'default', 'description': 'Template', 'op_id': 7, 'path': 'XYZ'}] user = User.query.filter_by(emailid=self.userdata_1[0]).first() assert user is not None @@ -103,7 +103,7 @@ def test_add_all_users_default_operation_collaborator(self): assert add_user(self.userdata_1[0], self.userdata_1[1], self.userdata_1[2]) add_all_users_default_operation(path='XYZ', description="Operation to keep all users", access_level='collaborator') - expected_result = [{'access_level': 'collaborator', 'category': 'default', + expected_result = [{'access_level': 'collaborator', 'active': True, 'category': 'default', 'description': 'Template', 'op_id': 7, 'path': 'XYZ'}] user = User.query.filter_by(emailid=self.userdata_1[0]).first() assert user is not None @@ -118,7 +118,7 @@ def test_add_all_users_default_operation_creator(self): # creator add_all_users_default_operation(path='XYZ', description="Operation to keep all users", access_level='creator') - expected_result = [{'access_level': 'creator', 'category': 'default', + expected_result = [{'access_level': 'creator', 'active': True, 'category': 'default', 'description': 'Template', 'op_id': 7, 'path': 'XYZ'}] user = User.query.filter_by(emailid=self.userdata_1[0]).first() result = self.fm.list_operations(user) @@ -132,7 +132,7 @@ def test_add_all_users_default_operation_creator_unknown_operation(self): # creator added to new operation add_all_users_default_operation(path='UVXYZ', description="Operation to keep all users", access_level='creator') - expected_result = [{'access_level': 'creator', 'category': 'default', + expected_result = [{'access_level': 'creator', 'active': True, 'category': 'default', 'description': 'Operation to keep all users', 'op_id': 7, 'path': 'UVXYZ'}] user = User.query.filter_by(emailid=self.userdata_1[0]).first() diff --git a/tests/_test_mscolab/test_server.py b/tests/_test_mscolab/test_server.py index 5aaf8ff58..fb52928e2 100644 --- a/tests/_test_mscolab/test_server.py +++ b/tests/_test_mscolab/test_server.py @@ -320,6 +320,25 @@ def test_get_operation_details(self): data = json.loads(response.data.decode('utf-8')) assert data["path"] == path + def test_set_last_used(self): + assert add_user(self.userdata[0], self.userdata[1], self.userdata[2]) + with self.app.test_client() as test_client: + operation, token = self._create_operation(test_client, self.userdata) + response = test_client.post('/set_last_used', data={"token": token, + "op_id": operation.id}) + assert response.status_code == 200 + data = json.loads(response.data.decode('utf-8')) + assert data["success"] is True + + def test_update_last_used(self): + assert add_user(self.userdata[0], self.userdata[1], self.userdata[2]) + with self.app.test_client() as test_client: + operation, token = self._create_operation(test_client, self.userdata) + response = test_client.post('/update_last_used', data={"token": token}) + assert response.status_code == 200 + data = json.loads(response.data.decode('utf-8')) + assert data["success"] is True + def test_get_users_without_permission(self): assert add_user(self.userdata[0], self.userdata[1], self.userdata[2]) unprevileged_user = 'UV20@uv20', 'UV20', 'uv20' diff --git a/tests/_test_msui/test_mscolab.py b/tests/_test_msui/test_mscolab.py index af56b7694..30da6ac56 100644 --- a/tests/_test_msui/test_mscolab.py +++ b/tests/_test_msui/test_mscolab.py @@ -521,6 +521,36 @@ def test_handle_leave_operation(self, mockmessage): assert self.window.listViews.count() == 0 assert self.window.listOperationsMSC.model().rowCount() == 0 + @mock.patch("PyQt5.QtWidgets.QMessageBox.information") + @mock.patch("PyQt5.QtWidgets.QInputDialog.getText", return_value=("new_name", True)) + def test_handle_rename_operation(self, mockbox, mockpatch): + self._connect_to_mscolab() + self._create_user("something", "something@something.org", "something") + self._create_operation("flight1234", "Description flight1234") + assert self.window.listOperationsMSC.model().rowCount() == 1 + self._activate_operation_at_index(0) + assert self.window.mscolab.active_op_id is not None + self.window.actionRenameOperation.trigger() + QtWidgets.QApplication.processEvents() + QtTest.QTest.qWait(0) + assert self.window.mscolab.active_op_id is not None + assert self.window.mscolab.active_operation_name == "new_name" + + @mock.patch("PyQt5.QtWidgets.QMessageBox.information") + @mock.patch("PyQt5.QtWidgets.QInputDialog.getText", return_value=("new_desciption", True)) + def test_update_description(self, mockbox, mockpatch): + self._connect_to_mscolab() + self._create_user("something", "something@something.org", "something") + self._create_operation("flight1234", "Description flight1234") + assert self.window.listOperationsMSC.model().rowCount() == 1 + self._activate_operation_at_index(0) + assert self.window.mscolab.active_op_id is not None + self.window.actionUpdateOperationDesc.trigger() + QtWidgets.QApplication.processEvents() + QtTest.QTest.qWait(0) + assert self.window.mscolab.active_op_id is not None + assert self.window.mscolab.active_operation_desc == "new_desciption" + def test_get_recent_op_id(self): self._connect_to_mscolab() self._create_user("anton", "anton@something.org", "something") @@ -546,6 +576,30 @@ def test_get_recent_operation(self): assert operation["path"] == "flight1234" assert operation["access_level"] == "creator" + def test_open_chat_window(self): + self._connect_to_mscolab() + self._create_user("something", "something@something.org", "something") + self._create_operation("flight1234", "Description flight1234") + assert self.window.listOperationsMSC.model().rowCount() == 1 + self._activate_operation_at_index(0) + assert self.window.mscolab.active_op_id is not None + self.window.actionChat.trigger() + QtWidgets.QApplication.processEvents() + QtTest.QTest.qWait(0) + assert self.window.mscolab.chat_window is not None + + def test_close_chat_window(self): + self._connect_to_mscolab() + self._create_user("something", "something@something.org", "something") + self._create_operation("flight1234", "Description flight1234") + assert self.window.listOperationsMSC.model().rowCount() == 1 + self._activate_operation_at_index(0) + assert self.window.mscolab.active_op_id is not None + self.window.actionChat.trigger() + QtWidgets.QApplication.processEvents() + self.window.mscolab.close_chat_window() + assert self.window.mscolab.chat_window is None + def test_delete_operation_from_list(self): self._connect_to_mscolab() self._create_user("other", "other@something.org", "something")