Skip to content

Commit

Permalink
feature(qchat): send a geojson layer through websocket
Browse files Browse the repository at this point in the history
  • Loading branch information
gounux committed Oct 27, 2024
1 parent e12d92b commit 6173438
Show file tree
Hide file tree
Showing 4 changed files with 161 additions and 4 deletions.
94 changes: 93 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,13 +34,16 @@
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,
Expand Down Expand Up @@ -149,6 +153,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 +221,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 @@ -519,6 +529,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 +578,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 +761,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 +817,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)
57 changes: 54 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,42 @@ 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)

# 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_name}' layer: #{nb_features} features using 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))
10 changes: 10 additions & 0 deletions qtribu/logic/qchat_messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,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
4 changes: 4 additions & 0 deletions qtribu/logic/qchat_websocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from qtribu.logic.qchat_messages import (
QChatExiterMessage,
QChatGeojsonMessage,
QChatImageMessage,
QChatLikeMessage,
QChatMessage,
Expand Down Expand Up @@ -54,6 +55,7 @@ def __init__(self):
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 @@ -106,3 +108,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 6173438

Please sign in to comment.