From 574c6b5121675de70b9bc45998e93349be9170fb Mon Sep 17 00:00:00 2001 From: Guilhem Allaman <40383801+gounux@users.noreply.github.com> Date: Fri, 1 Nov 2024 15:17:30 +0100 Subject: [PATCH] feature(qchat): send a geojson layer through websocket (#213) --- qtribu/gui/dck_qchat.py | 109 +++++++++++++++++++++++++- qtribu/gui/qchat_tree_widget_items.py | 58 +++++++++++++- qtribu/logic/qchat_messages.py | 15 ++++ qtribu/logic/qchat_websocket.py | 10 ++- 4 files changed, 187 insertions(+), 5 deletions(-) diff --git a/qtribu/gui/dck_qchat.py b/qtribu/gui/dck_qchat.py index 240efe98..1b735e98 100644 --- a/qtribu/gui/dck_qchat.py +++ b/qtribu/gui/dck_qchat.py @@ -1,5 +1,6 @@ # standard import base64 +import json import os import tempfile from functools import partial @@ -8,7 +9,7 @@ # PyQGIS from PyQt5 import QtWebSockets # noqa QGS103 -from qgis.core import Qgis, QgsApplication +from qgis.core import Qgis, QgsApplication, QgsJsonExporter, QgsMapLayer from qgis.gui import QgisInterface, QgsDockWidget from qgis.PyQt import uic from qgis.PyQt.QtCore import QPoint, Qt @@ -33,18 +34,22 @@ QCHAT_NICKNAME_MINLENGTH, ) from qtribu.gui.qchat_tree_widget_items import ( + MESSAGE_COLUMN, QChatAdminTreeWidgetItem, + QChatGeojsonTreeWidgetItem, QChatImageTreeWidgetItem, QChatTextTreeWidgetItem, ) from qtribu.logic.qchat_api_client import QChatApiClient from qtribu.logic.qchat_messages import ( QChatExiterMessage, + QChatGeojsonMessage, QChatImageMessage, QChatLikeMessage, QChatNbUsersMessage, QChatNewcomerMessage, QChatTextMessage, + QChatUncompliantMessage, ) from qtribu.logic.qchat_websocket import QChatWebsocket from qtribu.tasks.dizzy import DizzyTask @@ -139,6 +144,9 @@ def __init__( # initialize websocket client self.qchat_ws = QChatWebsocket() self.qchat_ws.error.connect(self.on_ws_error) + self.qchat_ws.uncompliant_message_received.connect( + self.on_uncompliant_message_received + ) self.qchat_ws.text_message_received.connect(self.on_text_message_received) self.qchat_ws.image_message_received.connect(self.on_image_message_received) self.qchat_ws.nb_users_message_received.connect( @@ -149,6 +157,7 @@ def __init__( ) self.qchat_ws.exiter_message_received.connect(self.on_exiter_message_received) self.qchat_ws.like_message_received.connect(self.on_like_message_received) + self.qchat_ws.geojson_message_received.connect(self.on_geojson_message_received) # send message signal listener self.lne_message.returnPressed.connect(self.on_send_button_clicked) @@ -216,6 +225,11 @@ def on_widget_opened(self) -> None: self.cbb_room.currentIndexChanged.connect(self.on_room_changed) + # context menu on vector layer for sending as geojson in QChat + self.iface.layerTreeView().contextMenuAboutToShow.connect( + self.generate_qaction_send_geojson_layer + ) + # auto reconnect to room if needed if self.auto_reconnect_room: self.cbb_room.setCurrentText(self.auto_reconnect_room) @@ -413,6 +427,17 @@ def on_ws_error(self, error_code: int) -> None: # region websocket message received + def on_uncompliant_message_received(self, message: QChatUncompliantMessage) -> None: + self.log( + message=self.tr("Uncompliant message: {reason}").format( + reason=message.reason + ), + application=self.tr("QChat"), + log_level=Qgis.Critical, + push=PlgOptionsManager().get_plg_settings().notify_push_info, + duration=PlgOptionsManager().get_plg_settings().notify_push_duration, + ) + def on_text_message_received(self, message: QChatTextMessage) -> None: """ Launched when a text message is received from the websocket @@ -519,6 +544,14 @@ def on_like_message_received(self, message: QChatLikeMessage) -> None: self.settings.qchat_ring_tone, self.settings.qchat_sound_volume ) + def on_geojson_message_received(self, message: QChatGeojsonMessage) -> None: + """ + Launched when a geojson message is received from the websocket + """ + item = QChatGeojsonTreeWidgetItem(self.twg_chat, message) + self.twg_chat.addTopLevelItem(item) + self.twg_chat.scrollToItem(item) + # endregion def on_message_clicked(self, item: QTreeWidgetItem, column: int) -> None: @@ -560,6 +593,17 @@ def on_custom_context_menu_requested(self, point: QPoint) -> None: menu = QMenu(self.tr("QChat Menu"), self) + # if this is a geojson + if type(item) is QChatGeojsonTreeWidgetItem: + load_geojson_action = QAction( + QgsApplication.getThemeIcon("mActionAddLayer.svg"), + self.tr("Load geojson in QGIS"), + ) + load_geojson_action.triggered.connect( + partial(item.on_click, MESSAGE_COLUMN) + ) + menu.addAction(load_geojson_action) + # like message action if possible if item.can_be_liked: like_action = QAction( @@ -732,6 +776,11 @@ def on_widget_closed(self) -> None: self.cbb_room.currentIndexChanged.disconnect() self.initialized = False + # remove context menu on vector layer for sending as geojson in QChat + self.iface.layerTreeView().contextMenuAboutToShow.disconnect( + self.generate_qaction_send_geojson_layer + ) + def check_cheatcode(self, text: str) -> bool: """ Checks if a received message contains a cheatcode @@ -783,3 +832,61 @@ def on_renew_clicked(self) -> None: return_value = msg_box.exec() if return_value == QMessageBox.Yes: open_url_in_webviewer("https://qgis.org/funding/donate/", "qgis.org") + + def generate_qaction_send_geojson_layer(self, menu: QMenu) -> None: + menu.addSeparator() + send_geojson_action = QAction( + QgsApplication.getThemeIcon("mMessageLog.svg"), + self.tr("Send on QChat"), + self.iface.mainWindow(), + ) + send_geojson_action.triggered.connect(self.on_send_layer_to_qchat) + menu.addAction(send_geojson_action) + + def on_send_layer_to_qchat(self) -> None: + if not self.connected: + self.log( + message=self.tr( + "Not connected to QChat. Please connect to a room first" + ), + application=self.tr("QChat"), + log_level=Qgis.Critical, + push=PlgOptionsManager().get_plg_settings().notify_push_info, + duration=PlgOptionsManager().get_plg_settings().notify_push_duration, + ) + return + layer = self.iface.activeLayer() + if not layer: + self.log( + message=self.tr("No active layer in current QGIS project"), + application=self.tr("QChat"), + log_level=Qgis.Critical, + push=PlgOptionsManager().get_plg_settings().notify_push_info, + duration=PlgOptionsManager().get_plg_settings().notify_push_duration, + ) + return + if layer.type() != QgsMapLayer.VectorLayer: + self.log( + message=self.tr("Only vector layers can be sent on QChat"), + application=self.tr("QChat"), + log_level=Qgis.Critical, + push=PlgOptionsManager().get_plg_settings().notify_push_info, + duration=PlgOptionsManager().get_plg_settings().notify_push_duration, + ) + return + + exporter = QgsJsonExporter(layer) + exporter.setSourceCrs(layer.crs()) + exporter.setDestinationCrs(layer.crs()) + exporter.setTransformGeometries(True) + geojson_str = exporter.exportFeatures(layer.getFeatures()) + message = QChatGeojsonMessage( + type="geojson", + author=self.settings.author_nickname, + avatar=self.settings.author_avatar, + layer_name=layer.name(), + crs_wkt=layer.crs().toWkt(), + crs_authid=layer.crs().authid(), + geojson=json.loads(geojson_str), + ) + self.qchat_ws.send_message(message) diff --git a/qtribu/gui/qchat_tree_widget_items.py b/qtribu/gui/qchat_tree_widget_items.py index 461bb651..701899e3 100644 --- a/qtribu/gui/qchat_tree_widget_items.py +++ b/qtribu/gui/qchat_tree_widget_items.py @@ -1,7 +1,15 @@ import base64 +import json +import os +import tempfile from typing import Optional -from qgis.core import QgsApplication +from qgis.core import ( + QgsApplication, + QgsCoordinateReferenceSystem, + QgsProject, + QgsVectorLayer, +) from qgis.PyQt.QtCore import QTime from qgis.PyQt.QtGui import QBrush, QColor, QIcon, QPixmap from qgis.PyQt.QtWidgets import ( @@ -13,7 +21,11 @@ ) from qtribu.constants import ADMIN_MESSAGES_AVATAR, ADMIN_MESSAGES_NICKNAME -from qtribu.logic.qchat_messages import QChatImageMessage, QChatTextMessage +from qtribu.logic.qchat_messages import ( + QChatGeojsonMessage, + QChatImageMessage, + QChatTextMessage, +) from qtribu.toolbelt import PlgOptionsManager from qtribu.toolbelt.preferences import PlgSettingsStructure @@ -143,7 +155,7 @@ def can_be_copied_to_clipboard(self) -> bool: return True def copy_to_clipboard(self) -> None: - QgsApplication.instance().clipboard().setPixmap(self.message.text) + QgsApplication.instance().clipboard().setText(self.message.text) class QChatImageTreeWidgetItem(QChatTreeWidgetItem): @@ -186,3 +198,43 @@ def can_be_copied_to_clipboard(self) -> bool: def copy_to_clipboard(self) -> None: QgsApplication.instance().clipboard().setPixmap(self.pixmap) + + +class QChatGeojsonTreeWidgetItem(QChatTreeWidgetItem): + def __init__(self, parent: QTreeWidget, message: QChatGeojsonMessage): + super().__init__(parent, QTime.currentTime(), message.author, message.avatar) + self.message = message + self.init_time_and_author() + self.setText(MESSAGE_COLUMN, self.liked_message) + self.setToolTip(MESSAGE_COLUMN, self.liked_message) + + # set foreground color if sent by user + if message.author == self.settings.author_nickname: + self.set_foreground_color(self.settings.qchat_color_self) + + def on_click(self, column: int) -> None: + if column == MESSAGE_COLUMN: + # save geojson to temp file + save_path = os.path.join( + tempfile.gettempdir(), f"{self.message.layer_name}.geojson" + ) + with open(save_path, "w") as file: + json.dump(self.message.geojson, file) + # load geojson file into QGIS + layer = QgsVectorLayer(save_path, self.message.layer_name, "ogr") + layer.setCrs(QgsCoordinateReferenceSystem.fromWkt(self.message.crs_wkt)) + QgsProject.instance().addMapLayer(layer) + + @property + def liked_message(self) -> str: + layer_name = self.message.layer_name + nb_features = len(self.message.geojson["features"]) + crs = self.message.crs_authid + return f'' + + @property + def can_be_copied_to_clipboard(self) -> bool: + return True + + def copy_to_clipboard(self) -> None: + QgsApplication.instance().clipboard().setText(json.dumps(self.message.geojson)) diff --git a/qtribu/logic/qchat_messages.py b/qtribu/logic/qchat_messages.py index 01635eba..14642be2 100644 --- a/qtribu/logic/qchat_messages.py +++ b/qtribu/logic/qchat_messages.py @@ -7,6 +7,11 @@ class QChatMessage: type: str +@dataclass(init=True, frozen=True) +class QChatUncompliantMessage(QChatMessage): + reason: str + + @dataclass(init=True, frozen=True) class QChatTextMessage(QChatMessage): author: str @@ -41,3 +46,13 @@ class QChatLikeMessage(QChatMessage): liker_author: str liked_author: str message: str + + +@dataclass(init=True, frozen=True) +class QChatGeojsonMessage(QChatMessage): + author: str + avatar: Optional[str] + layer_name: str + crs_wkt: str + crs_authid: str + geojson: dict diff --git a/qtribu/logic/qchat_websocket.py b/qtribu/logic/qchat_websocket.py index 351b9f5e..ba3928e1 100644 --- a/qtribu/logic/qchat_websocket.py +++ b/qtribu/logic/qchat_websocket.py @@ -7,12 +7,14 @@ from qtribu.logic.qchat_messages import ( QChatExiterMessage, + QChatGeojsonMessage, QChatImageMessage, QChatLikeMessage, QChatMessage, QChatNbUsersMessage, QChatNewcomerMessage, QChatTextMessage, + QChatUncompliantMessage, ) from qtribu.toolbelt import PlgLogger @@ -48,12 +50,14 @@ def __init__(self): error = pyqtSignal(int) # QChat message signals + uncompliant_message_received = pyqtSignal(QChatUncompliantMessage) text_message_received = pyqtSignal(QChatTextMessage) image_message_received = pyqtSignal(QChatImageMessage) nb_users_message_received = pyqtSignal(QChatNbUsersMessage) newcomer_message_received = pyqtSignal(QChatNewcomerMessage) exiter_message_received = pyqtSignal(QChatExiterMessage) like_message_received = pyqtSignal(QChatLikeMessage) + geojson_message_received = pyqtSignal(QChatGeojsonMessage) def open(self, qchat_instance_uri: str, room: str) -> None: """ @@ -94,7 +98,9 @@ def on_message_received(self, text: str) -> None: """ message = json.loads(text) msg_type = message["type"] - if msg_type == "text": + if msg_type == "uncompliant": + self.uncompliant_message_received.emit(QChatUncompliantMessage(**message)) + elif msg_type == "text": self.text_message_received.emit(QChatTextMessage(**message)) elif msg_type == "image": self.image_message_received.emit(QChatImageMessage(**message)) @@ -106,3 +112,5 @@ def on_message_received(self, text: str) -> None: self.exiter_message_received.emit(QChatExiterMessage(**message)) elif msg_type == "like": self.like_message_received.emit(QChatLikeMessage(**message)) + elif msg_type == "geojson": + self.geojson_message_received.emit(QChatGeojsonMessage(**message))