Cargando plugins de QGIS en el visor de PyQGIS

Hasta el momento conocemos que con PyQGIS se pueden desarrollar aplicaciones independientes en el escritorio y plugins para QGIS. Pero… ¿Será posible cargar los plugins de QGIS desde las aplicaciones construidas con PyQGIS?

Con la adopción de Python como lenguaje de personalización, el proyecto QGIS ha facilitado en gran medida la utilización de su API (Interfaz de Programación de Aplicaciones). Por medio de las bindings de Python para QGis (llamadas PyQGIS) es posible llevar a cabo lo siguiente:

  • Desarrollar aplicaciones independientes en el escritorio.
  • Desarrollar plugins para QGIS.
  • Ejecutar scripts o sentencias de Python dentro de QGIS.

En esta ocasión integraremos dos de los casos, cargando plugins de QGIS en el visor de PyQGIS construido en un blog anterior. El resultado es el siguiente:

Plugins de QGIS en el visor de PyQGIS
Figura 1. Plugins de QGIS en el visor de PyQGIS.

¿Cómo se comunica QGIS con sus Plugins?

Para poder hacer uso de PyQGIS es indispensable conocer las diferentes librerías de clases que ofrece QGIS. Para esto, podemos consultar la documentación en línea disponible en este enlace: http://doc.qgis.org

QGIS se comunica con los plugins a través de la clase QgsInterface, la cual hace públicos ciertos métodos y propiedades de la clase principal de la aplicación, llamada QgisApp. Las únicas funcionalidades que puede realizar un plugin son las expuestas por QgsInterface.

Para aplicaciones independientes en el escritorio puede crearse una clase análoga a QgsInterface que exponga métodos y propiedades de la clase principal de la aplicación. En el visor de PyQGIS realizado en un blog anterior, la clase principal es VisorShapefiles que se encuentra en el módulo VisorShapefiles.py

QgsInterface en Python

La clase QgsInterface en Python debe hacer uso de métodos y propiedades existentes en la clase principal de la aplicación. El visor de PyQGIS que se realizó previamente es limitado en funcionalidades, por lo que puede que la clase QgsInterface que presento a continuación no sea directamente aplicable en el visor, entonces, para poder hacer uso efectivo del siguiente código se debe trabajar un poco más en el visor de PyQGIS agregando por ejemplo, una tabla de contenido para administrar las capas del mapa.

La clase QgsInterface en Python tiene la siguiente forma:

