Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature(qchat): send a geojson layer through websocket #213

Merged
merged 20 commits into from
Nov 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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))
Loading