Skip to content
Permalink
Browse files

Fix rendering of Vector Field marker symbol layer when map is rotated

Fixes #40916

(cherry picked from commit 157bdca)
  • Loading branch information
nyalldawson committed Feb 19, 2021
1 parent ea7e329 commit fdaf54c9ce8f81d39bd0db32e6ea4c5fbbd13486
@@ -136,56 +136,72 @@ void QgsVectorFieldSymbolLayer::renderPoint( QPointF point, QgsSymbolRenderConte

const QgsRenderContext &ctx = context.renderContext();

const QgsFeature *f = context.feature();
if ( !f )
if ( !context.feature() )
{
//preview
QPolygonF line;
line << QPointF( 0, 50 );
line << QPointF( 100, 50 );
mLineSymbol->renderPolyline( line, nullptr, context.renderContext() );
return;
}

const QgsFeature f( *context.feature() );

double xComponent = 0;
double yComponent = 0;

double xVal = 0;
if ( f && mXIndex != -1 )
if ( mXIndex != -1 )
{
xVal = f->attribute( mXIndex ).toDouble();
xVal = f.attribute( mXIndex ).toDouble();
}
double yVal = 0;
if ( f && mYIndex != -1 )
if ( mYIndex != -1 )
{
yVal = f->attribute( mYIndex ).toDouble();
yVal = f.attribute( mYIndex ).toDouble();
}

const QgsMapToPixel &m2p = ctx.mapToPixel();
const double mapRotation = m2p.mapRotation();

QPolygonF line;
line << point;

QPointF destPoint;
switch ( mVectorFieldType )
{
case Cartesian:
xComponent = ctx.convertToPainterUnits( xVal, mDistanceUnit, mDistanceMapUnitScale );
yComponent = ctx.convertToPainterUnits( yVal, mDistanceUnit, mDistanceMapUnitScale );
{
destPoint = QPointF( point.x() + mScale * ctx.convertToPainterUnits( xVal, mDistanceUnit, mDistanceMapUnitScale ),
point.y() - mScale * ctx.convertToPainterUnits( yVal, mDistanceUnit, mDistanceMapUnitScale ) );
break;
}

case Polar:
{
convertPolarToCartesian( xVal, yVal, xComponent, yComponent );
xComponent = ctx.convertToPainterUnits( xComponent, mDistanceUnit, mDistanceMapUnitScale );
yComponent = ctx.convertToPainterUnits( yComponent, mDistanceUnit, mDistanceMapUnitScale );
destPoint = QPointF( point.x() + mScale * ctx.convertToPainterUnits( xComponent, mDistanceUnit, mDistanceMapUnitScale ),
point.y() - mScale * ctx.convertToPainterUnits( yComponent, mDistanceUnit, mDistanceMapUnitScale ) );
break;
}

case Height:
xComponent = 0;
yComponent = ctx.convertToPainterUnits( yVal, mDistanceUnit, mDistanceMapUnitScale );
break;
default:
{
destPoint = QPointF( point.x(), point.y() - ( mScale * ctx.convertToPainterUnits( yVal, mDistanceUnit, mDistanceMapUnitScale ) ) );
break;
}
}

xComponent *= mScale;
yComponent *= mScale;
if ( !qgsDoubleNear( mapRotation, 0.0 ) && mVectorFieldType != Height )
{
const double radians = mapRotation * M_PI / 180.0;
destPoint = QPointF( cos( radians ) * ( destPoint.x() - point.x() ) - sin( radians ) * ( destPoint.y() - point.y() ) + point.x(),
sin( radians ) * ( destPoint.x() - point.x() ) + cos( radians ) * ( destPoint.y() - point.y() ) + point.y() );
}

QPolygonF line;
line << point;
line << QPointF( point.x() + xComponent, point.y() - yComponent );
mLineSymbol->renderPolyline( line, f, context.renderContext() );
line << destPoint;
mLineSymbol->renderPolyline( line, &f, context.renderContext() );
}

void QgsVectorFieldSymbolLayer::startRender( QgsSymbolRenderContext &context )
@@ -289,6 +289,7 @@ ADD_PYTHON_TEST(PyQgsTreeWidgetItem test_qgstreewidgetitem.py)
ADD_PYTHON_TEST(PyQgsUnitTypes test_qgsunittypes.py)
ADD_PYTHON_TEST(PyQgsValidityChecks test_qgsvaliditychecks.py)
ADD_PYTHON_TEST(PyQgsValidityResultsWidget test_qgsvalidityresultswidget.py)
ADD_PYTHON_TEST(PyQgsVectorFieldMarkerSymbolLayer test_qgsvectorfieldmarkersymbollayer.py)
ADD_PYTHON_TEST(PyQgsVectorFileWriter test_qgsvectorfilewriter.py)
ADD_PYTHON_TEST(PyQgsVectorFileWriterTask test_qgsvectorfilewritertask.py)
ADD_PYTHON_TEST(PyQgsVectorLayer test_qgsvectorlayer.py)
@@ -0,0 +1,265 @@
# -*- coding: utf-8 -*-

"""
***************************************************************************
test_qgsvectorfieldmarkersymbollayer.py
---------------------
Date : January 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. *
* *
***************************************************************************
"""

__author__ = 'Nyall Dawson'
__date__ = 'November 2021'
__copyright__ = '(C) 2021, Nyall Dawson'

import qgis # NOQA
from qgis.PyQt.QtCore import QDir, QVariant
from qgis.PyQt.QtGui import QImage, QColor, QPainter
from qgis.PyQt.QtXml import QDomDocument
from qgis.core import (QgsGeometry,
QgsFields,
QgsField,
QgsRenderContext,
QgsFeature,
QgsMapSettings,
QgsRenderChecker,
QgsReadWriteContext,
QgsSymbolLayerUtils,
QgsSimpleMarkerSymbolLayer,
QgsLineSymbolLayer,
QgsLineSymbol,
QgsMarkerSymbol,
QgsVectorFieldSymbolLayer
)
from qgis.testing import unittest, start_app

from utilities import unitTestDataPath

start_app()
TEST_DATA_DIR = unitTestDataPath()


class TestQgsVectorFieldMarkerSymbolLayer(unittest.TestCase):

def setUp(self):
self.report = "<h1>Python QgsVectorFieldMarkerSymbolLayer 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)

def testRender(self):
# test rendering
s = QgsMarkerSymbol()
s.deleteSymbolLayer(0)

field_marker = QgsVectorFieldSymbolLayer()
field_marker.setXAttribute('x')
field_marker.setYAttribute('y')
field_marker.setScale(4)

field_marker.setSubSymbol(QgsLineSymbol.createSimple({'color': '#ff0000', 'width': '2'}))

s.appendSymbolLayer(field_marker.clone())

g = QgsGeometry.fromWkt('Point(5 4)')
fields = QgsFields()
fields.append(QgsField('x', QVariant.Double))
fields.append(QgsField('y', QVariant.Double))
f = QgsFeature(fields)
f.setAttributes([2, 3])
f.setGeometry(g)

rendered_image = self.renderFeature(s, f)
assert self.imageCheck('vectorfield', 'vectorfield', rendered_image)

def testMapRotation(self):
# test rendering with map rotation
s = QgsMarkerSymbol()
s.deleteSymbolLayer(0)

field_marker = QgsVectorFieldSymbolLayer()
field_marker.setXAttribute('x')
field_marker.setYAttribute('y')
field_marker.setScale(4)

field_marker.setSubSymbol(QgsLineSymbol.createSimple({'color': '#ff0000', 'width': '2'}))

s.appendSymbolLayer(field_marker.clone())

g = QgsGeometry.fromWkt('Point(5 4)')
fields = QgsFields()
fields.append(QgsField('x', QVariant.Double))
fields.append(QgsField('y', QVariant.Double))
f = QgsFeature(fields)
f.setAttributes([2, 3])
f.setGeometry(g)

rendered_image = self.renderFeature(s, f, map_rotation=45)
assert self.imageCheck('rotated_map', 'rotated_map', rendered_image)

def testHeight(self):
# test rendering
s = QgsMarkerSymbol()
s.deleteSymbolLayer(0)

field_marker = QgsVectorFieldSymbolLayer()
field_marker.setXAttribute('x')
field_marker.setYAttribute('y')
field_marker.setScale(4)
field_marker.setVectorFieldType(QgsVectorFieldSymbolLayer.Height)

field_marker.setSubSymbol(QgsLineSymbol.createSimple({'color': '#ff0000', 'width': '2'}))

s.appendSymbolLayer(field_marker.clone())

g = QgsGeometry.fromWkt('Point(5 4)')
fields = QgsFields()
fields.append(QgsField('x', QVariant.Double))
fields.append(QgsField('y', QVariant.Double))
f = QgsFeature(fields)
f.setAttributes([2, 3])
f.setGeometry(g)

rendered_image = self.renderFeature(s, f)
assert self.imageCheck('height', 'height', rendered_image)

def testPolar(self):
# test rendering
s = QgsMarkerSymbol()
s.deleteSymbolLayer(0)

