Skip to content
Permalink
Browse files
[api] Implement map layer renderer for QgsGroupLayer
  • Loading branch information
nyalldawson committed Nov 23, 2021
1 parent bab7a91 commit ec5cc9396c5f3bb9aae61843737cab7e8719dab9
@@ -374,6 +374,7 @@ set(QGIS_CORE_SRCS
qgsgml.cpp
qgsgmlschema.cpp
qgsgrouplayer.cpp
qgsgrouplayerrenderer.cpp
qgshistogram.cpp
qgshstoreutils.cpp
qgshtmlutils.cpp
@@ -1010,6 +1011,7 @@ set(QGIS_CORE_HDRS
qgsgml.h
qgsgmlschema.h
qgsgrouplayer.h
qgsgrouplayerrenderer.h
qgshistogram.h
qgshstoreutils.h
qgshtmlutils.h
@@ -19,6 +19,7 @@
#include "qgsmaplayerfactory.h"
#include "qgspainting.h"
#include "qgsmaplayerlistutils.h"
#include "qgsgrouplayerrenderer.h"
#include "qgsmaplayerref.h"
#include "qgsvectorlayer.h"
#include "qgscoordinatetransform.h"
@@ -53,8 +54,7 @@ QgsGroupLayer *QgsGroupLayer::clone() const

QgsMapLayerRenderer *QgsGroupLayer::createMapRenderer( QgsRenderContext &context )
{
Q_UNUSED( context )
return nullptr;
return new QgsGroupLayerRenderer( this, context );
}

QgsRectangle QgsGroupLayer::extent() const
@@ -0,0 +1,135 @@
/***************************************************************************
qgsgrouplayerrenderer.cpp
----------------
Date : September 2021
Copyright : (C) 2021 by Nyall Dawson
Email : nyall dot dawson at gmail dot com
***************************************************************************/

/***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/

#include "qgsgrouplayerrenderer.h"
#include "qgsgrouplayer.h"
#include "qgsfeedback.h"
#include "qgspainteffect.h"
#include "qgsrendercontext.h"
#include "qgslogger.h"
#include <optional>

QgsGroupLayerRenderer::QgsGroupLayerRenderer( QgsGroupLayer *layer, QgsRenderContext &context )
: QgsMapLayerRenderer( layer->id(), &context )
, mFeedback( std::make_unique< QgsFeedback >() )
, mLayerOpacity( layer->opacity() )
{
const QList< QgsMapLayer * > layers = layer->childLayers();
const QgsCoordinateReferenceSystem destinationCrs = context.coordinateTransform().destinationCrs();
for ( QgsMapLayer *childLayer : layers )
{
// we have to temporarily set the context's crs and extent to the correct one for the child layer, BEFORE creating the
// child layer's renderer
QgsCoordinateTransform layerToDestTransform( childLayer->crs(), destinationCrs, context.transformContext() );
layerToDestTransform.setBallparkTransformsAreAppropriate( true );
context.setCoordinateTransform( layerToDestTransform );
try
{
const QgsRectangle extentInChildLayerCrs = layerToDestTransform.transformBoundingBox( context.mapExtent(), Qgis::TransformDirection::Reverse );
context.setExtent( extentInChildLayerCrs );
}
catch ( QgsCsException & )
{
QgsDebugMsg( QStringLiteral( "Error transforming extent of %1 to destination CRS" ).arg( childLayer->id() ) );
continue;
}

mChildRenderers.emplace_back( childLayer->createMapRenderer( context ) );
mRendererCompositionModes.emplace_back( childLayer->blendMode() );
mRendererOpacity.emplace_back( childLayer->type() != QgsMapLayerType::RasterLayer ? childLayer->opacity() : 1.0 );
mTransforms.emplace_back( layerToDestTransform );
}
}

QgsGroupLayerRenderer::~QgsGroupLayerRenderer() = default;

QgsFeedback *QgsGroupLayerRenderer::feedback() const
{
return mFeedback.get();
}

bool QgsGroupLayerRenderer::render()
{
QgsRenderContext &context = *renderContext();

context.painter()->save();
if ( mPaintEffect )
{
mPaintEffect->begin( context );
}

const QgsCoordinateReferenceSystem destinationCrs = context.coordinateTransform().destinationCrs();
bool canceled = false;
int i = 0;
for ( const std::unique_ptr< QgsMapLayerRenderer > &renderer : std::as_const( mChildRenderers ) )
{
if ( mFeedback->isCanceled() )
{
canceled = true;
break;
}

context.setCoordinateTransform( mTransforms[i] );

// don't need to catch exceptions here -- it would have already been caught in the QgsGroupLayerRenderer constructor!
const QgsRectangle extentInChildLayerCrs = mTransforms[i].transformBoundingBox( context.mapExtent(), Qgis::TransformDirection::Reverse );
context.setExtent( extentInChildLayerCrs );

QImage image;
QPainter *prevPainter = context.painter();
std::unique_ptr< QPainter > imagePainter;
if ( renderer->forceRasterRender() || true )
{
image = QImage( context.deviceOutputSize(), context.imageFormat() );
image.setDevicePixelRatio( static_cast<qreal>( context.devicePixelRatio() ) );
image.fill( 0 );
imagePainter = std::make_unique< QPainter >( &image );

context.setPainterFlagsUsingContext( imagePainter.get() );
context.setPainter( imagePainter.get() );
}
renderer->render();

if ( imagePainter )
{
imagePainter->end();
context.setPainter( prevPainter );
if ( context.useAdvancedEffects() )
context.painter()->setCompositionMode( mRendererCompositionModes[i] );

context.painter()->setOpacity( mRendererOpacity[i] );
context.painter()->drawImage( 0, 0, image );
context.painter()->setOpacity( 1.0 );
context.painter()->setCompositionMode( QPainter::CompositionMode_SourceOver );
}
i++;
}

if ( mPaintEffect )
{
mPaintEffect->end( context );
}

context.painter()->restore();

return !canceled;
}

bool QgsGroupLayerRenderer::forceRasterRender() const
{
return renderContext()->testFlag( Qgis::RenderContextFlag::UseAdvancedEffects ) && ( !qgsDoubleNear( mLayerOpacity, 1.0 ) );
}
@@ -0,0 +1,67 @@
/***************************************************************************
qgsgrouplayerrenderer.h
----------------
Date : September 2021
Copyright : (C) 2021 by Nyall Dawson
Email : nyall dot dawson at gmail dot com
***************************************************************************/

/***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/

#ifndef QGSGROUPLAYERRENDERER_H
#define QGSGROUPLAYERRENDERER_H

#define SIP_NO_FILE

#include "qgis_core.h"
#include "qgis_sip.h"
#include "qgsmaplayerrenderer.h"
#include <QPainter>
#include <tuple>
#include <vector>
#include <memory>

class QgsGroupLayer;
class QgsFeedback;
class QgsPaintEffect;
class QgsCoordinateTransform;

/**
* \ingroup core
* \brief Implementation of threaded rendering for group layers.
*
* \note not available in Python bindings
* \since QGIS 3.24
*/
class CORE_EXPORT QgsGroupLayerRenderer : public QgsMapLayerRenderer
{
public:

/**
* Constructor for a QgsGroupLayerRenderer, for the specified \a layer.
*/
QgsGroupLayerRenderer( QgsGroupLayer *layer, QgsRenderContext &context );
~QgsGroupLayerRenderer() override;
QgsFeedback *feedback() const override;
bool render() override;
bool forceRasterRender() const override;

private:
std::unique_ptr< QgsFeedback > mFeedback;
std::vector< std::unique_ptr< QgsMapLayerRenderer > > mChildRenderers;
std::vector< QPainter::CompositionMode > mRendererCompositionModes;
std::vector< double > mRendererOpacity;
std::vector< QgsCoordinateTransform > mTransforms;
double mLayerOpacity = 1.0;
std::unique_ptr< QgsPaintEffect > mPaintEffect;

};

#endif // QGSGROUPLAYERRENDERER_H
@@ -17,7 +17,12 @@
import gc
from qgis.PyQt.QtCore import (
QCoreApplication,
QEvent
QEvent,
QSize,
QDir
)
from qgis.PyQt.QtGui import (
QPainter
)
from tempfile import TemporaryDirectory

@@ -28,7 +33,9 @@
QgsCoordinateTransformContext,
QgsGroupLayer,
QgsGeometry,
QgsPointXY
QgsPointXY,
QgsMapSettings,
QgsMultiRenderChecker
)
from qgis.testing import start_app, unittest
from utilities import unitTestDataPath
@@ -40,6 +47,16 @@

class TestQgsGroupLayer(unittest.TestCase):

@classmethod
def setUpClass(cls):
cls.report = "<h1>Python QgsGroupLayer Tests</h1>\n"

@classmethod
def tearDownClass(cls):
report_file_path = "%s/qgistest.html" % QDir.tempPath()
with open(report_file_path, 'a') as report_file:
report_file.write(cls.report)

def test_children(self):
options = QgsGroupLayer.LayerOptions(QgsCoordinateTransformContext())
group_layer = QgsGroupLayer('test', options)
@@ -95,7 +112,7 @@ def test_extent(self):
self.assertTrue(group_layer.isValid())
layer1 = QgsVectorLayer('Point?crs=epsg:3111', 'Point', 'memory')
f = QgsFeature()
f.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(2478778,2487236)))
f.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(2478778, 2487236)))
layer1.startEditing()
layer1.addFeature(f)
layer1.commitChanges()
@@ -108,7 +125,7 @@ def test_extent(self):
self.assertEqual(extent.yMaximum(), 2487236)

