Skip to content

Commit

Permalink
feature(qchat): send a geojson layer through websocket (#213)
Browse files Browse the repository at this point in the history
  • Loading branch information
gounux authored Nov 1, 2024
1 parent b4afabc commit 574c6b5
Show file tree
Hide file tree
Showing 4 changed files with 187 additions and 5 deletions.
109 changes: 108 additions & 1 deletion qtribu/gui/dck_qchat.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# standard
import base64
import json
import os
import tempfile
from functools import partial
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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(
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
58 changes: 55 additions & 3 deletions qtribu/gui/qchat_tree_widget_items.py
Original file line number Diff line number Diff line change
@@ -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 (
Expand All @@ -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

Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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'<layer "{layer_name}": {nb_features} features, CRS={crs}>'

@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))
15 changes: 15 additions & 0 deletions qtribu/logic/qchat_messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
10 changes: 9 additions & 1 deletion qtribu/logic/qchat_websocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@

from qtribu.logic.qchat_messages import (
QChatExiterMessage,
QChatGeojsonMessage,
QChatImageMessage,
QChatLikeMessage,
QChatMessage,
QChatNbUsersMessage,
QChatNewcomerMessage,
QChatTextMessage,
QChatUncompliantMessage,
)
from qtribu.toolbelt import PlgLogger

Expand Down Expand Up @@ -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:
"""
Expand Down Expand Up @@ -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))
Expand All @@ -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))

0 comments on commit 574c6b5

Please sign in to comment.