diff --git a/.pylintrc b/.pylintrc index a07dcd81..f3ffecfa 100644 --- a/.pylintrc +++ b/.pylintrc @@ -219,14 +219,14 @@ redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io [BASIC] # Naming style matching correct argument names. -argument-naming-style=snake_case +argument-naming-style=camelCase # Regular expression matching correct argument names. Overrides argument- # naming-style. #argument-rgx= # Naming style matching correct attribute names. -attr-naming-style=snake_case +attr-naming-style=camelCase # Regular expression matching correct attribute names. Overrides attr-naming- # style. @@ -266,7 +266,7 @@ const-naming-style=UPPER_CASE docstring-min-length=-1 # Naming style matching correct function names. -function-naming-style=snake_case +function-naming-style=camelCase # Regular expression matching correct function names. Overrides function- # naming-style. @@ -278,7 +278,15 @@ good-names=i, k, ex, Run, - _ + _, + x, + y, + z, + pt, + a, + b, + f, + e # Include a hint for the correct naming format with invalid-name. include-naming-hint=yes @@ -291,7 +299,7 @@ inlinevar-naming-style=any #inlinevar-rgx= # Naming style matching correct method names. -method-naming-style=snake_case +method-naming-style=camelCase # Regular expression matching correct method names. Overrides method-naming- # style. @@ -318,7 +326,7 @@ no-docstring-rgx=^_ property-classes=abc.abstractproperty # Naming style matching correct variable names. -variable-naming-style=snake_case +variable-naming-style=camelCase # Regular expression matching correct variable names. Overrides variable- # naming-style. diff --git a/.qt_for_python/uic/speckle_qgis_dialog_base.py b/.qt_for_python/uic/speckle_qgis_dialog_base.py new file mode 100644 index 00000000..b7abd6f9 --- /dev/null +++ b/.qt_for_python/uic/speckle_qgis_dialog_base.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file '/Users/alan/Documents/Speckle/speckle-qgis/ui/speckle_qgis_dialog_base.ui' +# +# Created by: PyQt5 UI code generator 5.15.4 +# +# WARNING: Any manual changes made to this file will be lost when pyuic5 is +# run again. Do not edit this file unless you know what you are doing. + + +from PyQt5 import QtCore, QtGui, QtWidgets + + +class Ui_SpeckleQGISDialogBase(object): + def setupUi(self, SpeckleQGISDialogBase): + SpeckleQGISDialogBase.setObjectName("SpeckleQGISDialogBase") + SpeckleQGISDialogBase.resize(575, 651) + self.dockWidgetContents = QtWidgets.QWidget() + self.dockWidgetContents.setObjectName("dockWidgetContents") + self.gridLayout = QtWidgets.QGridLayout(self.dockWidgetContents) + self.gridLayout.setObjectName("gridLayout") + self.verticalLayout = QtWidgets.QVBoxLayout() + self.verticalLayout.setObjectName("verticalLayout") + self.formLayout = QtWidgets.QFormLayout() + self.formLayout.setContentsMargins(10, 10, 10, 10) + self.formLayout.setObjectName("formLayout") + self.streamListLabel = QtWidgets.QLabel(self.dockWidgetContents) + self.streamListLabel.setObjectName("streamListLabel") + self.formLayout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.streamListLabel) + self.streamList = QtWidgets.QListWidget(self.dockWidgetContents) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.MinimumExpanding) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.streamList.sizePolicy().hasHeightForWidth()) + self.streamList.setSizePolicy(sizePolicy) + self.streamList.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContents) + self.streamList.setResizeMode(QtWidgets.QListView.Fixed) + self.streamList.setViewMode(QtWidgets.QListView.ListMode) + self.streamList.setObjectName("streamList") + self.formLayout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.streamList) + self.streamListButtons = QtWidgets.QHBoxLayout() + self.streamListButtons.setObjectName("streamListButtons") + self.streams_add_button = QtWidgets.QPushButton(self.dockWidgetContents) + self.streams_add_button.setObjectName("streams_add_button") + self.streamListButtons.addWidget(self.streams_add_button) + self.streams_remove_button = QtWidgets.QPushButton(self.dockWidgetContents) + self.streams_remove_button.setObjectName("streams_remove_button") + self.streamListButtons.addWidget(self.streams_remove_button) + spacerItem = QtWidgets.QSpacerItem(40, 0, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.streamListButtons.addItem(spacerItem) + self.formLayout.setLayout(1, QtWidgets.QFormLayout.FieldRole, self.streamListButtons) + self.streamIdLabel = QtWidgets.QLabel(self.dockWidgetContents) + self.streamIdLabel.setObjectName("streamIdLabel") + self.formLayout.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.streamIdLabel) + self.streamIdField = QtWidgets.QLineEdit(self.dockWidgetContents) + self.streamIdField.setEnabled(False) + self.streamIdField.setClearButtonEnabled(False) + self.streamIdField.setObjectName("streamIdField") + self.formLayout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.streamIdField) + self.streamBranchLabel = QtWidgets.QLabel(self.dockWidgetContents) + self.streamBranchLabel.setObjectName("streamBranchLabel") + self.formLayout.setWidget(3, QtWidgets.QFormLayout.LabelRole, self.streamBranchLabel) + self.streamBranchDropdown = QtWidgets.QComboBox(self.dockWidgetContents) + self.streamBranchDropdown.setObjectName("streamBranchDropdown") + self.formLayout.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.streamBranchDropdown) + self.layersLabel = QtWidgets.QLabel(self.dockWidgetContents) + self.layersLabel.setObjectName("layersLabel") + self.formLayout.setWidget(4, QtWidgets.QFormLayout.LabelRole, self.layersLabel) + self.layersWidget = QtWidgets.QListWidget(self.dockWidgetContents) + self.layersWidget.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection) + self.layersWidget.setObjectName("layersWidget") + self.formLayout.setWidget(4, QtWidgets.QFormLayout.FieldRole, self.layersWidget) + self.messageLabel = QtWidgets.QLabel(self.dockWidgetContents) + self.messageLabel.setObjectName("messageLabel") + self.formLayout.setWidget(6, QtWidgets.QFormLayout.LabelRole, self.messageLabel) + self.messageInput = QtWidgets.QLineEdit(self.dockWidgetContents) + self.messageInput.setObjectName("messageInput") + self.formLayout.setWidget(6, QtWidgets.QFormLayout.FieldRole, self.messageInput) + self.horizontalLayout = QtWidgets.QHBoxLayout() + self.horizontalLayout.setObjectName("horizontalLayout") + self.reloadButton = QtWidgets.QPushButton(self.dockWidgetContents) + self.reloadButton.setEnabled(True) + self.reloadButton.setObjectName("reloadButton") + self.horizontalLayout.addWidget(self.reloadButton) + self.receiveButton = QtWidgets.QPushButton(self.dockWidgetContents) + self.receiveButton.setEnabled(True) + self.receiveButton.setObjectName("receiveButton") + self.horizontalLayout.addWidget(self.receiveButton) + self.sendButton = QtWidgets.QPushButton(self.dockWidgetContents) + self.sendButton.setObjectName("sendButton") + self.horizontalLayout.addWidget(self.sendButton) + self.formLayout.setLayout(7, QtWidgets.QFormLayout.FieldRole, self.horizontalLayout) + self.verticalLayout.addLayout(self.formLayout) + self.gridLayout.addLayout(self.verticalLayout, 0, 0, 1, 1) + SpeckleQGISDialogBase.setWidget(self.dockWidgetContents) + + self.retranslateUi(SpeckleQGISDialogBase) + QtCore.QMetaObject.connectSlotsByName(SpeckleQGISDialogBase) + + def retranslateUi(self, SpeckleQGISDialogBase): + _translate = QtCore.QCoreApplication.translate + SpeckleQGISDialogBase.setWindowTitle(_translate("SpeckleQGISDialogBase", "SpeckleQGIS")) + self.streamListLabel.setText(_translate("SpeckleQGISDialogBase", "Project Streams")) + self.streams_add_button.setText(_translate("SpeckleQGISDialogBase", "+")) + self.streams_remove_button.setText(_translate("SpeckleQGISDialogBase", "-")) + self.streamIdLabel.setText(_translate("SpeckleQGISDialogBase", "Active Stream")) + self.streamBranchLabel.setText(_translate("SpeckleQGISDialogBase", "Branch")) + self.layersLabel.setText(_translate("SpeckleQGISDialogBase", "Layer")) + self.messageLabel.setText(_translate("SpeckleQGISDialogBase", "Message")) + self.messageInput.setPlaceholderText(_translate("SpeckleQGISDialogBase", "Sent XXX objects from QGIS")) + self.reloadButton.setText(_translate("SpeckleQGISDialogBase", "Reload")) + self.receiveButton.setText(_translate("SpeckleQGISDialogBase", "Receive")) + self.sendButton.setText(_translate("SpeckleQGISDialogBase", "Send")) diff --git a/speckle-qgsi.pyproject.user b/speckle-qgsi.pyproject.user index 9f199b79..de608b5f 100644 --- a/speckle-qgsi.pyproject.user +++ b/speckle-qgsi.pyproject.user @@ -1,6 +1,6 @@ - + EnvironmentId @@ -259,7 +259,82 @@ true /Users/alan/Documents/Speckle/speckle-qgis/ui - 2 + + dwarf + + cpu-cycles + + + 250 + + -e + cpu-cycles + --call-graph + dwarf,4096 + -F + 250 + + -F + true + 4096 + false + false + 1000 + + true + + + false + false + false + false + true + 0.01 + 10 + true + kcachegrind + 1 + + 25 + + 1 + true + false + true + + valgrind + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 + 14 + + + 2 + + /Users/alan/Documents/Speckle/speckle-qgis/ui/streamlist_dialog.py + PythonEditor.RunConfiguration./Users/alan/Documents/Speckle/speckle-qgis/ui/streamlist_dialog.py + /Users/alan/Documents/Speckle/speckle-qgis/ui/streamlist_dialog.py + {8f321c72-628b-40a1-963b-ff676f35cb12} + /Users/alan/Documents/Speckle/speckle-qgis/ui/streamlist_dialog.py + false + true + false + true + /Users/alan/Documents/Speckle/speckle-qgis/ui + + 3 diff --git a/speckle/converter/geometry.py b/speckle/converter/geometry.py index 92c7a94f..f10a47ba 100644 --- a/speckle/converter/geometry.py +++ b/speckle/converter/geometry.py @@ -1,32 +1,43 @@ +""" This module contains all geometry conversion functionality To and From Speckle.""" + import math -from qgis.core import (QgsGeometry, QgsLineString, QgsMultiLineString, - QgsMultiPoint, QgsMultiPolygon, QgsPoint, QgsPointXY, QgsPolygon, - QgsProject, QgsCoordinateReferenceSystem, QgsCoordinateTransform, QgsWkbTypes) -from specklepy.objects.geometry import Point, Polyline, Mesh +from typing import List, Union + +from qgis.core import ( + QgsCoordinateReferenceSystem, + QgsCoordinateTransform, + QgsGeometry, + QgsLineString, + QgsPoint, + QgsPointXY, + QgsPolygon, + QgsProject, QgsCoordinateReferenceSystem, QgsCoordinateTransform, + QgsWkbTypes, +) from specklepy.objects import Base +from specklepy.objects.geometry import Line, Mesh, Point, Polyline -def extractGeometry(feature): +def convertToSpeckle(feature) -> Union[Base, List[Base], None]: try: geom = feature.geometry() except AttributeError: geom = feature - geomSingleType = QgsWkbTypes.isSingleType(geom.wkbType()) - geom_type = geom.type() + geomType = geom.type() - if geom_type == QgsWkbTypes.PointGeometry: + if geomType == QgsWkbTypes.PointGeometry: # the geometry type can be of single or multi type if geomSingleType: return pointToSpeckle(geom.constGet()) else: return [pointToSpeckle(pt) for pt in geom.parts()] - elif geom_type == QgsWkbTypes.LineGeometry: + elif geomType == QgsWkbTypes.LineGeometry: if geomSingleType: return polylineToSpeckle(geom) else: return [polylineToSpeckle(poly) for poly in geom.parts()] - elif geom_type == QgsWkbTypes.PolygonGeometry: + elif geomType == QgsWkbTypes.PolygonGeometry: if geomSingleType: return polygonToSpeckle(geom) else: @@ -36,7 +47,26 @@ def extractGeometry(feature): return None +def convertToNative(base: Base) -> Union[QgsGeometry, None]: + """Converts any given base object to QgsGeometry.""" + converted = None + conversions = [ + (Point, pointToNative), + (Line, lineToNative), + (Polyline, polylineToNative), + (Mesh, meshToNative), + ] + + for conversion in conversions: + if isinstance(base, conversion[0]): + converted = conversion[1](base) + break + + return converted + + def pointToSpeckle(pt: QgsPoint or QgsPointXY): + """Converts a QgsPoint to Speckle""" if isinstance(pt, QgsPointXY): pt = QgsPoint(pt) # when unset, z() returns "nan" @@ -50,47 +80,95 @@ def pointToSpeckle(pt: QgsPoint or QgsPointXY): return specklePoint -def polylineFromVertices(vertices, closed): +def pointToNative(pt: Point) -> QgsPoint: + """Converts a Speckle Point to QgsPoint""" + return QgsPoint(pt.x, pt.y, pt.z) + + +def lineToNative(line: Line) -> QgsLineString: + """Converts a Speckle Line to QgsLineString""" + + return QgsLineString(pointToNative(line.start), pointToNative(line.end)) + + +def polylineToNative(poly: Polyline) -> QgsLineString: + """Converts a Speckle Polyline to QgsLineString""" + + return QgsLineString([pointToNative(pt) for pt in poly.as_points()]) + + +def polygonToNative(poly: Base) -> QgsPolygon: + """Converts a Speckle Polygon base object to QgsPolygon. + This object must have a 'boundary' and 'voids' properties. + Each being a Speckle Polyline and List of polylines respectively.""" + + return QgsPolygon( + polylineToNative(poly["boundary"]), + [polylineToNative(void) for void in poly["voids"]], + ) + + +def meshToNative(mesh: Mesh): + """Converts a Speckle Mesh to QgsGeometry. Currently UNSUPPORTED""" + return None + + +def polylineFromVerticesToSpeckle(vertices, closed): + """Returns a Speckle Polyline given a list of QgsPoint instances and a boolean indicating if it's closed or not.""" specklePts = [pointToSpeckle(pt) for pt in vertices] # TODO: Replace with `from_points` function when fix is pushed. polyline = Polyline() polyline.value = [] polyline.closed = closed polyline.units = specklePts[0].units - for i in range(len(specklePts)): + for i, point in enumerate(specklePts): if closed and i == len(specklePts) - 1: continue - point = specklePts[i] polyline.value.extend([point.x, point.y, point.z]) return polyline def polylineToSpeckle(poly: QgsLineString): - return polylineFromVertices(poly.vertices(), False) + """Converts a QgsLineString to Speckle""" + return polylineFromVerticesToSpeckle(poly.vertices(), False) def polygonToSpeckle(geom: QgsPolygon): + """Converts a QgsPolygon to Speckle""" polygon = Base() - polygon.boundary = polylineFromVertices( - geom.exteriorRing().vertices(), True) + polygon.boundary = polylineFromVerticesToSpeckle( + geom.exteriorRing().vertices(), True + ) voids = [] for i in range(geom.numInteriorRings()): - intRing = polylineFromVertices(geom.interiorRing(i).vertices(), True) + intRing = polylineFromVerticesToSpeckle(geom.interiorRing(i).vertices(), True) voids.append(intRing) polygon.voids = voids return polygon -def transform(src: QgsPointXY, crs_src: QgsCoordinateReferenceSystem, crsDest: QgsCoordinateReferenceSystem): +def transform( + src: QgsPointXY, + crsSrc: QgsCoordinateReferenceSystem, + crsDest: QgsCoordinateReferenceSystem, +): + """Transforms a QgsPointXY from the source CRS to the destination.""" + transformContext = QgsProject.instance().transformContext() - xform = QgsCoordinateTransform(crs_src, crsDest, transformContext) + xform = QgsCoordinateTransform(crsSrc, crsDest, transformContext) # forward transformation: src -> dest dest = xform.transform(src) return dest -def reverseTransform(dest: QgsPointXY, crsSrc: QgsCoordinateReferenceSystem, crsDest: QgsCoordinateReferenceSystem): +def reverseTransform( + dest: QgsPointXY, + crsSrc: QgsCoordinateReferenceSystem, + crsDest: QgsCoordinateReferenceSystem, +): + """Transforms a QgsPointXY from the destination CRS to the source.""" + transformContext = QgsProject.instance().transformContext() xform = QgsCoordinateTransform(crsSrc, crsDest, transformContext) diff --git a/speckle/converter/layers.py b/speckle/converter/layers.py index 45b418bd..08903135 100644 --- a/speckle/converter/layers.py +++ b/speckle/converter/layers.py @@ -1,6 +1,12 @@ +""" +Contains all Layer related classes and methods. +""" import os import math +from typing import Any, List, Optional, Union + from qgis.core import Qgis, QgsWkbTypes, QgsPointXY, QgsGeometry, QgsMapLayer, QgsRasterBandStats, QgsRasterLayer, QgsVectorLayer, QgsCoordinateTransform +from qgis.PyQt.QtCore import QVariant from speckle.logging import logger from qgis.gui import QgsRendererWidget from speckle.converter.geometry import extractGeometry, rasterToMesh, transform @@ -13,43 +19,53 @@ from specklepy.objects import Base class CRS(Base): + """A very basic GIS Coordinate Reference System stored in wkt format""" name: str wkt: str units: str - def __init__(self, name, wkt, units, **kwargs) -> None: + def __init__(self, name=None, wkt=None, units=None, **kwargs) -> None: super().__init__(**kwargs) self.name = name self.wkt = wkt self.units = units class Layer(Base, chunkable={"features": 100}): - def __init__(self, name, crs, features=[], **kwargs) -> None: + """A GIS Layer""" + + def __init__( + self, + name=None, + crs=None, + features: List[Base] = [], + layerType: str = None, + **kwargs + ) -> None: super().__init__(**kwargs) self.name = name self.crs = crs + self.type = layerType self.features = features -def getLayers(tree, parent): +def getLayers(tree: QgsLayerTree, parent: QgsLayerTreeNode) -> List[QgsLayerTreeNode]: + """Gets a list of all layers in the given QgsLayerTree""" + children = parent.children() layers = [] for node in children: - isLayer = tree.isLayer(node) - if isLayer: + if tree.isLayer(node): layers.append(node) continue - isGroup = tree.isGroup(node) - if isGroup: + if tree.isGroup(node): layers.extend(getLayers(tree, node)) return layers def convertSelectedLayers(layers, selectedLayerNames, projectCRS, project): + """Converts the current selected layers to Speckle""" result = [] for layer in layers: - # if not(hasattr(layer, "fields")): - # continue if layer.name() in selectedLayerNames: result.append(layerToSpeckle(layer, projectCRS, project)) return result @@ -87,7 +103,7 @@ def reprojectLayer(layer, targetCRS, project): ''' def layerToSpeckle(layer, projectCRS, project): #now the input is QgsVectorLayer instead of qgis._core.QgsLayerTreeLayer - + """Converts a given QGIS Layer to Speckle""" layerName = layer.name() selectedLayer = layer.layer() crs = selectedLayer.crs() @@ -132,15 +148,15 @@ def featureToSpeckle(fieldnames, f, sourceCRS, targetCRS, project): # Try to extract geometry try: - geom = extractGeometry(f) - if (geom != None): - b['displayValue'] = geom + geom = convertToSpeckle(f) + if geom is not None: + b["displayValue"] = geom except Exception as error: - logger.logToUser("Error converting geometry: " + error, Qgis.Critical) + logger.logToUser("Error converting geometry: " + str(error), Qgis.Critical) for name in fieldnames: corrected = name.replace("/", "_").replace(".", "-") - if(corrected == "id"): + if corrected == "id": corrected == "applicationId" b[corrected] = str(f[name]) return b @@ -354,7 +370,7 @@ def receiveRaster(project, source_folder, name, epsg, rasterDimensions, bands, r ''' class RasterLayer(Base, speckle_type="Objects.Geometry." + "RasterLayer", chunkable={"Raster": 1000}, detachable={"Raster"}): - Raster: List[str] = None + Raster: Optional[List[str]] = None @ classmethod def from_list(cls, args: List[Any]) -> "RasterLayer": @@ -363,4 +379,89 @@ def from_list(cls, args: List[Any]) -> "RasterLayer": ) def to_list(self) -> List[Any]: + if(self.Raster is None): + raise Exception("This RasterLayer has no data set.") return self.Raster + + +def featureToNative(feature: Base) -> QgsFeature: + return + + +def layerToNative(layer: Layer) -> Union[QgsVectorLayer, QgsRasterLayer, None]: + layerType = type(layer.type) + if layer.type is None: + # Handle this case + return + elif layer.type.endswith("VectorLayer"): + vectorLayerToNative(layer) + elif layer.type.endswith("RasterLayer"): + rasterLayerToNative(layer) + return None + + +def getLayerAttributes(layer: Layer): + names = {} + for feature in layer.features: + featNames = feature.get_member_names() + for n in featNames: + if not (n in names): + try: + value = feature[n] + variant = getVariantFromValue(value) + if variant: + names[n] = QgsField(n, variant) + except Exception as error: + logger.log(str(error)) + return names.values() + + +def getVariantFromValue(value): + pairs = { + str: QVariant.String, + float: QVariant.Double, + int: QVariant.Int, + bool: QVariant.Bool, + } + t = type(value) + return pairs[t] + + +def vectorLayerToNative(layer: Layer): + opts = QgsVectorLayer.LayerOptions() + vl = None + for lyr in QgsProject.instance().mapLayers().values(): + if lyr.id() == layer.applicationId: + vl = lyr + break + if vl is None: + vl = QgsVectorLayer("Speckle", layer.name, "memory", opts) + pr = vl.dataProvider() + vl.startEditing() + attrs = getLayerAttributes(layer) + pr.addAttributes(attrs) + vl.setCrs(QgsCoordinateReferenceSystem.fromWkt(layer.crs.wkt)) + vl.commitChanges() + vl.startEditing() + # fets = [featureToNative(feature) for feature in layer.features] + # vl.addFeatures(fets) + vl.commitChanges() + QgsProject.instance().addMapLayer(vl) + return None + + +def rasterLayerToNative(layer: Layer): + rl = QgsRasterLayer("Speckle", layer.name, "memory", QgsRasterLayer.LayerOptions()) + + return None + + +def get_type(type_name): + try: + return getattr(__builtins__, type_name) + except AttributeError: + try: + obj = globals()[type_name] + except KeyError: + return None + return repr(obj) if isinstance(obj, type) else None diff --git a/speckle/logging.py b/speckle/logging.py index e28065d6..82bc4612 100644 --- a/speckle/logging.py +++ b/speckle/logging.py @@ -1,19 +1,26 @@ +"""Logging Utility Module for Speckle QGIS""" from qgis.core import Qgis, QgsMessageLog + class Logging: + """Holds utility methods for logging messages to QGIS""" + qgisInterface = None def __init__(self, iface) -> None: self.qgisInterface = iface - + def logToUser(self, message, level=Qgis.Info, duration=10): - self.log(message,level) - if(self.qgisInterface): + """Logs a specific message to the user in QGIS""" + self.log(message, level) + if self.qgisInterface: self.qgisInterface.messageBar().pushMessage( - "Speckle", message, - level=level, duration=duration) + "Speckle", message, level=level, duration=duration + ) def log(self, message, level=Qgis.Info): - QgsMessageLog.logMessage(message, 'Speckle', level=level) + """Logs a specific message to the Speckle messages panel.""" + QgsMessageLog.logMessage(message, "Speckle", level=level) + -logger = Logging(None) \ No newline at end of file +logger = Logging(None) diff --git a/speckle/utils.py b/speckle/utils.py index 99c085c6..6d5cb098 100644 --- a/speckle/utils.py +++ b/speckle/utils.py @@ -6,37 +6,45 @@ from speckle.logging import logger -MESSAGE_CATEGORY = 'Speckle' +MESSAGE_CATEGORY = "Speckle" + def get_qgis_python_path(): pythonExec = os.path.dirname(sys.executable) - if (sys.platform == "win32"): + if sys.platform == "win32": pythonExec += "\\python3" else: - pythonExec +="/bin/python3" + pythonExec += "/bin/python3" return pythonExec + def enable_remote_debugging(): try: import ptvsd except: QgsMessageLog.logMessage( - "PTVSD not installed, setting up now", MESSAGE_CATEGORY, Qgis.Info) - subprocess.call( - [get_qgis_python_path(), '-m', 'pip', 'install', 'ptvsd']) + "PTVSD not installed, setting up now", MESSAGE_CATEGORY, Qgis.Info + ) + subprocess.call([get_qgis_python_path(), "-m", "pip", "install", "ptvsd"]) try: import ptvsd + if ptvsd.is_attached(): QgsMessageLog.logMessage( - "Remote Debug for Visual Studio is already active", MESSAGE_CATEGORY, Qgis.Info) + "Remote Debug for Visual Studio is already active", + MESSAGE_CATEGORY, + Qgis.Info, + ) return - ptvsd.enable_attach(address=('localhost', 5678)) + ptvsd.enable_attach(address=("localhost", 5678)) # Enable this if you want to be able to hit early breakpoints. Execution will stop until IDE attaches to the port, but QGIS will appear to be unresponsive!!!! - #ptvsd.wait_for_attach() - + # ptvsd.wait_for_attach() + QgsMessageLog.logMessage( - "Attached remote Debug for Visual Studio", MESSAGE_CATEGORY, Qgis.Success) + "Attached remote Debug for Visual Studio", MESSAGE_CATEGORY, Qgis.Success + ) except Exception as e: QgsMessageLog.logMessage( - "Failed to attach to PTVSD", MESSAGE_CATEGORY, Qgis.Info) + "Failed to attach to PTVSD", MESSAGE_CATEGORY, Qgis.Info + ) diff --git a/speckle_qgis.py b/speckle_qgis.py index 756678c9..14ea8c3d 100644 --- a/speckle_qgis.py +++ b/speckle_qgis.py @@ -15,39 +15,77 @@ import os.path +from types import FunctionType +from typing import Any, Callable, List, Optional, Tuple -from speckle.logging import logger - -from qgis.PyQt.QtCore import QSettings, QTranslator, QCoreApplication, Qt +from qgis.core import Qgis, QgsProject +from qgis.PyQt.QtCore import QCoreApplication, QSettings, Qt, QTranslator from qgis.PyQt.QtGui import QIcon from qgis.PyQt.QtWidgets import QAction, QDockWidget, QListWidget from qgis.core import Qgis, QgsProject, QgsRasterLayer +from specklepy.api import operations +from specklepy.api.client import SpeckleException +from specklepy.api.credentials import StreamWrapper +from specklepy.api.models import Stream +from specklepy.objects import Base +from specklepy.transports.server import ServerTransport # Initialize Qt resources from file resources.py from resources import * +from speckle.converter.layers import ( + Layer, + convertSelectedLayers, + getLayers, + layerToNative, + layerToSpeckle, +) +from speckle.logging import logger +from ui.add_stream_modal import AddStreamModalDialog # Import the code for the dialog from ui.speckle_qgis_dialog import SpeckleQGISDialog -from specklepy.api import operations -from specklepy.api.client import SpeckleClient, SpeckleException -from specklepy.api.credentials import Account, get_local_accounts, StreamWrapper, get_default_account -from specklepy.transports.server import ServerTransport -from specklepy.objects import Base -from specklepy.api.models import Stream -from speckle.logging import * -from speckle.converter.geometry import * -from speckle.converter.layers import convertSelectedLayers, getLayers -from ui.add_stream_modal import AddStreamModalDialog +def traverseObject( + base: Base, + callback: Optional[Callable[[Base], bool]], + check: Optional[Callable[[Base], bool]], +): + if check and check(base): + res = callback(base) if callback else False + if res: + return + memberNames = base.get_member_names() + for name in memberNames: + try: + if ["id", "applicationId", "units", "speckle_type"].index(name): + continue + except: + # Do nothing + print() + traverseValue(base[name], callback, check) + + +def traverseValue( + value: Any, + callback: Optional[Callable[[Base], bool]], + check: Optional[Callable[[Base], bool]], +): + if isinstance(value, Base): + traverseObject(value, callback, check) + if isinstance(value, List): + for item in value: + traverseValue(item, callback, check) + + class SpeckleQGIS: """Speckle Connector Plugin for QGIS""" - dockwidget: QDockWidget = None - add_stream_modal: AddStreamModalDialog = None - current_streams: [(StreamWrapper, Stream)] = [] - - active_stream: (StreamWrapper, Stream) = None + dockwidget: Optional[QDockWidget] + add_stream_modal: AddStreamModalDialog + current_streams: List[Tuple[StreamWrapper, Stream]] = [] + + active_stream: Optional[Tuple[StreamWrapper, Stream]] qgis_project: QgsProject @@ -60,11 +98,9 @@ def __init__(self, iface): :type iface: QgsInterface """ # Save reference to the QGIS interface - + self.dockwidget = None self.iface = iface self.qgis_project = QgsProject().instance() - self.qgis_project.fileNameChanged.connect(self.reloadUI) - self.qgis_project.homePathChanged.connect(self.reloadUI) # initialize plugin directory self.plugin_dir = os.path.dirname(__file__) @@ -190,10 +226,9 @@ def initGui(self): def onClosePlugin(self): """Cleanup necessary items here when plugin dockwidget is closed""" - # print "** CLOSING FakePlugin" - # disconnects - self.dockwidget.closingPlugin.disconnect(self.onClosePlugin) + if self.dockwidget: + self.dockwidget.closingPlugin.disconnect(self.onClosePlugin) # remove this statement if dockwidget is to remain # for reuse if plugin is reopened @@ -210,6 +245,9 @@ def unload(self): self.iface.removeToolBarIcon(action) def onSendButtonClicked(self): + """Handles action when Send button is pressed.""" + if not self.dockwidget: + return # creating our parent base object project = QgsProject.instance() projectCRS = project.crs() @@ -228,6 +266,12 @@ def onSendButtonClicked(self): logger.logToUser("Please enter a Stream Url/ID.", Qgis.Warning) return + if self.active_stream is None: + logger.logToUser( + "There is no active stream. Please select a stream from the list." + ) + return + # Get the stream wrapper streamWrapper = self.active_stream[0] streamId = streamWrapper.stream_id @@ -235,7 +279,7 @@ def onSendButtonClicked(self): # Ensure the stream actually exists try: client.stream.get(streamId) - except Exception as error: + except SpeckleException as error: logger.logToUser(str(error), Qgis.Critical) return @@ -244,8 +288,8 @@ def onSendButtonClicked(self): try: # this serialises the block and sends it to the transport - hash = operations.send(base=base_obj, transports=[transport]) - except Exception as error: + objId = operations.send(base=base_obj, transports=[transport]) + except SpeckleException as error: logger.logToUser("Error sending data", Qgis.Critical) return @@ -254,18 +298,81 @@ def onSendButtonClicked(self): # you can now create a commit on your stream with this object client.commit.create( stream_id=streamId, - object_id=hash, - branch_name= self.dockwidget.streamBranchDropdown.currentText(), - message= "Sent objects from QGIS" if len(message) == 0 else message, + object_id=objId, + branch_name=self.dockwidget.streamBranchDropdown.currentText(), + message="Sent objects from QGIS" if len(message) == 0 else message, source_application="QGIS", ) logger.logToUser("Successfully sent data to stream: " + streamId) self.dockwidget.messageInput.setText("") - except Exception as e: + except SpeckleException as e: logger.logToUser("Error creating commit", Qgis.Critical) + def onReceiveButtonClicked(self): + """Handles action when the Receive button is pressed""" + + if not self.dockwidget: + return + + # Check if stream id/url is empty + if not self.dockwidget.streamIdField.text(): + logger.logToUser("Please enter a Stream Url/ID.", Qgis.Warning) + return + + if self.active_stream is None: + logger.logToUser( + "There is no active stream. Please select a stream from the list.", + Qgis.Error, + ) + return + + # Get the stream wrapper + streamWrapper = self.active_stream[0] + streamId = streamWrapper.stream_id + client = streamWrapper.get_client() + # Ensure the stream actually exists + try: + stream = client.stream.get(streamId) + if stream.branches is None: + return + branchName = self.dockwidget.streamBranchDropdown.currentText() + branch = None + for b in stream.branches.items: + if b.name == branchName: + branch = b + break + if branch is None or branch.commits is None: + return + except SpeckleException as error: + logger.logToUser(str(error), Qgis.Critical) + return + + # next create a server transport - this is the vehicle through which you will send and receive + transport = ServerTransport(client=client, stream_id=streamId) + + try: + commit = branch.commits.items[0] + objId = commit.referencedObject + if objId is None: + return + commitObj = operations.receive(objId, transport, None) + logger.log(f"Succesfully received {objId}") + + check: Callable[[Base], bool] = lambda base: isinstance(base, Layer) + + def callback(base: Base) -> bool: + if isinstance(base, Layer): + layerToNative(base) + return True + + traverseObject(commitObj, callback, check) + except SpeckleException as e: + logger.logToUser("Receive failed", Qgis.Error) + return def populateLayerDropdown(self): + if not self.dockwidget: + return # Fetch the currently loaded layers layers = QgsProject.instance().mapLayers().values() @@ -283,23 +390,27 @@ def populateLayerDropdown(self): self.dockwidget.layersWidget.addItems(nameDisplay) def populateProjectStreams(self): - + if not self.dockwidget: + return + self.dockwidget.streamList.clear() - self.dockwidget.streamList.addItems([ - f"{stream[1].name} - {stream[1].id}" for stream in self.current_streams - ]) + self.dockwidget.streamList.addItems( + [f"{stream[1].name} - {stream[1].id}" for stream in self.current_streams] + ) def populateActiveStreamBranchDropdown(self): - logger.log("populating branch from stream") + if not self.dockwidget: + return self.dockwidget.streamBranchDropdown.clear() - if(self.active_stream is None): + if self.active_stream is None or self.active_stream[1].branches is None: return - self.dockwidget.streamBranchDropdown.addItems([ - f"{branch.name}" for branch in self.active_stream[1].branches.items - ]) + + self.dockwidget.streamBranchDropdown.addItems( + [f"{branch.name}" for branch in self.active_stream[1].branches.items] + ) def reloadUI(self): - if(self.dockwidget is not None): + if self.dockwidget is not None: self.active_stream = None self.get_project_streams() self.populateLayerDropdown() @@ -317,17 +428,26 @@ def run(self): self.pluginIsActive = True if self.dockwidget is None: self.dockwidget = SpeckleQGISDialog() + self.qgis_project.fileNameChanged.connect(self.reloadUI) + self.qgis_project.homePathChanged.connect(self.reloadUI) # Setup events on first load only! self.dockwidget.sendButton.clicked.connect(self.onSendButtonClicked) + self.dockwidget.receiveButton.clicked.connect(self.onReceiveButtonClicked) self.dockwidget.reloadButton.clicked.connect(self.reloadUI) # connect to provide cleanup on closing of dockwidget self.dockwidget.closingPlugin.connect(self.onClosePlugin) # Connect streams section events - self.dockwidget.streams_add_button.clicked.connect(self.onStreamAddButtonClicked) - self.dockwidget.streams_remove_button.clicked.connect(self.onStreamRemoveButtonClicked) - self.dockwidget.streamList.itemSelectionChanged.connect(self.onActiveStreamChanged) + self.dockwidget.streams_add_button.clicked.connect( + self.onStreamAddButtonClicked + ) + self.dockwidget.streams_remove_button.clicked.connect( + self.onStreamRemoveButtonClicked + ) + self.dockwidget.streamList.itemSelectionChanged.connect( + self.onActiveStreamChanged + ) self.get_project_streams() @@ -345,13 +465,13 @@ def run(self): self.dockwidget.show() def onStreamAddButtonClicked(self): - logger.log("on stream add") self.add_stream_modal = AddStreamModalDialog(None) self.add_stream_modal.handleStreamAdd.connect(self.handleStreamAdd) self.add_stream_modal.show() - + def onStreamRemoveButtonClicked(self): - logger.log("on stream remove") + if not self.dockwidget: + return index = self.dockwidget.streamList.currentIndex().row() self.current_streams.pop(index) self.active_stream = None @@ -362,18 +482,20 @@ def onStreamRemoveButtonClicked(self): self.populateProjectStreams() def onActiveStreamChanged(self): - logger.log("on active stream changed") - if(len(self.current_streams) == 0): + if not self.dockwidget: + return + if len(self.current_streams) == 0: return index = self.dockwidget.streamList.currentRow() - if(index == -1): + if index == -1: return self.active_stream = self.current_streams[index] - self.dockwidget.streamIdField.setText(self.dockwidget.streamList.currentItem().text()) + self.dockwidget.streamIdField.setText( + self.dockwidget.streamList.currentItem().text() + ) self.populateActiveStreamBranchDropdown() def handleStreamAdd(self, sw: StreamWrapper): - logger.log("handling stream addition") client = sw.get_client() stream = client.stream.get(sw.stream_id) self.current_streams.append((sw, stream)) @@ -396,9 +518,8 @@ def get_project_streams(self): try: sw = StreamWrapper(url) stream = sw.get_client().stream.get(sw.stream_id) - temp.append((sw,stream)) + temp.append((sw, stream)) except SpeckleException as e: logger.logToUser(e.message, Qgis.Warning) self.current_streams = temp - diff --git a/ui/speckle_qgis_dialog_base.ui b/ui/speckle_qgis_dialog_base.ui index 98622e97..82a08fb0 100644 --- a/ui/speckle_qgis_dialog_base.ui +++ b/ui/speckle_qgis_dialog_base.ui @@ -161,7 +161,7 @@ - false + true Receive