1.   # -*- coding:utf-8 -*-
2.
3. from PyQt4.QtCore import QObject, QString
4. from PyQt4.QtGui import QMenu
5.
6. class QgisInterface( QObject ):
7. """
8. Class to expose PyQGIS functionalities to plugins.
9.
10. visor: PyQGisApp instance. NOTE: visor must have several methods to
11. can be called by QgisInterface. Otherwise this won't work.
12. canvas: QgsMapCanvas instance.
13. """
14. def __init__( self, visor, canvas ):
15. QObject.__init__( self )
16. self.pyQgisApp = visor
17. self.canvas = canvas
18.
19. # Initialize to know if exists the plugins ToolBar
20. self.toolBarPlugins = None
21.
22. def zoomFull( self ):
23. """ Zoom to the map full extent """
24. self.canvas.zoomToFullExtent()
25.
26. def zoomToPrevious( self ):
27. """ Zoom to previous view extent """
28. self.canvas.zoomToPreviousExtent()
29.
30. def zoomToNext( self ):
31. """ Zoom to next view extent """
32. self.canvas.zoomToNextExtent()
33.
34. def zoomToActiveLayer( self ):
35. """ Zoom to extent of the active layer """
36. self.pyQgisApp.zoomACapaActiva()
37.
38. def activeLayer( self ):
39. """ Get pointer to the active layer (layer selected in the legend) """
40. if self.pyQgisApp.activeLayer():
41. return self.pyQgisApp.activeLayer().layer()
42. return None
43.
44. def addToolBarIcon( self, qAction ):
45. """ Add an icon to the plugins toolbar """
46. if not self.toolBarPlugins:
47. self.toolBarPlugins = self.addToolBar( "Plugins" )
48. self.toolBarPlugins.addAction( qAction )
49.
50. def removeToolBarIcon( self, qAction ):
51. """ Remove an action (icon) from the plugin toolbar """
52. if not self.toolBarPlugins:
53. self.toolBarPlugins = self.addToolBar( "Plugins" )
54. self.toolBarPlugins.removeAction( qAction )
55.
56. def addToolBar( self, name ):
57. """ Add toolbar with specified name """
58. toolBar = self.pyQgisApp.addToolBar( name )
59. toolBar.setObjectName( name )
60. return toolBar
61.
62. def mapCanvas( self ):
63. """ Return a pointer to the map canvas """
64. return self.canvas
65.
66. def mainWindow( self ):
67. """ Return a pointer to the main window. """
68. return self.pyQgisApp
69.
70. def addPluginToMenu( self, name, action ):
71. """ Add action to the plugins menu """
72. menu = self.getPluginMenu( name )
73. menu.addAction( action )
74.
75. def removePluginMenu( self, name, action ):
76. """ Remove action from the plugins menu """
77. menu = self.getPluginMenu( name )
78. menu.removeAction( action )
79.
80. def addDockWidget( self, area, dockwidget ):
81. """ Add a dock widget to the main window """
82. self.pyQgisApp.addDockWidget( area, dockwidget )
83. dockwidget.show()
84.
85. # refresh the map canvas
86. self.canvas.refresh()
87.
88. def removeDockWidget( self, dockwidget ):
89. """ Remove a dock widget from main window (doesn't delete it) """
90. self.pyQgisApp.removeDockWidget( dockwidget )
91.
92. # refresh the map canvas
93. self.canvas.refresh()
94.
95. def refreshLegend( self, mapLayer ):
96. """ Refresh the layer legend """
97. self.pyQgisApp.refreshLayerSymbology( mapLayer )
98.
99. def pluginMenu( self ):
100. """ Return the main plugins menu """
101. return self.pyQgisApp.menuPlugins
102.
103. def getPluginMenu( self, menuName ):
104. """ Return the secondary menu which owns to a plugin """
105. before = None
106.
107. if self.pluginMenu():
108. actions = self.pluginMenu().actions()
109. for action in actions:
110. comp = QString( menuName ).localeAwareCompare( action.text() )
111. if ( comp < 0 ):
112. before = action # Add item before this one
113. break
114. elif ( comp == 0 ):
115. # Plugin menu item already exists
116. return action.menu()
117.
118. # It doesn't exist, so create it
119. menu = QMenu( menuName, self.pluginMenu() )
120. if before:
121. self.pluginMenu().insertMenu( before, menu )
122. else:
123. self.pluginMenu().addMenu( menu )
124. return menu
125.
126. # TODO: Implement the following methods.
127. #def addVectorLayer( vectorLayerPath, baseName, providerKey ):
128. #def addRasterLayer( rasterLayerPath, baseName = QString() ):
129. #def addRasterLayer(url,layerName,providerKey,layers,styles,format,crs):
130. #virtual bool addProject( QString theProject ) = 0;
131. #virtual void newProject( bool thePromptToSaveFlag = false ) = 0
132. #virtual QList<QgsComposerView*> activeComposers() = 0

Cargando plugins al visor de PyQGIS

Los plugins de QGIS pueden descargarse de la dirección: https://plugins.qgis.org/

Para implementarlos en el visor de PyQGIS creamos una carpeta llamada plugins en el directorio raíz de la aplicación y copiamos los plugins que queramos tener en el visor. La estructura será similar a la siguiente:

Así debe lucir la carpeta 'Plugins'
Figura 2. Estructura de la carpeta plugins.

Para hacer más flexible el cargue de plugins es ideal contar con una clase que los administre, recorriendo de forma automática la carpeta plugins y realizando el cargue de cada uno de ellos. De esta manera, los plugins que se carguen en el visor serán los que se encuentren en la carpeta. Si agregamos otro plugin a la carpeta este será cargado en el visor y si queremos prescindir de un plugin lo removeremos de la misma.

El módulo plugins_manager.py se encarga de dicha tarea:

