Correctly set transform for QgsHighlight

When we are highlighting a feature, we skip the early transformation
and instead defer to the layer's renderer to handle this for us.
By correctly setting up the render context with the correct transform
and transformed map extent, we ensure that the highlight will render
correctly even for complex map symbols (eg geometry generators)

Fixes #48439
This commit is contained in:
Nyall Dawson 2022-06-17 11:09:43 +10:00
parent e67cd8c71c
commit 88ebb3f2f5
3 changed files with 117 additions and 29 deletions

View File

@ -83,35 +83,21 @@ void QgsHighlight::init()
void QgsHighlight::updateTransformedGeometry()
{
QgsCoordinateTransform ct = mMapCanvas->mapSettings().layerTransform( mLayer );
if ( ct.isValid() )
const QgsCoordinateTransform ct = mMapCanvas->mapSettings().layerTransform( mLayer );
// we don't auto-transform if we are highlighting a feature -- the renderer will take care
// of that for us
if ( ct.isValid() && !mGeometry.isNull() )
{
// reset to original geometry and transform
if ( !mGeometry.isNull() )
mGeometry = mOriginalGeometry;
try
{
mGeometry = mOriginalGeometry;
try
{
mGeometry.transform( ct );
}
catch ( QgsCsException & )
{
QgsDebugMsg( QStringLiteral( "Could not transform highlight geometry to canvas CRS" ) );
}
mGeometry.transform( ct );
}
else if ( mFeature.hasGeometry() )
catch ( QgsCsException & )
{
mFeature.setGeometry( mOriginalGeometry );
QgsGeometry g = mFeature.geometry();
try
{
g.transform( ct );
mFeature.setGeometry( g );
}
catch ( QgsCsException & )
{
QgsDebugMsg( QStringLiteral( "Could not transform highlight geometry to canvas CRS" ) );
}
QgsDebugMsg( QStringLiteral( "Could not transform highlight geometry to canvas CRS" ) );
}
}
updateRect();
@ -329,6 +315,27 @@ void QgsHighlight::paint( QPainter *p )
return;
QgsRenderContext context = createRenderContext();
const QgsCoordinateTransform layerToCanvasTransform = mMapCanvas->mapSettings().layerTransform( mLayer );
context.setCoordinateTransform( layerToCanvasTransform );
QgsRectangle mapExtentInLayerCrs = mMapCanvas->mapSettings().visibleExtent();
if ( layerToCanvasTransform.isValid() )
{
QgsCoordinateTransform approxTransform = layerToCanvasTransform;
approxTransform.setBallparkTransformsAreAppropriate( true );
try
{
mapExtentInLayerCrs = approxTransform.transformBoundingBox( mapExtentInLayerCrs, Qgis::TransformDirection::Reverse );
}
catch ( QgsCsException & )
{
QgsDebugMsg( QStringLiteral( "Error transforming canvas extent to layer CRS" ) );
}
}
if ( !mapExtentInLayerCrs.isFinite() )
{
return;
}
context.setExtent( mapExtentInLayerCrs );
// Because lower level outlines must be covered by upper level fill color
// we render first with temporary opaque color, which is then replaced

View File

@ -17,26 +17,38 @@ import shutil
from qgis.PyQt.QtCore import (
QSize,
Qt
Qt,
QDir,
)
from qgis.PyQt.QtGui import (
QColor,
QImage,
QPainter,
QResizeEvent
QResizeEvent,
QPixmap
)
from qgis.core import (
QgsVectorLayer,
QgsProject,
QgsRectangle,
QgsRenderChecker
QgsRenderChecker,
QgsCoordinateReferenceSystem,
QgsMultiRenderChecker,
QgsGeometryGeneratorSymbolLayer,
QgsFillSymbol,
QgsSingleSymbolRenderer,
QgsSymbol
)
from qgis.gui import (
QgsHighlight,
QgsMapCanvas
)
from qgis.gui import QgsHighlight
from qgis.testing import start_app, unittest
from qgis.testing.mocked import get_iface
from utilities import unitTestDataPath
start_app()
app = start_app()
TEST_DATA_DIR = unitTestDataPath()
@ -48,8 +60,13 @@ class TestQgsHighlight(unittest.TestCase):
self.iface.mapCanvas().viewport().resize(400, 400)
# For some reason the resizeEvent is not delivered, fake it
self.iface.mapCanvas().resizeEvent(QResizeEvent(QSize(400, 400), self.iface.mapCanvas().size()))
self.report = "<h1>Python QgsMapCanvas Tests</h1>\n"
def tearDown(self):
report_file_path = "%s/qgistest.html" % QDir.tempPath()
with open(report_file_path, 'a') as report_file:
report_file.write(self.report)
QgsProject.instance().removeAllMapLayers()
def runTestForLayer(self, layer, testname):
@ -115,6 +132,70 @@ class TestQgsHighlight(unittest.TestCase):
finally:
self.iface.mapCanvas().scene().removeItem(highlight)
def test_feature_transformation(self):
poly_shp = os.path.join(TEST_DATA_DIR, 'polys.shp')
layer = QgsVectorLayer(poly_shp, 'Layer', 'ogr')
sub_symbol = QgsFillSymbol.createSimple({'color': '#8888ff', 'outline_style': 'no'})
sym = QgsFillSymbol()
buffer_layer = QgsGeometryGeneratorSymbolLayer.create(
{'geometryModifier': 'buffer($geometry, -0.4)'})
buffer_layer.setSymbolType(QgsSymbol.Fill)
buffer_layer.setSubSymbol(sub_symbol)
sym.changeSymbolLayer(0, buffer_layer)
layer.setRenderer(QgsSingleSymbolRenderer(sym))
canvas = QgsMapCanvas()
canvas.setDestinationCrs(QgsCoordinateReferenceSystem('EPSG:3857'))
canvas.setFrameStyle(0)
canvas.resize(600, 400)
self.assertEqual(canvas.width(), 600)
self.assertEqual(canvas.height(), 400)
canvas.setLayers([layer])
canvas.setExtent(QgsRectangle(-11960254, 4247568, -11072454, 4983088))
canvas.show()
# need to wait until first redraw can occur (note that we first need to wait till drawing starts!)
while not canvas.isDrawing():
app.processEvents()
canvas.waitWhileRendering()
feature = layer.getFeature(1)
self.assertTrue(feature.isValid())
highlight = QgsHighlight(canvas, feature, layer)
color = QColor(Qt.red)
highlight.setColor(color)
color.setAlpha(50)
highlight.setFillColor(color)
highlight.show()
highlight.show()
self.assertTrue(self.canvasImageCheck('highlight_transform', 'highlight_transform', canvas))
def canvasImageCheck(self, name, reference_image, canvas):
self.report += "<h2>Render {}</h2>\n".format(name)
temp_dir = QDir.tempPath() + '/'
file_name = temp_dir + 'rendered_' + name + ".png"
image = QImage(canvas.size(), QImage.Format_ARGB32)
painter = QPainter(image)
canvas.render(painter)
painter.end()
image.save(file_name)
checker = QgsMultiRenderChecker()
checker.setControlPathPrefix("highlight")
checker.setControlName("expected_" + reference_image)
checker.setRenderedImage(file_name)
checker.setColorTolerance(2)
result = checker.runTest(name, 20)
self.report += checker.report()
print((self.report))
return result
if __name__ == '__main__':
unittest.main()

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB