Skip to content

Commit

Permalink
Fix rendering of Vector Field marker symbol layer when map is rotated
Browse files Browse the repository at this point in the history
Fixes #40916

(cherry picked from commit 157bdca)
  • Loading branch information
nyalldawson committed Feb 19, 2021
1 parent ea7e329 commit fdaf54c
Show file tree
Hide file tree
Showing 9 changed files with 302 additions and 20 deletions.
56 changes: 36 additions & 20 deletions src/core/symbology/qgsvectorfieldsymbollayer.cpp
Expand Up @@ -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 )
Expand Down
1 change: 1 addition & 0 deletions tests/src/python/CMakeLists.txt
Expand Up @@ -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)
Expand Down
265 changes: 265 additions & 0 deletions tests/src/python/test_qgsvectorfieldmarkersymbollayer.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()
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit fdaf54c

Please sign in to comment.