layer2 = QgsVectorLayer('Point?crs=epsg:4326', 'Point', 'memory')
f.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(142.178,-35.943)))
f.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(142.178, -35.943)))
layer2.startEditing()
layer2.addFeature(f)
layer2.commitChanges()
@@ -161,6 +178,67 @@ def test_save_restore(self):
self.assertEqual(restored_group_1.childLayers(), [restored_layer1, restored_layer2, restored_layer3])
self.assertEqual(restored_group_2.childLayers(), [restored_layer3, restored_layer1])

def test_render_group_opacity(self):
"""
Test rendering layers as a group with opacity
"""
vl1 = QgsVectorLayer(TEST_DATA_DIR + '/lines.shp')
self.assertTrue(vl1.isValid())
vl2 = QgsVectorLayer(TEST_DATA_DIR + '/points.shp')
self.assertTrue(vl2.isValid())
vl3 = QgsVectorLayer(TEST_DATA_DIR + '/polys.shp')
self.assertTrue(vl3.isValid())

options = QgsGroupLayer.LayerOptions(QgsCoordinateTransformContext())
group_layer = QgsGroupLayer('group', options)
group_layer.setChildLayers([vl1, vl2, vl3])
# render group with 50% opacity
group_layer.setOpacity(0.5)

mapsettings = QgsMapSettings()
mapsettings.setOutputSize(QSize(600, 400))
mapsettings.setOutputDpi(96)
mapsettings.setDestinationCrs(group_layer.crs())
mapsettings.setExtent(group_layer.extent())
mapsettings.setLayers([group_layer])

