Vytvoření zásuvného modulu

Zásuvné moduly, tzv. pluginy představují doplňkové nástroje. Jejich úlohou je rozšiřovat funkčnost a širokou škálu použití QGIS. Úvod do této problematiky je součástí školení QGIS pro začátečníky, kde se kromě jiného píše, že v současnosti existuje pro QGIS více než 300 zásuvných modulů napsaných v programovacím jazyku Python či C++.

V mnohých případech však může nastat situace, kdy žádný z existujících zásuvných modulů nesplňuje funkcionalitu jakou bychom právě potřebovali. Úroveň rozšiřovaní funkcionality QGIS je různorodá. Za pomoci jazyka Python může jít o přidání jednoduchého tlačítka až po tvorbu sofistikovaných nástrojů.

V následující části načrtneme návod jak si vlastní plugin vytvořit a postup následně odzkoušíme na jednoduchém reálném příkladě. Vytvoříme zásuvný modul s názvem Save Views, který exportuje grafický výstup ve formě obrázků ve formátu PNG pro každý prvek ve vybrané vektorové vrstvě do zvoleného výstupního adresáře.

Užitečné odkazy

Potřebné nástroje

I. Qt Creator

Pro tvorbu nového pluginu budeme potřebovat Qt Creator, což je aplikace vývojového frameworku s názvem Qt. Tento nástroj, resp. jeho součást Qt Designer, využijeme pro tvorbu uživatelského rozhraní nového modulu.

II. Python rozhraní pro Qt

Vzhledem k tomu, že budeme vyvíjet plugin v programovacím jazyku Python, musíme nainstalovat Python rozhraní, angl. Python bindings pro Qt. Pro tvorbu zásuvných modulů je potřeba pyrcc4.

Poznámka

Způsob instalace se v tomto případě liší podle platformy. Na Windows je možné stáhnout instalátor OSGeo4W, vybrat volbu Express Desktop a nainstalovat balík QGIS. Po instalaci je nástroj pyrcc4 dostupný v rámci OSGeo4W Shell. Na Mac OS je potřeba nainstalovat správce balíčků Homebrew a doinstalovat balíček PyQt. V případě Linuxu jde o balíček python-qt4. V Ubuntu jej např. nainstalujeme příkazem sudo apt-get install python-qt4.

III. Textový editor

Vhodný textový editor anebo integrované vývojové prostředí (IDE) jsou pro psaní zdrojového kódu důležité. Mezi oblíbené editory patří například Sublime Text, Vim, Emacs, Notepad++, TextWrangler, IDLE, Atom, Aquamacs, GNU Nano, Kate, gedit, prostředí Spyder či PyCharm a podobně.

IV. Zásuvný modul Plugin Builder

Tento velmi užitečný zásuvný modul do QGISu nám pomůže vytvořit všechny potřebné soubory a standardní podobu kódu pro budoucí plugin. Nainstalujeme jej klasickým způsobem pomocí správce zásuvných modulů v QGISu, viz. školení QGIS pro začátečníky.

V. Zásuvný modul Reloader plugin

Díky tomuto pluginu nemusíme při každé změně kódu restartovat QGIS. Změny se projeví ihned po jeho spuštění. Nainstalujeme jej klasickým způsobem pomocí správce zásuvných modulů v QGISu (pro jeho instalaci je potřeba povolit experimentální moduly v nastavení správce).

Pět základních kroků pro vytvoření pluginu Save Views

1. Vytvoření šablony nového pluginu pomocí zásuvného modulu Plugin Builder

2. Kompilace zdrojových kódů spojených s novým pluginem

3. Načtení nového pluginu ve správci zásuvných modulů

4. Vytvoření uživatelského rozhraní pomocí Qt Creator / Designer

5. Implementace funkcionality pluginu v rámci Python kódu

1. Vytvoření šablony nového pluginu

Po spuštění zásuvného modulu pro tvorbu pluginů se objeví dialogové okno, kde zadáme základní údaje o našem novém nástroji, viz. obr. 1. Na další stránce průvodce vyplníme podrobné informace o našem nástroji (About). V třetí části vybereme šablonu uživatelského prostředí, v našem případě Tool button with dialog, zadáme text, který se bude zobrazovat v menu. Nakonec vybereme, pod kterou položkou v menu náš nový plugin uživatel najde, například Vector. Na dalších stránkách průvodce je možné ovlivnit vytvoření dalších podpůrných souborů, vyplnit povinné a volitelné informace, například domovskou stránku, repositář modulu se zdrojovými kódy, označit nástroj jako experimentální a podobně.

