I created some PyQGIS functions to set up a print composer, modify the layout (adding maps, legend, title…) and print the result as an image file. This works fine with one or several vector layers – all of them are shown in the resulting image file.
However, when I try to create a print composer layout that includes a layer from the Openlayers plugin, I do not get the expected result – most of the time the basemap layer is missing at all and sometimes some artifacts show up.
The example code below should be instantly executable after setting the plotdir
parameter at the top to a desired output folder. It
- creates a new QGIS project
- creates two vector layers in memory, each with one point geometry somewhere
- loads the OpenStreetMap basemap from the OpenLayers plugin (by clicking through the QGIS Gui menu as I don't know how to load it otherwise)
- passes the two memory layers and the Openlayers layer to a newly created print composer, sets some layout stuff (legend, title…) and saves the result to an image file.
When I run this, the legend shows all three desired layers, but the canvas is blank (I would expect to see at least the basemap as the point geometries are rather small). Any suggestions how to properly include a basemap into a print composer with PyQGIS? I tried this on Windows 7 with QGIS 2.4 and 2.6.
The code:
# imports
from qgis.core import *
from qgis.utils import *
from PyQt4.QtCore import *
from PyQt4.QtGui import *
plotdir = r'F:\\'
###################################
# function to start a new qgis project
###################################
def startNewProject():
print('starting new project')
iface.newProject(thePromptToSaveFlag = False)
return
###################################
# function to create a memory layer and add it to the map layer registry
###################################
def createMemoryLayer(layerType, layerName, x, y):
print 'create memory layer'
myMemoryLayer = QgsVectorLayer(layerType, layerName, "memory")
QgsMapLayerRegistry.instance().addMapLayer(myMemoryLayer, True)
feat = QgsFeature()
feat.setGeometry(QgsGeometry.fromPoint(QgsPoint(x,y)))
(res, outFeats) = myMemoryLayer.dataProvider().addFeatures( [ feat ] )
return myMemoryLayer
###################################
# function to load OpenLayers plugin layers and move it to the bottom of the layer tree
###################################
def loadOpenLayersPluginMap(mapProvider, openLayersMap):
print 'loading openlayers plugin layer'
webMenu = iface.webMenu() #get object of the Web menu of QGIS
for webMenuItem in webMenu.actions(): #open the Web menu of QGIS and loop through the list of web plugins
if 'OpenLayers plugin' in webMenuItem.text(): #look for OpenLayers plugin entry in the Web menu
openLayersMenu = webMenuItem #and open it
for openLayersMenuItem in openLayersMenu.menu().actions(): #open the OpenLayers plugin menu entry and loop through the list of map providers
if mapProvider in openLayersMenuItem.text(): #if the desired mapProvider is found
mapProviderMenu = openLayersMenuItem #open its menu entry
for map in mapProviderMenu.menu().actions(): #loop through the list of maps for the opened provider
if openLayersMap in map.text(): #if the desired map entry is found
map.trigger() #click the entry to load the map as a layer
# move the layer to the bottom of the TOC by putting all other layers above it (openlayers layer cannot be moved directly)
root = QgsProject.instance().layerTreeRoot()
for child in root.children():
if isinstance(child, QgsLayerTreeLayer):
childClone = child.clone()
root.insertChildNode(0, childClone)
root.removeChildNode(child)
return
###################################
# function to zoom to the extent of a layer
###################################
def zoomToLayerExtent(layer):
print 'zooming to layer '
# set the desired layer as active and zoom to it
iface.setActiveLayer(layer)
iface.zoomToActiveLayer()
return
###################################
# function to save a map layout as image file with map composer
###################################
def saveImagesWithPrintComposer(layers, extentLayer, plottitle, plotdir):
print 'saving images with map composer'
#######
# set up layer set, extent and create a map renderer
#######
mapRenderer = QgsMapSettings() # new in QGIS 2.4 - replaces QgsMapRenderer()
layerSet = [layer.id() for layer in layers] # create a list of all layer IDs that shall be part of the composition
mapRenderer.setLayers(layerSet) # when using QgsMapRenderer, replace this with setLayerSet(layerset)
mapRectangle = extentLayer.extent() # set extent to the extent of the desired layer
mapRectangle.scale(2) # set scale as desired
mapRenderer.setExtent(mapRectangle)
mapRenderer.setOutputSize(QSize(1600,1200)) #when using QgsMapRenderer(), setOutputSize needs a second argument '[int] dpi'
#######
# create a composition and pass the renderer
#######
composition = QgsComposition(mapRenderer)
composition.setPlotStyle(QgsComposition.Print)
dpi = composition.printResolution()
dpmm = dpi / 25.4
width = int(dpmm * composition.paperWidth())
height = int(dpmm * composition.paperHeight())
#######
# add map to the composition
#######
x, y = 0, 0
w, h = composition.paperWidth(), composition.paperHeight()
composerMap = QgsComposerMap(composition, x, y, w, h)
composition.addItem(composerMap)
#######
# create title label
#######
composerLabel = QgsComposerLabel(composition)
composerLabel.setText(plottitle)
composerLabel.setFont(QFont("Arial",8))
composerLabel.adjustSizeToText()
composerLabel.setBackgroundEnabled(False)
composition.addItem(composerLabel)
composerLabel.setItemPosition(20,10)
#######
# create legend
#######
# create composer legend object
composerLegend = QgsComposerLegend(composition)
# set legend layers set (as the same as the map layerset)
composerLegend.model().setLayerSet(mapRenderer.layers()) #when using QgsMapRenderer, use layerSet() instead of layers()
# set titles
composerLegend.setTitle('')
newFont = QFont("Arial", 8)
composerLegend.setStyleFont(QgsComposerLegendStyle.Title, newFont)
composerLegend.setStyleFont(QgsComposerLegendStyle.Subgroup, newFont)
composerLegend.setStyleFont(QgsComposerLegendStyle.SymbolLabel, newFont)
# refresh legend
composerLegend.updateLegend()
# set feature count activated for all layers of the current map composition
composerLayerItem = QgsComposerLayerItem()
def activateFeatureCount(layer):
composerLayerItem.setLayerID(layer.id())
composerLayerItem.setShowFeatureCount(True)
return
[activateFeatureCount(layer) for layer in layers]
# set legend background
composerLegend.setBackgroundEnabled(False)
# set legend position
composerLegend.setItemPosition(20,20)
# add legend to composition
composition.addItem(composerLegend)
#######
# create image and initialize
#######
image = QImage(QSize(width, height), QImage.Format_ARGB32)
image.setDotsPerMeterX(dpmm * 1000)
image.setDotsPerMeterY(dpmm * 1000)
image.fill(0)
#######
# Render composition
#######
painter = QPainter(image)
sourceArea = QRectF(0, 0, composition.paperWidth(), composition.paperHeight())
targetArea = QRectF(0, 0, width, height)
composition.render(painter, targetArea, sourceArea)
painter.end()
#######
# Save composition to image file (other extensions possible)
#######
image.save(plotdir + plottitle + '.png', 'png')
return
###################################
# main function
###################################
def main():
# start a new qgis project
startNewProject()
# create memory layers
memoryLayer1 = createMemoryLayer('Linestring', 'MyFirstLayer', 2, 47) # some random coordinates
memoryLayer2 = createMemoryLayer('Linestring', 'MySecondLayer', 9, 48) # some random coordinates
# add openlayers plugin layer
loadOpenLayersPluginMap('OpenStreetMap', 'OpenStreetMap')
# zoom to first layer
zoomToLayerExtent(memoryLayer1)
# fetch all layers from the canvas
allLayers = QgsMapLayerRegistry.instance().mapLayers().values()
# save to file with print composer
saveImagesWithPrintComposer(allLayers, memoryLayer1, 'MyPlot', plotdir)
return
main()
print 'finished'
Best Answer
As directly using a Openlayers plugin layer in a programatically created print composition is not possible (see the accepted answer), this approach can at least be used to create a stationary image file (jpg/png) from a desired Openlayers map extent. Maybe this is helpful to somebody somehow...
It is based on the basemap2Image script which I modified slightly to make it callable from within a PyQGIS script running in the Python console. To use it, you need the following helper functions.
Getting the edge coordinates of a vector layer extent:
Call it by providing a QgsVectorLayer object, e.g.
Coordinates have to be passed to the script in EPSG:3857, so if your vector layer does not use that CRS by default, use the following function to transform the coordinates first:
Call it by providing your list of coordinates, the origin CRS and the desired destination CRS, e.g.:
Now create an empty file called
__init__.py
next to your script (this is necessary for importing functions from other scripts within the same folder) and another one, looking like this (this is the modified basemap2Image script):To call the main function of this script, you have to pass the following arguments:
htmlDirectory: path to the
html
directory that was part of the github package (clone the repository or download as zip, then copy the html directory to a convenient location)mapProvider: choose one from the
html
directory to which a corresponding .html file exists, e.g. 'google_streets' or 'osm'xMin: coordinates of layer extent in EPSG:3857
yMin: coordinates of layer extent in EPSG:3857
xMax: coordinates of layer extent in EPSG:3857
yMax: coordinates of layer extent in EPSG:3857
filename: path and filename of the output image file
width: width of the output image file
height: height of the output image file
Make sure to pass all arguments as strings since the script was originally developed to be used from the command line where no other types exist. An examplary call could look something like this:
Last but not least, in order to run other Python scripts from a running Python console script, you have to tell the console where to look for it. The easiest and most permanent way for me to do this was searching for the file
console.py
in my QGIS folder and add the linesys.path.append('/path/to/basemap2Image script')
under the import section (restart your QGIS to make it aware of the change).A lot of work for such a simple feature... however, when running this as described above, a window should pop up, showing you a worldmap of your desired Openlayers provider, then zooming to the given extent, save the content to the given filename and then close the window again. This image file could then be used as a basemap or whatever else in a print composition.