pythonqgis

Modify QGIS layer every/after ~5 seconds (without blocking the main thread)


I wrote a Python script for QGIS 3.36.2 (uses Python 3.12.3) that does the following:

  1. Create a layer
  2. Start an HTTP GET request to fetch new coordinates (runs asynchronously by default)
  3. Use these coordinates to draw a marker on the layer (the old marker is removed first, has to run on the main thread afaik)

Step 1 only happens once. 2. + 3. should run indefinitely but stop if there's an error or if the user stops the script. For testing I only want to run it e.g. 10 times.

What I've found/tried so far:

How do I repeat steps 2 and 3 multiple times if there's no error, e.g. 5 seconds after the last iteration finished, without blocking the GUI (especially the map viewer) with some type of sleep and preferably without using any extra libraries?

My code:

#imports here
class ArrowDrawerClass:
    layer = None
    dataprovider = None
    feature = None
    repeat = True
    url = "someURL"
    repeatCounter = 0
    myscheduler = sched.scheduler(time.time,time.sleep)
    
    def __init__(self):
        self.createNewLayer()
    
    def createNewLayer(self):
        layername = "ArrowLayer"
        self.layer =  QgsVectorLayer('Point', layername, "memory")
        self.dataprovider = self.layer.dataProvider()
        self.feature = QgsFeature()
        #Set symbol, color,... of layer here
        QgsProject.instance().addMapLayers([self.layer])

    def doRequest(self):
        request = QNetworkRequest(QUrl(self.url))
        request.setTransferTimeout(10000) #10s
        self.manager = QNetworkAccessManager()
        self.manager.finished.connect(self.handleResponse)
        self.manager.get(request)

    def handleResponse(self, reply):
        err = reply.error()

        if err == QtNetwork.QNetworkReply.NetworkError.NoError:
            bytes = reply.readAll()
            replytext = str(bytes, 'utf-8').strip()
            #extract coordinates here ...
            self.drawArrow(x,y)
        else:
            self.displayError(str(err),reply.errorString())

    def drawArrow(self,x,y):
        self.layer.dataProvider().truncate() #removes old marker
        point1 = QgsPointXY(x,y)
        self.feature.setGeometry(QgsGeometry.fromPointXY(point1))
        self.dataprovider.addFeatures([self.feature])
        self.layer.updateExtents()
        self.layer.triggerRepaint()
        self.repeatCounter += 1
        self.repeatEverything()

    def displayError(self,code,msg):
        self.repeat = False
        #show error dialog here

    def start(self):
        self.myscheduler.enter(0,0,self.doRequest)
        self.myscheduler.run()

    def repeatEverything(self):
        print("counter:",self.repeatCounter)
        if self.repeat and self.repeatCounter<10:
            print("repeat")
            self.myscheduler.enter(5,0,self.test) #TODO: Call "self.doRequest()" instead
            self.myscheduler.run()
        else:
            print("don't repeat!")

    def test(self):
        print("test!")

adc = ArrowDrawerClass()
adc.start()

Solution

  • I managed to accomplish this with a "single shot" (only triggers once) QTimer:

    from PyQt5.QtCore import QTimer
    #Other imports here
    
    class ArrowDrawerClass:
        #Declare variables here
    
        def __init__(self):
            self.timer = QTimer()
            self.timer.setSingleShot(True)
            self.timer.timeout.connect(self.doRequest)
            self.createNewLayer()
        
        #def createNewLayer(self): #No changes
        #def doRequest(self): #No changes
        #def handleResponse(self, reply): #No changes
        
        def drawArrow():
            #draw arrow here
            self.repeatCounter += 1
            self.repeatEverything()
        
        def displayError(self,code,msg):
            self.stopTimer()
            self.repeat = False
            #show error dialog here
        
        def repeatEverything(self):
            print("counter:",self.repeatCounter)
            #print("Main Thread:",(isinstance(threading.current_thread(), threading._MainThread)))
            if self.repeat and self.repeatCounter<10:
                self.startTimer()
            else:
                self.stopTimer()
        
        def startTimer(self):
            if not self.timer.isActive():
                self.timer.start(5000) #5s
        
        def stopTimer(self):
            if self.timer.isActive():
                self.timer.stop()
    
    adc = ArrowDrawerClass()
    adc.doRequest() #Call the function directly, so there's no 5s delay at the beginning
    

    This doesn't block the UI or freeze QGIS (apart from a mini-freeze caused by truncate() but that's a different problem).

    According to the docs, QTimer uses the event loop and the second print in repeatEverything always output True in my tests, so there shouldn't be a need to worry about updating the UI.