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