1.  # -*- encoding:utf-8 -*-
2.
3. import imp, os, fileinput
4. from PyQt4.QtGui import QMenu
5.
6. from qgisInterface import QgisInterface
7.
8. class Plugins():
9. """ Class to load plugins to the main application """
10. def __init__( self, visor, canvas ):
11. self.qgisInterface = QgisInterface( visor, canvas )
12. self.pyQgisApp = visor
13.
14. self.pyQgisApp.menuPlugins = None
15. self.plugins = []
16. self.cargarPlugins()
17.
18. def cargarPlugins( self ):
19. """ Validate there is a Plugins folder and load the plugins to the app """
20. dir_plugins = os.path.join( os.path.abspath( '' ), 'plugins' )
21.
22. if os.path.exists( dir_plugins ):
23. for root, dirs, files in os.walk( dir_plugins ):
24. bPlugIn = False
25.
26. if not dir_plugins == root: # Subfolders in folder Plugins
27. if '__init__.py' in files: # Filter plugins folders
28.
29. # Verify that __init__.py file belongs to a plugin
30. for line in fileinput.input( os.path.join( root, '__init__.py' ) ):
31. linea = line.strip()
32. if linea == ("def classFactory(iface):") or linea == "def classFactory( iface ):":
33. bPlugIn = True
34. break
35. fileinput.close()
36.
37. if bPlugIn:
38. if not self.pyQgisApp.menuPlugins:
39. self.crearMenuPlugins()
40. print "+----------------=== LOADED PLUGINS ===----------------+"
41.
42. # Load the plugin
43. nombre_plugin = os.path.basename( root )
44. print ' * ',nombre_plugin,':',
45. f, filename, description = imp.find_module( nombre_plugin, [ 'plugins' ] )
46.
47. try: # Load and initialize the plugin
48. paquete = imp.load_module( nombre_plugin, f, filename, description )
49. self.plugins.append( paquete.classFactory( self.qgisInterface ) )
50. except Exception, e:
51. print ' ERROR!:',e
52. else:
53. self.plugins[ -1 ].initGui()
54. print ' OK!'
55. else:
56. print "Plugins folder not found."
57.
58. def crearMenuPlugins( self ):
59. """ Create the Plugins menu on menu bar """
60. self.pyQgisApp.menuPlugins = QMenu( "&Plugins", self.pyQgisApp.menuBar )
61. actions = self.pyQgisApp.menuBar.actions()
62. lastAction = actions[ len( actions ) - 1 ]
63. self.pyQgisApp.menuBar.insertMenu( lastAction, self.pyQgisApp.menuPlugins )

Como podemos notar, el módulo itera sobre los subdirectorios validando que correspondan a plugins e intenta cargarlos al visor devolviendo un listado de los plugins cargados y de los que no se pudieron cargar, generalmente, por problemas de dependencias.

Reporte de plugins cargados
Figura 3. Reporte de plugins cargados y problemas de dependencias.

Por último, debemos llamar el módulo plugins_manager.py desde la clase principal del visor. En el archivo VisorShapefiles.py, en la sección de importación de módulos agregamos la línea: from plugins_manager import Plugins

Al final del método __init__() agregamos: self.plugins = Plugin( self )

La sentencia anterior ejecuta el método __init__() de la clase Plugins del módulo plugins_manager.py, el cual carga los plugins al visor.

Los plugins cargados en el visor lucen así:

fTools: Análisis espacial
Figura 4. Plugin fTools de Carson Farmer para realizar análisis espacial.
Referencia lineal
Figura 5. Plugin de referencia lineal de Martin Dobias para la empresa Faunalia.
Value Tool
Figura 6. Plugin para obtener valores de las bandas para un ráster.
Table Manager
Figura 7. Plugin para administrar la tabla de atributos de una capa vectorial.
Zoom to point
Figura 8. Plugin de Gary Sherman para acercar y centrar el mapa en un punto.
PostGIS Manager
Figura 9. Plugin de Martin Dobias para administrar capas de PostGIS.

¿Cómo construir un plugin para QGIS?

Los plugins de QGIS son directorios con una estructura determinada, deben tener un archivo __init__.py que sirva para reconocer el plugin y para inicializarlo, para información detallada podemos consultar el enlace:

https://docs.qgis.org/testing/en/docs/pyqgis_developer_cookbook/plugins/index.html

Gary Sherman, el fundador de QGIS, ha realizado una utilidad web para construir un plugin básico. La dirección en internet de la herramienta es: http://pyqgis.org/builder/plugin_builder.py Se trata de un formulario en el que diligenciamos datos acerca del plugin (autor, nombre, descripción, versión, etc.) para obtener un archivo comprimido con la carpeta del plugin listo para usar.

Formulario para construir plugins
Figura 10. Formulario de la utilidad Plugin Builder.

Si copiamos la carpeta del plugin en la ubicación adecuada, podemos verlo en acción:

Mi plugin
Figura 11. Plugin generado por la herramienta web Plugin Builder.

Como vemos, se trata de una ventana con botones de aceptar y cancelar, lo cual sirve como base para comenzar nuestra personalización.

Dimitris Kavroudakis ha generado una utilidad muy similar para crear la estructura de un plugin de QGIS, se puede consultar en la página web: http://www.dimitrisk.gr/qgis/creator

Archivos para descargar

Los módulos plugins_manager.py y qgsInterface.py se encuentran disponibles para su descarga en este enlace.

Conclusiones

La arquitectura modular de QGIS permite utilizar funcionalidades realizadas en forma de plugins, en aplicaciones independientes en el escritorio.

Por medio de plugins de QGIS podemos extender considerablemente las capacidades del visor de PyQGIS, agregando funcionalidades de análisis espacial, administración de bases de datos PostgreSQL/PostGIS y consola del programa estadístico R, entre muchos otros.

Podemos realizar nuestros propios plugins basándonos en herramientas web que facilitan la creación de la estructura de los mismos.

Referencias