GEOG 489
Advanced Python Programming for GIS

4.12.6 Create and Integrate New Class QGISEventAndTrackLayerCreator

PrintPrint

In addition to showing the current bus tracker states in the dock widget, we want our plugin to add two new layers to the currently open QGIS project that show the progress of the analysis and, once the analysis is finished, contain its results. The two layers will correspond to the two output files we produced in the original project in Section 4.10:

  • a polyline layer showing routes taken by the different bus vehicles with the bus IDs as attribute,
  • a point layer showing the detected events with the type of the event and a short description as attributes.

Since we don’t want to just produce these layers at the end of the analysis but want these to be there from the start of the analysis and continuously update whenever a new bus GPS observation is processed or an event is detected, we are going to write some code that reacts to the observationProcessed and eventDetected signals emitted by our class BusTrackAnalyzer (see the part of Section 4.12.4 where we added these signals). We will define a new class for all this that will be called QGISEventAndTrackLayerCreator and it will be defined in a new file qgis_event_and_track_layer_creator.py. The class definition consists of the constructor and two methods called addObservationToTrack(…) and addEvent(…) that will be connected to the corresponding signals of the analyzer.

Let’s start with the beginning of the class definition and the constructor. All following code needs to be placed in file qgis_event_and_track_layer_creator.py that you need to create in the plugin folder.

import qgis 

class QGISEventAndTrackLayerCreator(): 

     def __init__(self): 
         self._features = {}    # dictionary mapping bus id string to polyline feature in bus track layer  
         self._pointLists = {}  # dictionary mapping bus id string to list of QgsPointXY objects for creating the poylines from 

         # get project currently open in QGIS 
         currentProject = qgis.core.QgsProject.instance() 

         # create track layer and symbology, then add to current project   
         self.trackLayer = qgis.core.QgsVectorLayer('LineString?crs=EPSG:4326&field=BUS_ID:integer', 'Bus tracks' , 'memory') 
         self.trackProv = self.trackLayer.dataProvider() 

         lineMeta = qgis.core.QgsApplication.symbolLayerRegistry().symbolLayerMetadata("SimpleLine") 
         lineLayer = lineMeta.createSymbolLayer({'color': '0,0,0'}) 
         markerMeta = qgis.core.QgsApplication.symbolLayerRegistry().symbolLayerMetadata("MarkerLine") 

         markerLayer = markerMeta.createSymbolLayer({'width': '0.26', 'color': '0,0,0', 'placement': 'lastvertex'}) 
         symbol = qgis.core.QgsSymbol.defaultSymbol(self.trackLayer.geometryType()) 
         symbol.deleteSymbolLayer(0) 
         symbol.appendSymbolLayer(lineLayer) 
         symbol.appendSymbolLayer(markerLayer) 

         trackRenderer = qgis.core.QgsSingleSymbolRenderer(symbol) 
         self.trackLayer.setRenderer(trackRenderer) 

         currentProject.addMapLayer(self.trackLayer) 

         # create event layer and symbology, then add to current project 
         self.eventLayer = qgis.core.QgsVectorLayer('Point?crs=EPSG:4326&field=TYPE:string(50)&field=INFO:string(255)', 'Bus events' , 'memory') 
         self.eventProv = self.eventLayer.dataProvider() 

         colors = { "BusEncounterEvent": 'yellow', "BusStoppedEvent": 'orange', "EnteringDepotEvent": 'blue', "LeavingDepotEvent": 'green' } 

         categories = [] 
         for ev in colors: 
             categories.append( qgis.core.QgsRendererCategory( ev, qgis.core.QgsMarkerSymbol.createSimple({'name': 'square', 'size': '3.0', 'color': colors[ev]}), ev )) 

         eventRenderer = qgis.core.QgsCategorizedSymbolRenderer("TYPE", categories) 
         self.eventLayer.setRenderer(eventRenderer) 

         currentProject.addMapLayer(self.eventLayer) 

To be able to build polylines for the bus tracks and update these whenever a new observation has been processed by the analyzer, we need to maintain dictionaries with the QGIS features and point lists for each bus vehicle track. These are created at the beginning of the constructor code in lines 6 and 7. In addition, the constructor accesses the currently open QGIS project (line 10) and adds the two new layers called “Bus tracks” and “Bus events” to it (lines 29 and 44). The rest of the code is mainly for setting the symbology of these two layers: For the track layer, we use black lines with a red circle marker at the end to indicate the current location of the vehicle (lines 16 to 27) as shown in the image below. For the events, we use square markers in different colors based on the TYPE of the event (lines 35 to 42).

 Screenshot of what the text content above describes. Tracks marked with bus events      
Figure 4.47 QGIS symbology used for the bus track and event layers

