5

Yet another PyQgis question... sorry, but I'm an absolute beginner with this. I have a script running nicely when I execute it through the QGIS console (either step by step or using "Run Selected" in the editor). However, when I just click "Run" in the editor, I get different results. What I do is:

  1. Load one raster layer
  2. Select feature in existing vector layer and zoom to it
  3. Apply Local Histogram Stretch
  4. Create map and export it

With the step by step solution, I assume the script is "waiting" for each action to finish, which I can see as the map content is constantly changing. This is not the case when I run the script from the editor. Why? Is there some workaround?

time.sleep() did not do the trick, and I can't get my head around QTimer Class

I'm using QGIS 2.14. This is my full code (the main part happens near the end, when I trigger the stretch):

import os, time
from PyQt4.QtCore import *
from PyQt4.QtXml import *
from PyQt4.QtGui import *
from qgis import *

image_path = PATH_TO_IMAGE
export_path = EXPORT_PATH
template = PATH_TO_TEMPLATE


def exportMap(tempFile, canvas, bbox, outName):
    template_file = file(tempFile)
    template_content = template_file.read()
    template_file.close()
    document = QDomDocument()
    document.setContent(template_content)
    composer = QgsComposition(canvas.mapSettings())
    composer.loadFromTemplate(document)
    map_item = composer.getComposerItemById('main_map')
    map_item.setMapCanvas(canvas)
    bbox.scale(1.2)
    map_item.zoomToExtent(bbox)
    canvas.refresh()
    map_item.updateCachedImage()
    legend_item = composer.getComposerItemById('legend')
    legend_item.updateLegend()
    composer.refreshItems()
    composer.update()
    composer.exportAsPDF(outName)

# get map canvas
canvas = iface.mapCanvas()
li = iface.legendInterface()

# list all layers
layers = li.layers()
# get specific layer by displayed name
lakes = [l for l in layers if l.name() == 'lakes'][0]
iface.legendInterface().setLayerVisible(lakes, True)
# load raster
image = image_path
raster = QgsRasterLayer(image, QFileInfo(image).baseName())
iface.addRasterLayer(image)
canvas.refresh()
# make sure vector layer is on top
root = QgsProject.instance().layerTreeRoot()
for ch in root.children():
    if ch.layerName() == lakes.name():
        clone = ch.clone()
        root.insertChildNode(0, clone)
        root.removeChildNode(ch)

for feat in lakes.getFeatures():
    outname = abbrev[feat.attribute('name')] + '_' + QFileInfo(image).baseName() + '.pdf'
    outfile = os.path.join(export_path, outname)
    # select single feature based on attribute "name"
    expr = QgsExpression(""" \"name\" = '{0}' """.format(feat.attribute('name')))
    selection = [l.id() for l in lakes.getFeatures(QgsFeatureRequest(expr))]
    lakes.setSelectedFeatures(selection)
    # zoom to selected feature
    canvas.zoomToSelected(lakes)
    # bounding box of selected feature
    bbox = lakes.boundingBoxOfSelected()
    # deselect
    lakes.setSelectedFeatures([])
    # apply local stretch to active raster layer
    iface.setActiveLayer(raster)
    iface.mainWindow().findChild(QAction, 'mActionLocalCumulativeCutStretch').trigger()
    exportMap(template, canvas, bbox, outfile)

# remove raster
for ch in root.children():
    if ch.layerName() == raster.name():
        root.removeChildNode(ch)

With the help of @LaughU and this post, I finally got it to work. My workaround now looks like this:

  • Load all layers I want to use
  • print a useless line!, which is very important because I'm loading a raster layer that needs time to render
  • use a more or less useless function that does nothing, but is connected to the mapCanvas().mapCanvasRefreshed signal
    • call my export-function (which shall take some arguments I need to set) afterwards

So my code now looks like this:

canvas = iface.mapCanvas()
li = iface.legendInterface()
# list all layers
layers = li.layers()
myLayer = [l for l in layers if l.name() == 'myName'][0]
# dummy function
def dummyAction():
    pass
# load raster
image = 'PATH_TO_RASTER'
raster = QgsRasterLayer(image, QFileInfo(image).baseName())
iface.addRasterLayer(image)
### LOAD ALL ADDITIONAL LAYERS, THEN: ###
print 'This is a useless line, but forces the map renderer to wait!'
# apply local stretch to active raster layer
iface.setActiveLayer(raster)
iface.mainWindow().findChild(QAction, 'mActionLocalCumulativeCutStretch').trigger()
### SET UP COMPOSER, THEN: ###
canvas.refresh()
canvas.mapCanvasRefreshed.connect(dummyAction)
### DO THE ACTUAL EXPORT ###