field_marker = QgsVectorFieldSymbolLayer()
field_marker.setXAttribute('x')
field_marker.setYAttribute('y')
field_marker.setVectorFieldType(QgsVectorFieldSymbolLayer.Polar)
field_marker.setScale(1)

field_marker.setSubSymbol(QgsLineSymbol.createSimple({'color': '#ff0000', 'width': '2'}))

s.appendSymbolLayer(field_marker.clone())

g = QgsGeometry.fromWkt('Point(5 4)')
fields = QgsFields()
fields.append(QgsField('x', QVariant.Double))
fields.append(QgsField('y', QVariant.Double))
f = QgsFeature(fields)
f.setAttributes([6, 135])
f.setGeometry(g)

rendered_image = self.renderFeature(s, f)
assert self.imageCheck('polar', 'polar', rendered_image)

def testPolarAnticlockwise(self):
# test rendering
s = QgsMarkerSymbol()
s.deleteSymbolLayer(0)

field_marker = QgsVectorFieldSymbolLayer()
field_marker.setXAttribute('x')
field_marker.setYAttribute('y')
field_marker.setVectorFieldType(QgsVectorFieldSymbolLayer.Polar)
field_marker.setAngleOrientation(QgsVectorFieldSymbolLayer.CounterclockwiseFromEast)
field_marker.setScale(1)

field_marker.setSubSymbol(QgsLineSymbol.createSimple({'color': '#ff0000', 'width': '2'}))

s.appendSymbolLayer(field_marker.clone())

g = QgsGeometry.fromWkt('Point(5 4)')
fields = QgsFields()
fields.append(QgsField('x', QVariant.Double))
fields.append(QgsField('y', QVariant.Double))
f = QgsFeature(fields)
f.setAttributes([6, 135])
f.setGeometry(g)

rendered_image = self.renderFeature(s, f)
assert self.imageCheck('anticlockwise_polar', 'anticlockwise_polar', rendered_image)

def testPolarRadians(self):
# test rendering
s = QgsMarkerSymbol()
s.deleteSymbolLayer(0)

field_marker = QgsVectorFieldSymbolLayer()
field_marker.setXAttribute('x')
field_marker.setYAttribute('y')
field_marker.setVectorFieldType(QgsVectorFieldSymbolLayer.Polar)
field_marker.setScale(1)
field_marker.setAngleUnits(QgsVectorFieldSymbolLayer.Radians)

field_marker.setSubSymbol(QgsLineSymbol.createSimple({'color': '#ff0000', 'width': '2'}))

s.appendSymbolLayer(field_marker.clone())

g = QgsGeometry.fromWkt('Point(5 4)')
fields = QgsFields()
fields.append(QgsField('x', QVariant.Double))
fields.append(QgsField('y', QVariant.Double))
f = QgsFeature(fields)
f.setAttributes([6, 135])
f.setGeometry(g)

rendered_image = self.renderFeature(s, f)
assert self.imageCheck('radians_polar', 'radians_polar', rendered_image)

def renderFeature(self, symbol, f, buffer=20, map_rotation=0):
image = QImage(200, 200, QImage.Format_RGB32)

painter = QPainter()
ms = QgsMapSettings()
extent = f.geometry().constGet().boundingBox()
# buffer extent by 10%
if extent.width() > 0:
extent = extent.buffered((extent.height() + extent.width()) / buffer)
else:
extent = extent.buffered(buffer / 2)

ms.setExtent(extent)
ms.setOutputSize(image.size())
ms.setRotation(map_rotation)
context = QgsRenderContext.fromMapSettings(ms)
context.setPainter(painter)
context.setScaleFactor(96 / 25.4) # 96 DPI
context.expressionContext().setFeature(f)

painter.begin(image)
try:
image.fill(QColor(0, 0, 0))
symbol.startRender(context, f.fields())
symbol.renderFeature(f, context)
symbol.stopRender(context)
finally:
painter.end()

return image

def imageCheck(self, name, reference_image, image):
self.report += "<h2>Render {}</h2>\n".format(name)
temp_dir = QDir.tempPath() + '/'
file_name = temp_dir + 'symbol_' + name + ".png"
image.save(file_name, "PNG")
checker = QgsRenderChecker()
checker.setControlPathPrefix("symbol_vectorfield")
checker.setControlName("expected_" + reference_image)
checker.setRenderedImage(file_name)
checker.setColorTolerance(2)
result = checker.compareImages(name, 20)
self.report += checker.report()
print((self.report))
return result


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

0 comments on commit fdaf54c

Please sign in to comment.