../_images/np_plugin_builder.png

Obrázek 1: Dialogové okno zásuvného modulu na tvorbu pluginů.

Následně se objeví okno, kde je potřebné zadat cestu do kterého adresář s nástrojem uložíme (obr. 2). Obvykle to je .qgis2/python/plugins umístěný v domovském adresáři uživatele. Jeho umístění se liší v závislosti na platformě.

../_images/np_plugin_dir.png

Obrázek 2: Adresář obsahující všechny nainstalované zásuvné moduly QGIS.

Po tomto kroku se objeví potvrzující dialog, tzv. Plugin Builder Results obsahující souhrnné informace.

2. Kompilace

Přepneme se do adresáře, kde byl plugin SaveViews vytvořen. Pod Linuxem například pomocí příkazu cd .qgis2/python/plugins/SaveViews a spustíme make. Tento příkaz kompiluje tzv. Resource Collection File (*.qrc). Jde vlastně o spuštění výše uvedeného pyrcc4.

3. Načtení nového pluginu ve správci zásuvných modulů

Po restartu QGISu by měl být v sekci Zásuvné moduly ‣ Spravovat a instalovat zásuvné moduly viditelný i plugin Save Views. Zaškrtnutím box_yes se objeví jeho ikona npicon v hlavní liště tak jako jsme zadali, tj. v sekci Vector. (obr. 3).

../_images/np_plugin_menu.png

Obrázek 3: Nový plugin dostupný pod položkou Vector.

Spuštěním otevřeme dialog nástroje, který obsahuje tlačítka Cancel a OK (obr. 4).

../_images/np_plugin_dlg.png

Obrázek 4: Dialogové okno modulu Save Views po prvním spuštění.

Tip

V této fázi je dobré se zamyslet nad funkcionalitou pluginu, jaký bude typ vstupních dat a podobně. Na základě našich požadavků je vhodné si vytvořit testovací sadu.

Vytvoříme si jednoduchý projekt v QGISu, který bude obsahovat několik vektorových vrstev. Na obr. 5 jsou zobrazeny například požární stanice, železnice, kraje, velkoplošné území a státní hranice České republiky.

../_images/np_project.png

Obrázek 5: Příklad projektu s vektorovými vrstvami v QGIS.

4. Vytvoření uživatelského rozhraní pomocí Qt Creator

Vzhled a elementy dialogového okna pluginu budeme upravovat v programu Qt Creator / Designer. V hlavní liště zvolíme File ‣ Open File or Project a otevřeme soubor s příponou *.ui. V našem případě save_views_dialog_base.ui, který najdeme v adresáři vytvořeného pluginu. Na obr. 6 je znázorněné prozatím prázdné okno s objekty (tzv. widgety) SaveViewsDialogBase a button_box. Pomocí metody drag-and-drop je možné z levého panelu přidávat další objekty a jejich názvy a vlastnosti měnit v pravé části okna aplikace Qt Creator / Designer.

../_images/np_qt_creator.png

Obrázek 6: Dialogové okno vytvářeného pluginu v prostředí aplikace Qt Creator.

První dva objekty (widgety), které přidáme budou tzv. Combo Box z kategorie Input Widgets a Label z kategorie Display Widgets. V pravém panelu předvolený text objektu label změníme na Select a layer (obr. 7).

../_images/np_qt_label.png

Obrázek 7: Editace objektů dialogového okna.

Po uložení File ‣ Save přejdeme do prostředí QGIS, kde použijeme plugin Plugin Reloader. V Choose a plugin to be reloaded nastavíme SaveViews (obr. 8) a plugin spustíme. Tím se aktualizuje podoba našeho pluginu. Po kliknutí na ikonu Save Views se otevře okno totožné s návrhem na obr. 7.

../_images/np_plugin_reloader.png

Obrázek 8: Konfigurace zásuvného modulu Plugin Reloader.

5. Implementace funkcionality nástroje a další úpravy

Řekněme, že chceme, aby se po spuštění pluginu Combo Box automaticky naplnil vektorovými vrstvami aktuálního projektu. Hlavním souborem, který se stará o logiku jednotlivých objektů, je v našem případě save_views.py. Otevřeme jej v textovém editoru a najdeme metodu run. Tato metoda se spouští při každém startu pluginu. Do jejího těla (obr. 9) umístíme následující kód.

# populate the Combo Box with the layers loaded in QGIS
self.dlg.comboBox.clear()
layers = self.iface.legendInterface().layers()
layer_list = []
for layer in layers:
    layer_list.append(layer.name())