Why is this such a pain?

s6hebern
  • 1,236
  • 10
  • 19
  • Where is the complete PyQGIS code for your question? – xunilk Nov 23 '17 at 14:37
  • @xunilk - added it – s6hebern Nov 23 '17 at 15:12
  • Did you use time.sleep() directly before the line exportMap(template, canvas, bbox, outfile)? And if so, did you give it a decent time to wait (e.g. to wait 10 seconds: time.sleep(10))? – Joseph Nov 24 '17 at 10:12
  • Yes, I did. When using the GUI, it usually takes 1-2 seconds, and I used 10 for testing. Same result, so I searched around and found QTimer Class, but I don't know how to use it properly. – s6hebern Nov 24 '17 at 11:53
  • There is a signal which is fired once the mapcomposer is rendert. If you use this to connect to you export function you will get a good result – LaughU Nov 24 '17 at 22:51
  • This may help: https://gis.stackexchange.com/questions/189735/how-to-iterate-over-layers-and-export-them-as-png-images-with-pyqgis-in-a-standa/189825#189825 (uses QTimer) – XIY Nov 25 '17 at 09:08
  • @LaughU - Do you know what signal this is and how I can grab it? – s6hebern Nov 27 '17 at 07:33
  • @XIY - Thanks, I found this one, too, and tried to do it like this using QTimer.singleShot(1000, exportMap(template, canvas, bbox, outfile)), but it did not work and I got the error message TypeError: arguments did not match any overloaded call: QTimer.singleShot(int, QObject, SLOT()): argument 2 has unexpected type 'NoneType' QTimer.singleShot(int, callable): argument 2 has unexpected type 'NoneType' – s6hebern Nov 27 '17 at 07:39
  • @s6hebern see my answer for an explanation. p.s. please dont tell me you studied in a town in southwestern germany starting with T... – LaughU Nov 27 '17 at 09:24
  • @s6hebern QTimer.singleShot() expects a callable function as the second argument that it will later call itself. e.g. if exportMap() took no arguments, QTimer.singleShot(1000, exportMap) would be correct syntax - note that you aren't calling the function, just supplying it to singleShot(). You can't supply a function and it's arguments in this way, so you'd need to use partial. e.g. from functools import partial and then QTimer.singleShot(1000, partial(exportMap, template, canvas, bbox, outfile)). partial() returns a callable function that singleShot() can use later. – XIY Nov 27 '17 at 09:42
  • Having explained that, @LaughU's solution looks much nicer if it works for you! – XIY Nov 27 '17 at 09:44

1 Answers1

2

I am using QGIS 2.18.13 and I encountered the same problem. The solution for me was to connect my map creation function when the map was finished refreshing.

My condensed code looks this:

def load_layer(self, temp_layer):
    # loads my layers which I want to display
    QgsMapLayerRegistry.instance().addMapLayer(temp_layer) # for demo
    self.iface.mapCanvas().refreshAllLayers()
    self.iface.mapCanvas().mapCanvasRefreshed.connect(self.mapcreater)

def mapcreater(self, templet):
    self.iface.mapCanvas().mapCanvasRefreshed.disconnect(self.mapcreater) # importen else you could end up in a loop
    template_file = file(templet, 'rt')
    template_content = template_file.read()
    template_file.close()
    prepared_name = "my_name.pdf" 
    maprender = self.iface.mapCanvas().mapRenderer()
    composition = QgsComposition(maprender)
    docu = QDomDocument()
    docu.setContent(template_content)
    composition.loadFromTemplate(docu)
    composition.setUseAdvancedEffects(False)
    map_report = composition.getComposerMapById(0)
    map_report.zoomToExtent(maprender.extent())

    self.iface.mapCanvas().refreshAllLayers()
    composition.refreshItems()
    composition.exportAsPDF(os.path.join("C:\my_folder\", prepared_name)) 

The worflow would be:

  • Disable all layers you dont want to show
  • Load all layers you want to show
  • Refresh the layers with iface.mapCanvas().refreshAllLayers()
  • Connect the mapCanvasRefreshed signal to you map creation function
  • Inside your map creation function disconnect the signal again
  • Create the map and maybe do some clean up of layers and visablity
LaughU
  • 4,176
  • 5
  • 21
  • 41