4

Typically, when I want to use a QGIS processing tool for my PyQGIS standalone script I write something like:

processing.run('buffer:...'{…})

Now, I would like to use the functionality of the QGIS Plugin NNJoin in my script. Is that possible and if yes how is it done?

I'm using QGIS 3.4

ADDITION: I found the following documentation for a processing tool in 3.10. join Attributes by nearest

If I implement this in my standalone script, it won't work in my QGIS 3.4 right?

  • 1
    It's possible, but hard. I applied NNJoin Plugin on my standalone PyQGIS application with GUI. But the solution which worked for me more likely won't work for you. If I have time I'll explain how I did. – Kadir Şahbaz Mar 17 '20 at 12:45
  • Does your application use a GUI? – Kadir Şahbaz Mar 17 '20 at 12:51
  • That would be great! I would appreciate it. Even if your solution doesn't work for me it would help me to get a better understanding of the whole QGIS Plugins topic. Yes, I does use a GUI – DGIS Mar 17 '20 at 12:52
5

Fist of all, for using a plugin (NNJoin here) in standalone script, you need to examine that plugin's code, decide which part you can call (I don't think this is always possible but I'm not sure 100%) and what changes/addings you need in your standalone script.

How works NNJoin plugin?: It opens a QDialog window, you select options, you click OK button and it processes.

So, you have two solutions here:

1. You can open NNJoin dialog window by script, you select options manually and click OK to run.

2. Or you can set options by script without opening NNJoin window and trigger the OK button.


SOLUTION 1:

I have two files: main.py (minimal pyqgis standalone script) and MapViewer.py (includes all GUI)

main.py:

import os
import sys
from qgis.core import QgsApplication

QgsApplication.setPrefixPath(os.environ['QGIS_PREFIX_PATH'], True)
qgs = QgsApplication([], True)
qgs.initQgis()

from MapViewer import MapViewer
main_window = MapViewer()
main_window.show()
qgs.exec_()
qgs.exitQgis()

MapViewer.py: It includes a layers panel, a map canvas, a status bar with a button. Please review the script. I added all explanation to the script as comment (PART 1, PART 2, ...).

import sys
from qgis.core import *
from qgis.gui import *
from qgis.PyQt.QtWidgets import *

######## PART 1: Add QGIS plugin path to the system path ########
import sys
sys.path.append(r"C:\Users\user\AppData\Roaming\QGIS\QGIS3\profiles\ks3.12\python\plugins")
########

######## PART 2: Import plugin dialog ########
from NNJoin.NNJoin_gui import NNJoinDialog
########

class MapViewer(QMainWindow):
    def __init__(self):
        QMainWindow.__init__(self, None)

        self._canvas = QgsMapCanvas()
        self._root = QgsProject.instance().layerTreeRoot()

        self.bridge = QgsLayerTreeMapCanvasBridge(self._root, self._canvas)
        self.model = QgsLayerTreeModel(self._root)
        self.model.setFlag(0x25043)
        self.model.setFlag(QgsLayerTreeModel.ShowLegend)
        self.layer_treeview = QgsLayerTreeView()
        self.layer_treeview.setModel(self.model)

        self.layer_tree_dock = QDockWidget("Layers")
        self.layer_tree_dock.setObjectName("layers")
        self.layer_tree_dock.setFeatures(QDockWidget.NoDockWidgetFeatures)
        self.layer_tree_dock.setWidget(self.layer_treeview)

        self.splitter = QSplitter()
        self.splitter.addWidget(self.layer_tree_dock)
        self.splitter.addWidget(self._canvas)
        self.splitter.setCollapsible(0, False)
        self.splitter.setStretchFactor(1, 1)

        self.layout = QHBoxLayout()
        self.layout.addWidget(self.splitter)
        self.contents = QWidget()
        self.contents.setLayout(self.layout)
        self.setCentralWidget(self.contents)

        ######## PART 3: Add messagebar to main window ########
        # We need this, because parent widget of NNJoin dialog
        # calls this messagebar.
        self.bar = QgsMessageBar()
        self.statusBar().addWidget(self.bar)
        self.messageBar = lambda: self.bar
        ########

        self.button = QPushButton("NNJoin")
        self.button.setMaximumHeight(30)
        self.button.setMaximumWidth(80)
        self.statusBar().addPermanentWidget(self.button)
        ######## PART 4: Connect click event of button to method ########
        self.button.clicked.connect(self.open_nnjoin_window)
        self.load_layers()

        ######## PART 5: Create an instance of plugin dioalg ########
        # 'self' is used here instead of 'self.iface' in plugin
        self.dlg = NNJoinDialog(self)
        ########

    def load_layers(self):
        # Adding two sample layers
        self.layer1 = QgsVectorLayer("file1.shp", 'Layer1', 'ogr')
        self.layer2 = QgsVectorLayer("file2.shp", 'Layer2', 'ogr')

        layers = [self.layer1, self.layer2]
        QgsProject.instance().addMapLayers(layers)
        self._canvas.setExtent(self.layer1.extent())
        self._canvas.setLayers(layers)

    ######## PART 6: Initialising NNJoin dialog ########
    # this method runs by clicking the button
    # this part was copied from NNJoin source code and modified
    def open_nnjoin_window(self):
        self.dlg.progressBar.setValue(0.0)
        self.dlg.outputDataset.setText('Result')

        layers = QgsProject.instance().mapLayers()
        layerslist = []
        for id in layers.keys():
            if layers[id].type() == QgsMapLayer.VectorLayer:
                if not layers[id].isValid():
                    QMessageBox.information(None, 'Information',
                        'Layer ' + layers[id].name() + ' is not valid')
                if layers[id].wkbType() != QgsWkbTypes.NoGeometry:
                    layerslist.append((layers[id].name(), id))

        self.dlg.inputVectorLayer.clear()
        for layerdescription in layerslist:
            self.dlg.inputVectorLayer.addItem(layerdescription[0],
                                        layerdescription[1])
        self.dlg.joinVectorLayer.clear()

        for layerdescription in layerslist:
            self.dlg.joinVectorLayer.addItem(layerdescription[0],
                                        layerdescription[1])

        self.dlg.show()
        self.dlg.exec_()
    ########

Note that NNJoin plugin doesn't return any layer reference, but just add a layer to QgsProject instance. So you should get the returned layer using QgsProject.instance().mapLayersByName("LAYER_NAME").

  • I will add "Solution 2" (how to do without opening NNJoin dialog), if I can and if I have time. – Kadir Şahbaz Mar 17 '20 at 21:09
  • Hello Kadir, thanks a lot for the answer so far! When I have time (in 1 or 2 days) I will work through your solution and see if it works for me. – DGIS Mar 19 '20 at 11:25
4

There's a few posts like this one which mentions that it may be difficult to call plugins from a standalone script especially if they're designed around an interface (iface).


You should be able to call the Join Attributes by Nearest tool but note that this was released in QGIS 3.8 so you may need to update your version.

  • 1
    mmmh ok I see, thanks for the link. – DGIS Mar 17 '20 at 11:42

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.