diff --git a/src/core/symbology/qgsvectorfieldsymbollayer.cpp b/src/core/symbology/qgsvectorfieldsymbollayer.cpp
index d67acecc4c0a..e065a6c25ab9 100644
--- a/src/core/symbology/qgsvectorfieldsymbollayer.cpp
+++ b/src/core/symbology/qgsvectorfieldsymbollayer.cpp
@@ -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 )
diff --git a/tests/src/python/CMakeLists.txt b/tests/src/python/CMakeLists.txt
index 926f6f5f23f6..716206bb86d0 100644
--- a/tests/src/python/CMakeLists.txt
+++ b/tests/src/python/CMakeLists.txt
@@ -311,6 +311,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)
diff --git a/tests/src/python/test_qgsvectorfieldmarkersymbollayer.py b/tests/src/python/test_qgsvectorfieldmarkersymbollayer.py
new file mode 100644
index 000000000000..f7ff7a98b73c
--- /dev/null
+++ b/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 = "
Python QgsVectorFieldMarkerSymbolLayer Tests
\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 += "Render {}
\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()
diff --git a/tests/testdata/control_images/symbol_vectorfield/expected_anticlockwise_polar/expected_anticlockwise_polar.png b/tests/testdata/control_images/symbol_vectorfield/expected_anticlockwise_polar/expected_anticlockwise_polar.png
new file mode 100644
index 000000000000..3bfd2eb79a32
Binary files /dev/null and b/tests/testdata/control_images/symbol_vectorfield/expected_anticlockwise_polar/expected_anticlockwise_polar.png differ
diff --git a/tests/testdata/control_images/symbol_vectorfield/expected_height/expected_height.png b/tests/testdata/control_images/symbol_vectorfield/expected_height/expected_height.png
new file mode 100644
index 000000000000..ce054628a063
Binary files /dev/null and b/tests/testdata/control_images/symbol_vectorfield/expected_height/expected_height.png differ
diff --git a/tests/testdata/control_images/symbol_vectorfield/expected_polar/expected_polar.png b/tests/testdata/control_images/symbol_vectorfield/expected_polar/expected_polar.png
new file mode 100644
index 000000000000..3314a6c5fffc
Binary files /dev/null and b/tests/testdata/control_images/symbol_vectorfield/expected_polar/expected_polar.png differ
diff --git a/tests/testdata/control_images/symbol_vectorfield/expected_radians_polar/expected_radians_polar.png b/tests/testdata/control_images/symbol_vectorfield/expected_radians_polar/expected_radians_polar.png
new file mode 100644
index 000000000000..f8e979781ac4
Binary files /dev/null and b/tests/testdata/control_images/symbol_vectorfield/expected_radians_polar/expected_radians_polar.png differ
diff --git a/tests/testdata/control_images/symbol_vectorfield/expected_rotated_map/expected_rotated_map.png b/tests/testdata/control_images/symbol_vectorfield/expected_rotated_map/expected_rotated_map.png
new file mode 100644
index 000000000000..e6afa2a379c7
Binary files /dev/null and b/tests/testdata/control_images/symbol_vectorfield/expected_rotated_map/expected_rotated_map.png differ
diff --git a/tests/testdata/control_images/symbol_vectorfield/expected_vectorfield/expected_vectorfield.png b/tests/testdata/control_images/symbol_vectorfield/expected_vectorfield/expected_vectorfield.png
new file mode 100644
index 000000000000..0b015c178660
Binary files /dev/null and b/tests/testdata/control_images/symbol_vectorfield/expected_vectorfield/expected_vectorfield.png differ