self.dlg.comboBox.addItems(layer_list)
../_images/np_run_method.png

Obrázek 9: Editace kódu v jazyku Python s cílem naplnit Combo Box vektorovými vrstvami.

Na obr. 10 je vidět, že po restartu a novém spuštění Save Views se změny úspěšně projeví.

../_images/np_cb_filled.png

Obrázek 10: Vzhled dialogového okna po změnách ve zdrojovém kódu.

Obdobně vložíme do okna další elementy a přiřadíme jim příslušnou funkcionalitu. Kromě popisu Select output directory půjde o objekty Line Edit a Tool Button. Pro button_box ve vlastnostech změníme tlačítko OK na Save All, nastavíme přiměřené rozměry pro každý element a upravený soubor *.ui uložíme. Důležité jsou názvy jednotlivých objektů, viz. obr. 11, budeme je ještě potřebovat.

../_images/np_final_dlg.png

Obrázek 11: Finální návrh dialogového okna pluginu Save Views.

Poznámka

Pokud je uživatelské rozhraní definováno více objekty (widgety) je vhodné je rozumně pojmenovat. V případě našeho jednoduchého modulu si vystačíme s předvolenými názvy.

V dalším kroku opět editujeme soubor save_views.py. Potřebujeme přidat kód, který zabezpečí, aby se po kliknutí na tlačítko ... otevřel dialog, ve kterém zvolíme adresář pro uložení výsledných obrazových souborů pro každý prvek ve vybrané vektorové vrstvě. O tuto funkcionalitu se postará metoda select_output_directory(). Přidáme ji například nad metodu run() (obr. 12).

# open directory browser and populate the line edit widget
def select_output_dir(self):
    self.dirname = QFileDialog.getExistingDirectory(self.dlg, "Select directory ","/home")
    self.dlg.lineEdit.setText(self.dirname)
../_images/np_select_output_dir.png

Obrázek 12: Metoda, která otevře dialog pro výběr výstupního adresáře.

Do sekce import na začátek souboru je nutné přidat QFileDialog jako from PyQt4.QtGui import QAction, QIcon, QFileDialog. Následně propojíme metodu select_output_dir() s tlačítkem toolButton (tlačítko ...) přidáním těchto řádků do metody __init__(). Soubor uložíme, plugin restartujeme a vyzkoušíme (obr. 13).

# clear the previously loaded text (if any) in the line edit widget
self.dlg.lineEdit.clear()
# connect the select_output_file method to the clicked signal of the tool button widget
self.dlg.toolButton.clicked.connect(self.select_output_dir)
../_images/np_skuska_1.png

Obrázek 13: Načtení adresáře pro grafické výstupy pomocí nového pluginu.

Posledním krokem je změnit to, aby se po kliknutí na tlačítko Save all opravdu provedlo, co chceme. Začneme importem QColor a QPixmap. Vytvoříme novou metodu save_views(). Potom vyhledáme metodu run() a najdeme řádek obsahující pass. Ten nahradíme volání této funkce.

def save_views(self, layers):
     # save graphical output for every row in attribute table
     selectedLayerIndex = self.dlg.comboBox.currentIndex()
     selectedLayerName = self.dlg.comboBox.currentText()
     selectedLayer = layers[selectedLayerIndex]

     frame_count = selectedLayer.dataProvider().featureCount()

     if frame_count <= 1:
         print "Layer must have more than one feature!"
     else:
         for feature in range(int(frame_count)):
             selection = [int(feature)]
             selectedLayer.setSelectedFeatures(selection)
             self.iface.mapCanvas().setSelectionColor(QColor("transparent"));
             box = selectedLayer.boundingBoxOfSelected()
             self.iface.mapCanvas().setExtent(box)
             pixmap = QPixmap(self.iface.mapCanvas().mapSettings().outputSize().width(),
             self.iface.mapCanvas().mapSettings().outputSize().height())
             mapfile = self.dirname + "/" + selectedLayerName + "_" + format(feature, "03d") + ".png"
             self.iface.mapCanvas().saveAsImage(mapfile, pixmap)
             selectedLayer.removeSelection()

         # save also full extend of vector layer
         canvas = self.iface.mapCanvas()
         canvas.setExtent(selectedLayer.extent())
         pixmap = QPixmap(self.iface.mapCanvas().mapSettings().outputSize().width(),
         self.iface.mapCanvas().mapSettings().outputSize().height())
         mapfile = self.dirname + "/" + selectedLayerName + "_full" + ".png"
         self.iface.mapCanvas().saveAsImage(mapfile, pixmap)