renderchecker = QgsMultiRenderChecker()
renderchecker.setMapSettings(mapsettings)
renderchecker.setControlPathPrefix('group_layer')
renderchecker.setControlName('expected_group_opacity')
result = renderchecker.runTest('expected_group_opacity')
TestQgsGroupLayer.report += renderchecker.report()
self.assertTrue(result)

def test_render_group_blend_mode(self):
"""
Test rendering layers as a group limits child layer blend mode scope
"""
vl1 = QgsVectorLayer(TEST_DATA_DIR + '/lines.shp')
self.assertTrue(vl1.isValid())
vl2 = QgsVectorLayer(TEST_DATA_DIR + '/points.shp')
self.assertTrue(vl2.isValid())

options = QgsGroupLayer.LayerOptions(QgsCoordinateTransformContext())
group_layer = QgsGroupLayer('group', options)
group_layer.setChildLayers([vl2, vl1])
vl1.setBlendMode(QPainter.CompositionMode_DestinationIn)

mapsettings = QgsMapSettings()
mapsettings.setOutputSize(QSize(600, 400))
mapsettings.setOutputDpi(96)
mapsettings.setDestinationCrs(group_layer.crs())
mapsettings.setExtent(group_layer.extent())
mapsettings.setLayers([group_layer])

renderchecker = QgsMultiRenderChecker()
renderchecker.setMapSettings(mapsettings)
renderchecker.setControlPathPrefix('group_layer')
renderchecker.setControlName('expected_group_child_blend_mode')
result = renderchecker.runTest('expected_group_child_blend_mode')
TestQgsGroupLayer.report += renderchecker.report()
self.assertTrue(result)


if __name__ == '__main__':
unittest.main()
Binary file not shown.
Binary file not shown.

0 comments on commit ec5cc93

Please sign in to comment.