Now we are going to add the definition of the method addObservationToTrack(…) that will be connected to the observationProcessed signal emitted when the analyzer object has completed the execution of nextStep().

	def addObservationToTrack(self, observation): 
         """add new vertex to a bus polyline based on the given Observation object""" 
         busId = observation.busTracker.bus.busId; 

         # create new point for this observation 
         p =  qgis.core.QgsPointXY(observation.timepoint.lon, observation.timepoint.lat) 

         # add point to point list and (re)create polyline geometry 
         if busId in self._features:      # we already have a point list and polyline feature for this bus 
             feat = self._features[busId] 
             points = self._pointLists[busId] 
             points.append(p) 

             # recreate polyline geometry and replace in layer 
             polyline = qgis.core.QgsGeometry.fromPolylineXY(points) 
             self.trackProv.changeGeometryValues({feat.id(): polyline}) 

         else:                            # new bus id we haven't seen before 
             # create new  polyline and feature     
             polyline = qgis.core.QgsGeometry.fromPolylineXY([p]) 
             feat = qgis.core.QgsFeature() 
             feat.setGeometry(polyline) 
             feat.setAttributes([int(busId)]) 
             _, f = self.trackProv.addFeatures([feat]) 
             # store point list and polyline feature in respective dictionaries 
             self._features[busId] = f[0] 
             self._pointLists[busId] = [p] 

         # force redraw of layer 
         self.trackLayer.triggerRepaint() 
         qgis.utils.iface.mapCanvas().refresh()

The Observation object given to this method as a parameter provides us with access to all the relevant information we need to update the polyline feature for the bus this observation is about. First, we extract the ID of the bus (line 3) and create a new QgsPointXY object from the Timepoint stored in the Observation object (line 6). If we already have a polyline feature for this vehicle, we get the corresponding feature and point lists from the features and pointLists dictionaries, add the new point to the point list and create a new polyline geometry from it, and finally change the geometry of that feature in the bus track layer to this new geometry (lines 10 to 16). If instead this is the first observation of this vehicle, we create a point list for it to be stored in the pointList dictionary as well as a new polyline geometry with just that single point, and we then set up a new QgsFeature object for this polyline that is added to the bus track layer and also the features dictionary (lines 19 to 27). At the very end of the method, we make sure that the layer is repainted in the QGIS map canvas.

Now we add the code for the addEvent(…) method completing the definition of our class QGISEventAndTrackLayerCreator:

		def addEvent(self, busEvent): 
         	"""add new event point feature to event layer based on the given BusEvent object""" 
            # create point feature with information from busEvent 
            p =  qgis.core.QgsPointXY(busEvent.timepoint.lon, busEvent.timepoint.lat) 
            feat = qgis.core.QgsFeature() 
            feat.setGeometry(qgis.core.QgsGeometry.fromPointXY(p)) 
            feat.setAttributes([type(busEvent).__name__, busEvent.description()]) 

            # add feature to event layer and force redraw 
            self.eventProv.addFeatures([feat]) 

            self.eventLayer.triggerRepaint() 
            qgis.utils.iface.mapCanvas().refresh()

This method is much simpler because we don’t have to modify existing features in the layer but rather always add one new point feature to the event layer. All information required for this is taken from the bus event object given as a parameter: The coordinates for the next point feature are taken from the Timepoint stored in the event object (line 4), for the TYPE field of the event we take the type of the event object (line 7), and for the INFO field we take the string returned by calling the event object’s description(…) method (also line 7).

To incorporate this new class into our current plugin, we need to make a few more modifications to the class BusTrackAnalyzerForQGISDockWidget in file bus_track_analyzer_for_qgis_dock_widget.py. Here are the instructions for this:

  1. Remove the # at the beginning of the line “from .qgis_event_and_track_layer_creator import …” to uncomment this line and import our new class.
  2. Now we add a new auxiliary method to the class definition that has the purpose of creating an object of our new class BusTrackAnalyzerForQGISDockWidget and connecting its methods to the corresponding signals of the analyzer object. Add the following definition as the first method after the “closeEvent” method, so directly after the comment “# own methods added to the template file”.
    	def createLayerCreator(self): 
             """creates a new QGISEventAndTrackLayerCreator for showing events and tracks on main QGIS window and connects it to analyzer""" 
             self.layerCreator = QGISEventAndTrackLayerCreator() 
             self.analyzer.observationProcessed.connect(self.layerCreator.addObservationToTrack) 
             self.analyzer.eventDetected.connect(self.layerCreator.addEvent) 
    

    We already set up an instance variable layerCreator in the constructor code and we are using it here for storing the newly created layer creator object. Then we connect the signals to the two methods of the layer creator object.

  3. Now we just need to uncomment two lines to make sure that the method createLayerCreator() is called at the right places, namely after we read in data from the input files and when the analyzer is reset. In both cases, we want to create a new layer creator object that will set up two new layers in the current QGIS project. The lines that you need to uncomment for this by removing the leading # are:
    • the line “# self.createLayerCreator()” in method readData()
    • the line “# self.createLayerCreator()” in method reset()

    You should make sure that the indentation is correct after removing the hashmarks.

That’s it, we are done with the code for our plugin!