../_images/np_run-code.png

Obrázek 14: Doplnění kódu do metody run().

Grafické výstupy po aplikovaní na vrstvu krajů jsou zobrazeny na obr. 15. Jejich názvy v adresáři závisí na názvu konkrétní vektorové vrstvy. Liší se pouze pořadovým číslem. Kompletní obsah výsledného souboru save_views.py je uvedený níže.

../_images/np_plugin_result.png

Obrázek 15: Grafické soubory uložené ve zvoleném adresáři pro vektorovou vrstvu krajů České republiky.

Tip

V případě, že chceme změnit ikonu, stačí nový soubor s ikonkou, např. obr. 16, uložit do adresáře ~/.qgis2/python/plugins/SaveViews jako soubor icon.png a spustit příkaz make clean && make v příkazové řádce. Nakonec restartujeme plugin pomocí modulu Plugin Reloader.

../_images/np_new_icon.png

Obrázek 16: Příklad nové ikonky

Výsledný soubor save_views.py je ke stažení také zde.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
# -*- coding: utf-8 -*-
"""
/***************************************************************************
 SaveViews
                                 A QGIS plugin
 This plugin saves map image for every feature in attribute table of vector layer.
                              -------------------
        begin                : 2016-03-06
        git sha              : $Format:%H$
        copyright            : (C) 2016 by GISMentors
        email                : info@gismentors.eu
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 ***************************************************************************/
"""
from PyQt4.QtCore import QSettings, QTranslator, qVersion, QCoreApplication
from PyQt4.QtGui import QAction, QIcon, QFileDialog, QColor, QPixmap
# Initialize Qt resources from file resources.py
import resources
# Import the code for the dialog
from save_views_dialog import SaveViewsDialog
import os.path
from qgis.core import *


