1

I am trying to render a number of related QgsRasterLayer objects that are related to eachother (they are slippy-type tiles). Ideally I would like one item in the legend that can have its position changed (i.e. move above or below other legend items) and add custom actions/properties dialog to, but have the actual rendering of the tiles be done by QgsRasterLayer.

My first approach was to attempt to subclass QgsPluginLayer and override the draw() method to call QgsRasterLayer.draw() for each of my loaded rasters. This does nothing, even if layers are added to the QgsMapLayerRegistry.

def draw(self, renderContext):
    for rasterlayer in self.loadedlayers.values():
       rasterlayer.draw(renderContext)

My second approach was to create a group in the legend that is somehow hidden. I can add the layer to the layer tree using

mygroup = QgsProject.instance().layerTreeRoot().addGroup("mygroup")
for rasterlayer in self.loadedlayers.values():
    mygroup.addMapLayer(rasterlayer)

but it doesn't seem possible to hide this group from the user. From Showing only some layers in QGIS legend? it doesn't seem like there is an alternative. I feel as though there is an easy solution to this that I'm not seeing.

EDIT: I tried creating a QgsMapLayerRenderer object that renders the layer to an image using a QgsMapRenderer, and while this occasionally works, it often produces output that appears to have elements of the current screen in the output image (maybe having something to do with using a QgsMapRenderer() while one is already invoked on the current thread?). In my QgsPluginLayer I have:

def createMapRenderer(self, context):
    return MultiRasterRenderer(self, context)

And my QgsMapLayerRenderer is this (includes some debugging output):

class MultiRasterRenderer(QgsMapLayerRenderer):

    def __init__(self, layer, context):
        QgsMapLayerRenderer.__init__(self, layer.id())
        self.layer = layer
        self.context = context

    def render(self):
        fout = open("/Users/dewey/Desktop/outputdump.txt", "w")
        rendererContext = self.context
        if len(self.layer.loadedlayers) > 0:
            img = QImage(QSize(self.context.painter().device().width(),
                                    self.context.painter().device().height()),
                              QImage.Format_ARGB32_Premultiplied)
            painter = QPainter()
            painter.begin(img)
            painter.setRenderHint(QPainter.Antialiasing)

            render = QgsMapRenderer()
            render.setLayerSet([layer.id() for layer in self.layer.loadedlayers.values()])
            render.setProjectionsEnabled(True)
            render.setDestinationCrs(rendererContext.coordinateTransform().destCRS())


            fout.write("Context extent: " + rendererContext.extent().toString() + "\n")


            if render.setExtent(rendererContext.extent()):
                render.setOutputSize(img.size(), img.logicalDpiX())
                render.render(painter)
                painter.end()

                img.save("/Users/dewey/Desktop/output.jpg")

                rendererContext.painter().drawImage(0, 0, img)

                fout.write("Render extent: " + render.extent().toString() + "\n")
            else:
                fout.write("render.setExtent() returned false\n")
        else:
            fout.write("No tiles to load, skipping rendering")

        fout.close()
        return True
  • Maybe a VRT virtual raster works for you. Raster layers are grouped and will be visualized and processed together. – Detlev Jan 2 '16 at 7:14
  • I considered this, but the documentation is poor and because many tiled image datasets are .png images, they are pallleted and not RGB, which complicates writing a VRT XML file. Even if this is the case, I still have to render it without it showing up in the legend, which seems impossible. – paleolimbot Jan 2 '16 at 22:07
1

After much debugging, I finally found a somewhat simple solution. When creating a new QImage() it seems, the QImage() is not blank, and thus the solution to this problem was to call QImage.fill(Qt.transparent) before rendering the map to the image. It was unnecessary to invoke a QgsMapLayerRenderer because there was no multi-threaded rendering happening...all was happening from the renderer thread. The final QgsPluginLayer code is as follows:

class QOSMTileLayer(QgsPluginLayer):

    LAYER_TYPE = "QOSM_LAYER_TYPE"

    def __init__(self, layertype, layerName):
        QgsPluginLayer.__init__(self, QOSMTileLayer.LAYER_TYPE, layerName)
        self.layertype = layertype
        self.loadedtiles = set()
        self.loadedlayers = {}
        self.setValid(True)

    def refrshlayers(self):
        ...

    def draw(self, rendererContext):
        if len(self.loadedlayers) > 0:

            render = QgsMapRenderer()
            render.setLayerSet(self.loadedlayers.values())
            render.setProjectionsEnabled(True)
            render.setDestinationCrs(rendererContext.coordinateTransform().destCRS())
            render.setExtent(rendererContext.extent())

            img = QImage(QSize(rendererContext.painter().device().width(),
                        rendererContext.painter().device().height()),
                  QImage.Format_ARGB32_Premultiplied)
            img.fill(Qt.transparent) #needed, apparently the QImage() is not empty

            painter = QPainter()
            painter.begin(img)
            painter.setRenderHint(QPainter.Antialiasing)
            render.setOutputSize(img.size(), img.logicalDpiX())
            render.render(painter)
            painter.end()

            rendererContext.painter().drawImage(0, 0, img)            
        else:
            #no tiles.
            pass

        return True

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.