GEOG 489
Advanced Python Programming for GIS

4.10.2.5 The Bus Tracker Widget for visualizing the analysis process

PrintPrint

As we explained before, we also wanted to set up a QT widget that shows the status of each bus tracker while the data is being processed and we implemented the class BusTrackerWidget in bus_tracker_widget.py derived from QWidget for this purpose. In lines 30 to 44 of main.py we are creating the main window for this program and in line 41 we add an instance of BusTrackerWidget to the QScrollArea in the center of that window:

class MainWidget(QWidget): 
     """main window for this application containing a button to start the analysis and a scroll area for the BusTrackerWidget"""      
     def __init__(self, analyzer): 
         super(MainWidget,self).__init__() 
         self.resize(300,500) 
         grid = QGridLayout(self) 
         self.button = QPushButton("Run") 
         grid.addWidget(self.button,0,0) 
         self.busTrackerWidget = BusTrackerWidget(analyzer) # place BusTrackerWidget for our BusTrackAnalyzer in scroll area 
         scroll = QScrollArea() 
         scroll.setWidgetResizable(True) 
         scroll.setWidget(self.busTrackerWidget) 
         grid.addWidget(scroll, 1, 0) 

         self.button.clicked.connect(self.run) # when button is clicked call run() function 

We are embedding the widget in a QScrollArea to make sure that vertical and horizontal scrollbars will automatically appear when the content becomes too large to be displayed. For this to work, we only have to set the minimum width and height properties of the BusTrackerWidget object accordingly. Please note that in line 53 of main.py we are calling the method updateContent() of BusTrackerWidget so that the widget and its content will be repainted whenever another analysis step has been performed.

Looking at the definition of class BusTrackerWidget in bus_tracker_widget.py, the important things happen in its paintEvent(…) method in lines 24 to 85. Remember that this is the method that will be called whenever QT creates a paint event for this widget, so not only when we force this by calling the repaint() method but also when the parent widget has been resized, for example.

def paintEvent(self, event): 
         """draws content with bus status information""" 
         # set minimum dimensions basd on number of buses 
         self.setMinimumHeight(len(self.analyzer.allBusTrackers) * 23 + 50) 
         self.setMinimumWidth(425) 
         
         # create QPainter and start drawing 
         qp = QtGui.QPainter() 
         qp.begin(self) 

         normalFont = qp.font() 
         boldFont = qp.font() 
         boldFont.setBold(True) 

         # draw time of last processed Timepoint at the top 
         qp.setPen(Qt.darkGreen) 
         qp.setFont(boldFont) 
         if self.analyzer.lastProcessedTimepoint: 
             qp.drawText(5,10,"Time: {0}".format(self.analyzer.lastProcessedTimepoint.time)) 
         qp.setPen(Qt.black) 

         …

The first thing that happens in this code is that we set the minimum height property of our widget based on the number of BusTracker objects that we need to provide status information for (line 27). For the minimum width, we can instead use a fixed value that is large enough to display the rows. Next, we create the QPainter object needed for drawing (line 31 and 32) and draw the time of the last Timepoint processed by the analyzer to the top of the window (line 42) unless its value is still None.

	# loop through all BusTrackers in the BusTrackAnalyzer 
        for index, tracker in enumerate(self.analyzer.allBusTrackers): 
          ...  

In the main for-loop starting in line 46, we go through the list of BusTracker objects in the associated BusTrackAnalyzer object and produce one row for each of them. All drawing operations in the loop body compute the y coordinate as the product of the index of the current BusTracker in the list available in variable index and the constant 23 for the height of a single row (e.g., 20 +23 * index in line 49 for drawing the bus status icon).

             # draw icon reflecting bus status 
    	     qp.drawPixmap(5,20 + 23 * index,self._busIcons[tracker.status]) 

             # draw speed circles 
             color = Qt.transparent 
             if tracker.speedEstimate: 
                 if tracker.speedEstimate < 15:  
                     color = Qt.red 
                 elif tracker.speedEstimate < 25: 
                     color = QtGui.QColor(244, 176, 66) 
                 elif tracker.speedEstimate < 40: 
                     color = Qt.yellow 
                 else: 
                     color = Qt.green 
             qp.setBrush(color) 
             qp.drawEllipse(80, 23 + 23 * index, 7, 7) 
             …

The bus status icon is drawn in line 49 using the drawPixmap(…) method with the QPixmap icon from the _busIcons dictionary for the given status of the bus tracker. Next, the small colored circle for the speed has to be drawn which happens in line 53. The color is determined by the if-elif construct in lines 53 to 61.

             # draw bus id and line text 
             qp.setFont(boldFont) 
             qp.drawText(100, 32 + 23 * index, tracker.bus.busId + "  [line "+tracker.bus.line+"]") 

             # draw status and speed text 
             qp.setFont(normalFont) 
             statusText = "currently " + tracker.status 
             if tracker.status == BusTracker.STATUS_INDEPOT: 
                 statusText += " " + tracker.depot.name 
             elif tracker.status == BusTracker.STATUS_DRIVING: 
                 if not tracker.speedEstimate: 
                     speedText = "???" 
                 else: 
                     speedText  = "{0:.2f} mph".format(tracker.speedEstimate) 
                 statusText += " with " + speedText 

             qp.drawText(200, 32 + 23 * index, statusText )

Next the bus ID and bus line number are drawn (line 66 and 67). This is followed by the code for creating the status information that contains some case distinctions to construct the string that should be displayed in line 81 in variable statusText based on the current status of the bus.

Now run the program, make the main window as large as possible and observe how the status information in the bus tracker widget is constantly updated like in the brief video below. Keep in mind that you can change the speed this all runs in by increasing the value of variable delay defined in line 21 of main.py. Refresher for running python scripts from the OSGeo4W Shell, you can use python-qgis-ltr, so e.g.

python-qgis-ltr main.py

The video has been recorded with a value of 5. [NOTE:  This video (:58) does NOT contain sound.]

After developing the Bus Track Analyzer code as a standalone QGIS based application, we will now turn to the (optional) topic of creating QGIS plugins and how our analyzer code can be turned into a plugin that, as an extension, displays the bus trajectories and detected event live on the QGIS map canvas while the anaylsis is performed.