class SaveViews:
    """QGIS Plugin Implementation."""

    def __init__(self, iface):
        """Constructor.

        :param iface: An interface instance that will be passed to this class
            which provides the hook by which you can manipulate the QGIS
            application at run time.
        :type iface: QgsInterface
        """
        # Save reference to the QGIS interface
        self.iface = iface
        # initialize plugin directory
        self.plugin_dir = os.path.dirname(__file__)
        # initialize locale
        locale = QSettings().value('locale/userLocale')[0:2]
        locale_path = os.path.join(
            self.plugin_dir,
            'i18n',
            'SaveViews_{}.qm'.format(locale))

        if os.path.exists(locale_path):
            self.translator = QTranslator()
            self.translator.load(locale_path)

            if qVersion() > '4.3.3':
                QCoreApplication.installTranslator(self.translator)

        # Create the dialog (after translation) and keep reference
        self.dlg = SaveViewsDialog()

        # Declare instance attributes
        self.actions = []
        self.menu = self.tr(u'&Save Views')
        # TODO: We are going to let the user set this up in a future iteration
        self.toolbar = self.iface.addToolBar(u'SaveViews')
        self.toolbar.setObjectName(u'SaveViews')

        # clear the previously loaded text (if any) in the line edit widget
        self.dlg.lineEdit.clear()
        # connect the select_output_file method to the clicked signal of the tool button widget
        self.dlg.toolButton.clicked.connect(self.select_output_dir)

    # noinspection PyMethodMayBeStatic
    def tr(self, message):
        """Get the translation for a string using Qt translation API.

        We implement this ourselves since we do not inherit QObject.

        :param message: String for translation.
        :type message: str, QString

        :returns: Translated version of message.
        :rtype: QString
        """
        # noinspection PyTypeChecker,PyArgumentList,PyCallByClass
        return QCoreApplication.translate('SaveViews', message)


    def add_action(
        self,
        icon_path,
        text,
        callback,
        enabled_flag=True,
        add_to_menu=True,
        add_to_toolbar=True,
        status_tip=None,
        whats_this=None,
        parent=None):
        """Add a toolbar icon to the toolbar.

        :param icon_path: Path to the icon for this action. Can be a resource
            path (e.g. ':/plugins/foo/bar.png') or a normal file system path.
        :type icon_path: str

        :param text: Text that should be shown in menu items for this action.
        :type text: str

        :param callback: Function to be called when the action is triggered.
        :type callback: function

        :param enabled_flag: A flag indicating if the action should be enabled
            by default. Defaults to True.
        :type enabled_flag: bool

        :param add_to_menu: Flag indicating whether the action should also
            be added to the menu. Defaults to True.
        :type add_to_menu: bool

        :param add_to_toolbar: Flag indicating whether the action should also
            be added to the toolbar. Defaults to True.
        :type add_to_toolbar: bool

        :param status_tip: Optional text to show in a popup when mouse pointer
            hovers over the action.
        :type status_tip: str

        :param parent: Parent widget for the new action. Defaults None.
        :type parent: QWidget

        :param whats_this: Optional text to show in the status bar when the
            mouse pointer hovers over the action.

        :returns: The action that was created. Note that the action is also
            added to self.actions list.
        :rtype: QAction
        """

        icon = QIcon(icon_path)
        action = QAction(icon, text, parent)
        action.triggered.connect(callback)
        action.setEnabled(enabled_flag)

        if status_tip is not None:
            action.setStatusTip(status_tip)

        if whats_this is not None:
            action.setWhatsThis(whats_this)

        if add_to_toolbar:
            self.toolbar.addAction(action)

        if add_to_menu:
            self.iface.addPluginToVectorMenu(
                self.menu,
                action)

        self.actions.append(action)

        return action

    def initGui(self):
        """Create the menu entries and toolbar icons inside the QGIS GUI."""

        icon_path = ':/plugins/SaveViews/icon.png'
        self.add_action(
            icon_path,
            text=self.tr(u'Save Views'),
            callback=self.run,
            parent=self.iface.mainWindow())


    def unload(self):
        """Removes the plugin menu item and icon from QGIS GUI."""
        for action in self.actions:
            self.iface.removePluginVectorMenu(
                self.tr(u'&Save Views'),
                action)
            self.iface.removeToolBarIcon(action)
        # remove the toolbar
        del self.toolbar

    def select_output_dir(self):
        self.dirname = QFileDialog.getExistingDirectory(self.dlg, "Select directory ","/home")
        self.dlg.lineEdit.setText(self.dirname)

    def save_views(self, layers):
        selectedLayerIndex = self.dlg.comboBox.currentIndex()
        selectedLayerName = self.dlg.comboBox.currentText()
        selectedLayer = layers[selectedLayerIndex]

        frame_count = selectedLayer.dataProvider().featureCount()

        if frame_count <= 1:
            print "Layer must have more than one feature!"
        else:                
            for feature in range(int(frame_count)):
                selection = [int(feature)]
                selectedLayer.setSelectedFeatures(selection)
                self.iface.mapCanvas().setSelectionColor(QColor("transparent"));
                box = selectedLayer.boundingBoxOfSelected()
                self.iface.mapCanvas().setExtent(box)
                pixmap = QPixmap(self.iface.mapCanvas().mapSettings().outputSize().width(),
                                 self.iface.mapCanvas().mapSettings().outputSize().height())
                mapfile = self.dirname + "/" + selectedLayerName + "_" + format(feature, "03d") + ".png"
                self.iface.mapCanvas().saveAsImage(mapfile, pixmap)
                selectedLayer.removeSelection()

            # save also full extend of vector layer                       
            canvas = self.iface.mapCanvas()
            canvas.setExtent(selectedLayer.extent())
            pixmap = QPixmap(self.iface.mapCanvas().mapSettings().outputSize().width(),
                                 self.iface.mapCanvas().mapSettings().outputSize().height())
            mapfile = self.dirname + "/" + selectedLayerName + "_full" + ".png"
            self.iface.mapCanvas().saveAsImage(mapfile, pixmap)

    def run(self,qgis):
        """Run method that performs all the real work"""
        # populate the Combo Box with the layers loaded in QGIS
        self.dlg.comboBox.clear()
        layers = self.iface.legendInterface().layers()
        layer_list = []
        for layer in layers:
            layer_list.append(layer.name())
        self.dlg.comboBox.addItems(layer_list)

        # show the dialog
        self.dlg.show()
        # Run the dialog event loop
        result = self.dlg.exec_()
        # See if OK was pressed
        if result:
            # save graphical output for every row in attribute table
            self.save_views(layers)

Jiný příklad využití

Na obrázku obr. 17 je uveden projekt s katastrálními daty. Vyznačené jsou parcely, přes které budou procházet plánované inženýrské sítě.

../_images/np_kn_project.png

Obrázek 17: Znázornění parcel přes které májí procházet plánované inženýrské sítě.

Použitím pluginu Save Views můžeme pro každého vlastníka vyhodnotit grafické znázornění jeho parcely, na které bude zapsané věcné břemeno (obr. 18).

../_images/np_kn_project_views.png

Obrázek 18: Pohled na každou parcelu jako výsledek zásuvného modulu Save Views.