Přeskočit obsah

QGIS - tvorba zásuvného modulu

V této části se zaměříme na tvorbu zásuvných modulů pro software QGIS.

Založme zásuvný modul na posledním příkladu z minulého cvičení. Pro větší přehlednost tak učiníme v následujících krocích. Takový postup je doporučován z toho důvodu, že se řeší zvlášť možné problémy vzniknuvší při implementaci funkcionality a při tvorbě zásuvného modulu.

  1. tvorba funkce
    1. ze skriptu vytvoříme funkci
    2. parametrizujeme funkci
    3. rozšíříme funkci o další zamýšlenou funkcionalitu
  2. implementujme funkci jako součást zásuvného modulu

Tvorba funkce k vytvoření obalové vrstvy

Připomeňme si, jak vypadal skript, který vytvářel obalovou zónu kolem bodů z vrstvy lucas a vypisoval jejich obsah.

from qgis.core import QgsProject

# mapLayersByName() vraci seznam
for feature in QgsProject.instance().mapLayersByName('lucas')[0].getFeatures():
    if feature['obs_dist']:  # nutno pokud jsou nektere prvky NULL
        # druhym parametrem je pocet lomovych bodu pouzitych k vytvoreni kruznice
        # (jedna se o body navic ke ctyrem potrebnym pro ctverec)
        buffer = feature.geometry().buffer(feature["obs_dist"], 5)

        point = feature.geometry().asPoint()
        print(f'[{point.x()}, {point.y()}]: {buffer.area()} m2')

Tvorba funkce je jednoduchá.

from qgis.core import QgsProject

def lucas_buffer():
    # mapLayersByName() vraci seznam
    for feature in QgsProject.instance().mapLayersByName('lucas')[0].getFeatures():
        if feature['obs_dist']:  # nutno pokud jsou nektere prvky NULL
            # druhym parametrem je pocet lomovych bodu pouzitych k vytvoreni kruznice
            # (jedna se o body navic ke ctyrem potrebnym pro ctverec)
            buffer = feature.geometry().buffer(feature["obs_dist"], 5)

            point = feature.geometry().asPoint()
            print(f'[{point.x()}, {point.y()}]: {buffer.area()} m2')

if __name__ == '__main__':
    lucas_buffer()

Přestože však byla tvorba funkce jednoduchá, dává nám mnoho možností. Pro opětovné použití funkce na jiných vrstvách je zajisté záhodno ji parametrizovati. Zároveň v případě, že není vstupní vrstva nalezena, vyvoláme vyjímku.

from qgis.core import QgsProject

def lucas_buffer(input_layer):
    # mapLayersByName() vraci seznam
    layers = QgsProject.instance().mapLayersByName(input_layer)
    if not layers:
        raise Exception(f"Vrstva {input_layer} nenalezena")

    for feature in layers[0].getFeatures():
        if feature['obs_dist']:  # nutno pokud jsou nektere prvky NULL
            # druhym parametrem je pocet lomovych bodu pouzitych k vytvoreni kruznice
            # (jedna se o body navic ke ctyrem potrebnym pro ctverec)
            buffer = feature.geometry().buffer(feature["obs_dist"], 5)

            point = feature.geometry().asPoint()
            print(f'[{point.x()}, {point.y()}]: {buffer.area()} m2')

if __name__ == '__main__':
    lucas_buffer('lucas')

Úkol

Přepišme funkci tak, aby vyvolávala vyjímku i v případě, že je nalezeno více vrstev stejného názvu.

Vytvoření výstupní vektorové vrstvy

Vytvořme novou funkci create_new_layer, která na základě specifikace (typ geometrie a souřadnicový systém, struktura atributové tabulky vytvoří novou vektorovou vrstvu. Takto vytvořenou vrstvu na konci skriptu přidejme do mapové okna.

Dále upravme funkci lucas_buffer tak, aby vytvářela nové prvky obalové zóny a zapisovala je včetně relevatních atributů do nové vektorové vrstvy vytvořené voláním funkce create_new_layer.

from qgis.core import QgsProject, QgsVectorLayer, QgsField, QgsFeature
from PyQt5.QtCore import QVariant


def create_new_layer(layer_type, layer_name, layer_fields):
    new_layer = QgsVectorLayer(layer_type, layer_name, 'memory')
    for field_name, field_type in layer_fields.items():
        new_layer.dataProvider().addAttributes(
            (QgsField(field_name, field_type),)
        )

    new_layer.updateFields()

    return new_layer


def lucas_buffer(input_layer, output_layer):
    # mapLayersByName() vraci seznam
    layers = QgsProject.instance().mapLayersByName(input_layer)
    if not layers:
        raise Exception(f"Vrstva {input_layer} nenalezena")

    for feature in layers[0].getFeatures():
        if feature['obs_dist']:  # nutno pokud jsou nektere prvky NULL
            # druhym parametrem je pocet lomovych bodu pouzitych k vytvoreni kruznice
            # (jedna se o body navic ke ctyrem potrebnym pro ctverec)
            buffer = feature.geometry().buffer(feature["obs_dist"], 5)

            buffer_feature = QgsFeature()
            buffer_feature.setGeometry(buffer)
            buffer_feature.setAttributes([
                feature['point_id'], feature['lc1'],
                feature['obs_dist'], buffer.area()])
            output_layer.addFeature(buffer_feature)

        new_layer.updateExtents()

if __name__ == '__main__':
    new_layer = create_new_layer('Polygon?crs=epsg:3035', 'lucas - buffers', {
        'point_id': QVariant.Int, 'lc1': QVariant.String,
        'obs_dist': QVariant.Double, 'buffer_area': QVariant.Double})

    with edit(new_layer):
        lucas_buffer('lucas', new_layer)

    QgsProject.instance().addMapLayer(new_layer)

Úkol

Přepišme funkci tak, aby souřadnicový systém výstupní vrstvy byl odvozen z vektorové vrstvy vstupní.

Tvorba zásuvného modulu

Nejprve nainstalujme dva nové zásuvné moduly:

  • Plugin Builder - použijeme pro vytvoření šablony pluginu
  • Plugin Reloader - tento plugin umožní znovunačíst již zavedený zásuvný modul. To nám zásadně usnadní vývoj a testování našeho zásuvného modulu v prostředí QGISu.

Vytvoření šablony zásuvného modulu

Spustíme Plugins > Plugin Builder a pomocí jednoduchého průvodce vytvořme šablonu našeho zásuvného modulu.

V prvním kroku dejinujme:

  • Class name - název Python třídy implementující zásuvný modul (bez diakritiky, mezer a pod)
  • Plugin name - název zásuvného modulu, tak jak se bude zobrazovat v menu
  • Description - krátký popis modulu, tak jak se bude zobrazovat v dialogu Plugins
  • Module name - název Python modulu a zároveň adresáře se zásuvným modulem (bez diakritiky, mezer a pod)
  • Version number - verze zásuvného modulu
  • Minimum QGIS version - minimální verze QGIS nutná pro fungování zásuvného modulu
  • Author/Company - autor zásuvného modulu
  • Email address - kontakt na autora

Poznámka

Položky Description, Version number, Minimum QGIS version, Author/Company a Email adress lze jednoduše změnit po vytvoření zásuvného modulu v souboru metadata.txt.

V dalším kroku popišme funcionalitu zásuvného modulu. Tato informace se bude zobrazovat v dialogu Plugins. Popis lze změnit po vytvoření zásuvného modulu v souboru metadata.txt.

V následujícím kroku definujme způsob začlenění zásuvného modulu do prostředí QGISu. Máme tři možnosti:

  • Tool button with dialog - dialog s tlačítkem
  • Tool button with dock widget - připnutelné okno s tlačítkem
  • Processing provider - integrace do nástrojů zpracování

V našem případě zvolme připnutelné okno s tlačítkem.

Dále definujme podpůrné komponenty zásuvného modulu. Typicky je pro nás důležitá:

  • Internatialization v případě, že plánuje zásuvný modul lokalizovat do různých jazyků
  • pb_tool usnadní sestavení zásuvného modulu do formy zip archivu a jeho publikaci v repozitáři zásuvných modulů QGIS

Mezi povinné informace patří i odkaz na repozitár se zdrojovým kódem zásuvného modulu, na systém pro hlášení chyb, domovskou stránku a klíčová slova. Všechny tyto položky lze jednoduše změnit po vytvoření zásuvného modulu v souboru metadata.txt.

Na poslední stránce průvodce zvolíme adresář, do kterého se nově vytvořený plugin vytvoří.

Načtení zásuvného modulu v QGIS

Ve výchozím nastavení QGIS při startu vyhledává zásuvné moduly v profilu uživatele. V nastavení ale můžeme přidat i další adresáře, ve kterých bude zásuvné moduly vyhledávat. To se nám bude pří dalším vývoji a testování zásuvného modulu hodit.

V dialogu Settings > Options, záložce System sekci Enviroment definujme proměnou prostředí QGIS_PLUGINPATH. Tato proměnná bude odkazovat na adresář, ve kterém je umístěn podadresář se zásuvným modulem.

Todo

Přidat screenshot s načteným pluginem

Definice uživatelského rozhraní

Výchozí uživatelské rozhraní bude velice minimalistické. Bylo by vhodné jej doplnit o potřebné vstupní parametry:

  • vstupní vektorová vrstva
  • výstupní vektorová vrstva
  • tlačítko, které zásuvný modul spustí

Lze tak učiniti dvěma různými způsoby. Za využití programovacího jazyka Python (PyQt), nebo za pomoci nástroje Qt Designer. Přidejme jednotlivé prvky v programu Qt Designer. Tím si ukážeme jeho výhody (jednoduché a intuitivní ovládání), ale i nevýhody (jest vhodný pro rychlý návrh, ale nejsme v něm schopni vytvořit složitější signály a zdířky). Jeden, nikoli však jediný, z možných návrhů může nakonec vypadati následujícím způsobem.

Funkcionalita zásuvného modulu

Ačkoli jsme si vytvořili oku lahodící grafické uživatelské rozhraní, rádi bychom mu dodali potřebnou funkcionalitu. Upravme soubor fgis_plugin/fgis_plugin.py tak, aby se po stisknutí tlačítka Run spustila funkce lucas_buffer. Po vygenerování vypadá soubor následujícím způsobem.

# -*- coding: utf-8 -*-
"""
/***************************************************************************
 FGISPluginDockWidget
                                 A QGIS plugin
 QGIS plugin for the FGIS course
 Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/
                             -------------------
        begin                : 2024-03-13
        git sha              : $Format:%H$
        copyright            : (C) 2024 by 155FGIS
        email                : 155fgis@fsv.cvut.cz
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   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.                                   *
 *                                                                         *
 ***************************************************************************/
"""

import os

from qgis.PyQt import QtGui, QtWidgets, uic
from qgis.PyQt.QtCore import pyqtSignal

FORM_CLASS, _ = uic.loadUiType(os.path.join(
    os.path.dirname(__file__), 'fgis_plugin_dockwidget_base.ui'))


class FGISPluginDockWidget(QtWidgets.QDockWidget, FORM_CLASS):

    closingPlugin = pyqtSignal()

    def __init__(self, parent=None):
        """Constructor."""
        super(FGISPluginDockWidget, self).__init__(parent)
        # Set up the user interface from Designer.
        # After setupUI you can access any designer object by doing
        # self.<objectname>, and you can use autoconnect slots - see
        # http://doc.qt.io/qt-5/designer-using-a-ui-file.html
        # #widgets-and-dialogs-with-auto-connect
        self.setupUi(self)

    def closeEvent(self, event):
        self.closingPlugin.emit()
        event.accept()

Přidejme tlačítku run_button odpovídající signál. Abychom mohli importovat funkci lucas_buffer, uložme si náš dosavadní skript pod názvem lucas_tools do adresáře se zásuvným modulem. Po našich úravách by měl skript vypadati podobně jako v následujícím příkladu.

# -*- coding: utf-8 -*-
"""
/***************************************************************************
 FGISPluginDockWidget
                                 A QGIS plugin
 QGIS plugin for the FGIS course
 Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/
                             -------------------
        begin                : 2024-03-13
        git sha              : $Format:%H$
        copyright            : (C) 2024 by 155FGIS
        email                : 155fgis@fsv.cvut.cz
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   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.                                   *
 *                                                                         *
 ***************************************************************************/
"""

import os

from qgis.core import QgsProject, edit
from qgis.PyQt import QtGui, QtWidgets, uic
from qgis.PyQt.QtCore import pyqtSignal
from PyQt5.QtCore import QVariant

from .lucas_tools import create_new_layer, lucas_buffer

FORM_CLASS, _ = uic.loadUiType(os.path.join(
    os.path.dirname(__file__), 'fgis_plugin_dockwidget_base.ui'))


class FGISPluginDockWidget(QtWidgets.QDockWidget, FORM_CLASS):

    closingPlugin = pyqtSignal()

    def __init__(self, parent=None):
        """Constructor."""
        super(FGISPluginDockWidget, self).__init__(parent)
        # Set up the user interface from Designer.
        # After setupUI you can access any designer object by doing
        # self.<objectname>, and you can use autoconnect slots - see
        # http://doc.qt.io/qt-5/designer-using-a-ui-file.html
        # #widgets-and-dialogs-with-auto-connect
        self.setupUi(self)

        self.run_button.clicked.connect(self.run)

    def run(self):
        new_layer = create_new_layer('Polygon?crs=epsg:3035', self.output_layer.toPlainText(), {
            'point_id': QVariant.Int, 'lc1': QVariant.String,
            'obs_dist': QVariant.Double, 'buffer_area': QVariant.Double})

        with edit(new_layer):
            lucas_buffer(
                self.input_layer.currentText(),
                new_layer
            )

        QgsProject.instance().addMapLayer(new_layer)

    def closeEvent(self, event):
        self.closingPlugin.emit()
        event.accept()

Úkol

Upravte funkce v souboru lucas_tools tak, aby nebylo v fgis_plugin_dockwidget.py zapotřebí duplikovat kód pro definici nové vrstvy.

Úprava zásuvného modulu

Zásuvný modul rozšiřme o novou funkcionalitu - stažení fotografií pro vybrané LUCAS body.

Fotografie dostupné pro zvolené LUCAS body můžeme jednoduše stáhnout pomocí Python balíčku st-lucas. Tento balíček doinstalujeme:

python3 -m pip install st_lucas

Nejrpve vyzkoušejme stažení fotografií pro daný bod LUCAS a rok měření:

from zipfile import ZipFile
from st_lucas import LucasIO

point_id = 46682050
target_dir = "/home/martin/Downloads"

lucasio = LucasIO()
lucasio.data = "/home/martin/Downloads/lukas_sample.gpkg"
images = lucasio.get_images(2018, point_id)
filename = lucasio.download_images(images, target_dir)

with ZipFile(filename) as zip:
    zip.extractall(target_dir)

Na základě toho rozšířme zdrojový kód o novou funkci unzip_lucas_photos(). Dále modifikujme funkci lucas_buffer_photo() tab, aby fotografie byly staženy a rozbaleny do cílového adresáře v následující struktuře (pointid_year):

├── 46642052_2018
│   ├── E.jpg
│   ├── N.jpg
│   ├── P.jpg
│   ├── S.jpg
│   └── W.jpg
...

Do atributové tabulky výstupní vrstvy přidejme nový atribut photo odkazující na soubor P.jpg.

from pathlib import Path
from zipfile import ZipFile

from qgis.core import QgsProject, QgsVectorLayer, QgsField, QgsFeature
from PyQt5.QtCore import QVariant

from st_lucas import LucasIO

def create_new_layer(layer_type, layer_name, layer_fields):
    new_layer = QgsVectorLayer(layer_type, layer_name, 'memory')
    for field_name, field_type in layer_fields.items():
        new_layer.dataProvider().addAttributes(
            (QgsField(field_name, field_type),)
        )

    new_layer.updateFields()

    return new_layer


def unzip_lucas_photos(filename, output_dir):
    target_dir = output_dir / filename.stem
    with ZipFile(filename) as zip:
        zip.extractall(target_dir)

    return target_dir


def lucas_buffer(input_layer, output_layer, output_dir):
    # mapLayersByName() vraci seznam
    layers = QgsProject.instance().mapLayersByName(input_layer)
    if not layers:
        raise Exception(f"Vrstva {input_layer} nenalezena")

    lucasio = LucasIO()
    lucasio.data = layers[0].dataProvider().dataSourceUri().split("|")[0]

    for feature in layers[0].getFeatures():
        if feature['obs_dist']:  # nutno pokud jsou nektere prvky NULL
            # druhym parametrem je pocet lomovych bodu pouzitych k vytvoreni kruznice
            # (jedna se o body navic ke ctyrem potrebnym pro ctverec)
            buffer = feature.geometry().buffer(feature["obs_dist"], 5)

            images = lucasio.get_images(feature["survey_year"], feature["point_id"])
            filename = lucasio.download_images(images, output_dir)
            photo_path = unzip_lucas_photos(Path(filename), Path(output_dir))

            buffer_feature = QgsFeature()
            buffer_feature.setGeometry(buffer)
            buffer_feature.setAttributes([
                feature['point_id'], feature['lc1'],
                feature['obs_dist'], buffer.area(),
                str(photo_path / "P.jpg")])
            output_layer.addFeature(buffer_feature)

        new_layer.updateExtents()

new_layer = create_new_layer('Polygon?crs=epsg:3035', 'lucas - buffers', {
    'point_id': QVariant.Int, 'lc1': QVariant.String,
    'obs_dist': QVariant.Double, 'buffer_area': QVariant.Double,
    'photo': QVariant.String})

output_dir = "/tmp/lucas_photos"
if not Path(output_dir).exists():
    os.makedirs(output_dir)
with edit(new_layer):
    lucas_buffer('lucas', new_layer, output_dir)

QgsProject.instance().addMapLayer(new_layer)

Úkol

Upravme funkci tak, aby nestahovala zip archiv s fotografiemi opakovaně.

Tip

Fotografie může být zobrazena přímo v atributové tabulce.

Ve vlastnostech vrstvy (Attributes Form) nastavíme: Widget Type na Attachment a Integrated Document Viewer Type na Image.

Po otevření atributové tabulky se přepneme do režimu formuláře.

Úkol

Upravte zásuvný modul tak, aby byl attribut photo nastaven automaticky na Attachment a Image.

Úkol

Vyzkoušejte vytvořit další zásuvný modul. Můžete čerpat z materiálů školení skupiny GISMentors.

Publikace zásuvného modulu

Vytvořený zásuvný modul můžeme s kolegy sdílet ve formě zip archivu. Ten lze jednoduše nainstalovat z Plugins > Manage and Install Plugins... > Install from ZIP.

Zip archiv se zásuvným modulem lze vytvořit pomocí nástroje pb_tool. Nejprve jej doinstalujeme:

python3 -m pip install pb_tool

Nástroj poskytuje celou řadu voleb (podrobné informace můžete vypsat pomocí pb_tool --help). Nám postačí pb_tool zip pro vytvoření zip archivu.

V našem případě jsme do struktury zásuvného modulu přidali soubor lucas_tools.py. Ten musíme začlenit do konfigurace pb_tool.cfg:

python_files: __init__.py fgis_plugin.py fgis_plugin_dockwidget.py lucas_tools.py

Poznámka

Pokud nemáme sepsánu dokumentaci (adresář help), tak můžeme její sestavení vypnout. Zakomentujeme řádek:

# dir: help/build/html

Poznámka

V případě, že jste měnili ikonku zásuvného modulu (icon.png) je nutné spustit

pb_tool compile

Poté můžeme vytvořit zip archiv.

pb_tool zip

Soubor se vytvoří v adresáři zip_build.

Úkol

Vyzkoušejte instalaci zásvuného modulu z vytvořeného zip archivu.

QGIS plugins web portal

V případě, že jsme se zásuvným modulem spokojeni a chceme jej sdílet s ostatními uživateli QGIS, tak je tu možnost jej publikovat na webovém portálu QGIS. Zásuvný modul bude ve výsledku snadno instalovatelný z Plugins > Manage and Install Plugins....

Postup: Nejprve si vytvoříme na webovém portálu učet https://plugins.qgis.org/accounts/login/ a poté zásuvný modul nahrajeme ve formě zip archivu. Zásuvný modul projde hodnocením. V případě, že splní všechny podmínky (licence GNU GPL, dokumentace, ...) bude schálen a tím se stane snadno dostupný pro další uživatele QGISu.