diff --git a/python/core/auto_generated/qgsfileutils.sip.in b/python/core/auto_generated/qgsfileutils.sip.in index 450ab901f76..81c508e9c26 100644 --- a/python/core/auto_generated/qgsfileutils.sip.in +++ b/python/core/auto_generated/qgsfileutils.sip.in @@ -198,7 +198,7 @@ E.g. if ``path`` is "/home/user/Pictures/test.png", the returned list will conta %Docstring Creates a unique file path name from a desired path by appending "_" (where "" is an integer number) before the file suffix. -E.g. if "/path/my_image.png" already exists "/path/my_image_1.png" (and "_2", "_3" etc.) will be checked until a file path that does not already exist is found. +E.g. if "/path/my_image.png" already exists "/path/my_image_2.png" (and "_3", "_4" etc.) will be checked until a file path that does not already exist is found. :param path: the desired path. diff --git a/python/core/auto_generated/symbology/qgssymbollayer.sip.in b/python/core/auto_generated/symbology/qgssymbollayer.sip.in index 807e429df18..baff1e2244c 100644 --- a/python/core/auto_generated/symbology/qgssymbollayer.sip.in +++ b/python/core/auto_generated/symbology/qgssymbollayer.sip.in @@ -1315,7 +1315,7 @@ The ``rings`` argument optionally specifies a list of polygon rings to render as virtual QImage toTiledPatternImage( ) const; %Docstring -Renders the symbol layer to an image that can be used as a seamless pattern fill +Renders the symbol layer as an image that can be used as a seamless pattern fill for polygons, this method is used by SLD export to generate image tiles for ExternalGraphic polygon fills. diff --git a/python/core/auto_generated/symbology/qgssymbollayerutils.sip.in b/python/core/auto_generated/symbology/qgssymbollayerutils.sip.in index 958ac809665..22bf467531a 100644 --- a/python/core/auto_generated/symbology/qgssymbollayerutils.sip.in +++ b/python/core/auto_generated/symbology/qgssymbollayerutils.sip.in @@ -999,12 +999,14 @@ Evaluates a map of properties using the given ``context`` and returns a variant static QSize tileSize( int width, int height, double &angleRad /In,Out/ ); %Docstring -Calculate the minimum size in pixels of a symbol tile given the symbol ``width`` and ``height`` and the grid rotation ``angle`` in radians. +Calculate the minimum size in pixels of a symbol tile given the symbol ``width`` and ``height`` and the symbol layer rotation ``angleRad`` in radians (counter clockwise). The method makes approximations and can modify ``angle`` in order to generate the smallest possible tile. -.. note:: +:param width: marker width, including margins +:param height: marker height, including margins +:param angleRad: symbol layer rotation angle in radians (counter clockwise), it may be approximated by the method to minimize the tile size. - Angle must be >= 0 and < 2 * PI +:return: the size of the tile .. versionadded:: 3.30 %End diff --git a/src/core/qgsfileutils.cpp b/src/core/qgsfileutils.cpp index 61d761759cb..7fb0ca01a8f 100644 --- a/src/core/qgsfileutils.cpp +++ b/src/core/qgsfileutils.cpp @@ -540,7 +540,7 @@ QString QgsFileUtils::uniquePath( const QString &path ) QFileInfo info { path }; const QString suffix { info.completeSuffix() }; const QString pathPattern { QString( suffix.length() > 0 ? path.chopped( suffix.length() + 1 ) : path ).append( QStringLiteral( "_%1." ) ).append( suffix ) }; - int i { 1 }; + int i { 2 }; QString uniquePath { pathPattern.arg( i ) }; while ( QFileInfo::exists( uniquePath ) ) { diff --git a/src/core/qgsfileutils.h b/src/core/qgsfileutils.h index 003f28fb1d6..c629e099fa0 100644 --- a/src/core/qgsfileutils.h +++ b/src/core/qgsfileutils.h @@ -228,7 +228,7 @@ class CORE_EXPORT QgsFileUtils /** * Creates a unique file path name from a desired path by appending "_" (where "" is an integer number) before the file suffix. * - * E.g. if "/path/my_image.png" already exists "/path/my_image_1.png" (and "_2", "_3" etc.) will be checked until a file path that does not already exist is found. + * E.g. if "/path/my_image.png" already exists "/path/my_image_2.png" (and "_3", "_4" etc.) will be checked until a file path that does not already exist is found. * * \param path the desired path. * \return the unmodified path if path is already unique or the new path with "_" (where "" is an integer number) appended to the file name before the suffix. diff --git a/src/core/symbology/qgssymbollayer.h b/src/core/symbology/qgssymbollayer.h index c312fb126f6..3c61b2f8130 100644 --- a/src/core/symbology/qgssymbollayer.h +++ b/src/core/symbology/qgssymbollayer.h @@ -1242,7 +1242,7 @@ class CORE_EXPORT QgsFillSymbolLayer : public QgsSymbolLayer double angle() const { return mAngle; } /** - * Renders the symbol layer to an image that can be used as a seamless pattern fill + * Renders the symbol layer as an image that can be used as a seamless pattern fill * for polygons, this method is used by SLD export to generate image tiles for * ExternalGraphic polygon fills. * diff --git a/src/core/symbology/qgssymbollayerutils.cpp b/src/core/symbology/qgssymbollayerutils.cpp index 845a8e54db7..19f1245c32f 100644 --- a/src/core/symbology/qgssymbollayerutils.cpp +++ b/src/core/symbology/qgssymbollayerutils.cpp @@ -5101,8 +5101,12 @@ QgsStringMap QgsSymbolLayerUtils::evaluatePropertiesMap( const QMap= 0 && angleRad < M_PI * 2 ); + angleRad = std::fmod( angleRad, M_PI * 2 ); + + if ( angleRad < 0 ) + { + angleRad += M_PI * 2; + } // tan with rational sin/cos struct rationalTangent @@ -5257,13 +5261,13 @@ QSize QgsSymbolLayerUtils::tileSize( int width, int height, double &angleRad ) } } - if ( qgsDoubleNear( angleRad, 0 ) ) + if ( qgsDoubleNear( angleRad, 0, 10E-3 ) ) { angleRad = 0; tileSize.setWidth( width ); tileSize.setHeight( height ); } - else if ( qgsDoubleNear( angleRad, M_PI_2 ) ) + else if ( qgsDoubleNear( angleRad, M_PI_2, 10E-3 ) ) { angleRad = M_PI_2; tileSize.setWidth( height ); @@ -5277,9 +5281,8 @@ QSize QgsSymbolLayerUtils::tileSize( int width, int height, double &angleRad ) for ( int idx = 0; idx < rationalTangents.count(); ++idx ) { const auto item = rationalTangents.at( idx ); - if ( qgsDoubleNear( item.angle, angleRad ) || item.angle > angleRad ) + if ( qgsDoubleNear( item.angle, angleRad, 10E-3 ) || item.angle > angleRad ) { - angleRad = item.angle; rTanIdx = idx; break; } diff --git a/src/core/symbology/qgssymbollayerutils.h b/src/core/symbology/qgssymbollayerutils.h index ecc36eecb5d..e6eab020dd4 100644 --- a/src/core/symbology/qgssymbollayerutils.h +++ b/src/core/symbology/qgssymbollayerutils.h @@ -898,9 +898,12 @@ class CORE_EXPORT QgsSymbolLayerUtils static QgsStringMap evaluatePropertiesMap( const QMap &propertiesMap, const QgsExpressionContext &context ); /** - * Calculate the minimum size in pixels of a symbol tile given the symbol \a width and \a height and the grid rotation \a angle in radians. + * Calculate the minimum size in pixels of a symbol tile given the symbol \a width and \a height and the symbol layer rotation \a angleRad in radians (counter clockwise). * The method makes approximations and can modify \a angle in order to generate the smallest possible tile. - * \note Angle must be >= 0 and < 2 * PI + * \param width marker width, including margins + * \param height marker height, including margins + * \param angleRad symbol layer rotation angle in radians (counter clockwise), it may be approximated by the method to minimize the tile size. + * \return the size of the tile * \since QGIS 3.30 */ static QSize tileSize( int width, int height, double &angleRad SIP_INOUT ); diff --git a/tests/src/python/test_qgsfileutils.py b/tests/src/python/test_qgsfileutils.py index a824b2372ef..e68770600f2 100644 --- a/tests/src/python/test_qgsfileutils.py +++ b/tests/src/python/test_qgsfileutils.py @@ -20,6 +20,7 @@ from qgis.core import ( Qgis, QgsFileUtils ) +from qgis.PyQt.QtCore import QTemporaryDir from qgis.testing import unittest from utilities import unitTestDataPath @@ -311,6 +312,24 @@ class TestQgsFileUtils(unittest.TestCase): self.assertEqual(QgsFileUtils.splitPathToComponents(''), []) self.assertEqual(QgsFileUtils.splitPathToComponents('c:/home/user'), ["c:", "home", "user"]) + def testUniquePath(self): + temp_dir = QTemporaryDir() + temp_path = temp_dir.path() + + with open(os.path.join(temp_path, 'test.txt'), 'w+') as f: + f.close() + + self.assertEqual(QgsFileUtils.uniquePath(os.path.join(temp_path, 'my_test.txt')), os.path.join(temp_path, 'my_test.txt')) + + self.assertEqual(QgsFileUtils.uniquePath(os.path.join(temp_path, 'test.txt')), os.path.join(temp_path, 'test_2.txt')) + + with open(os.path.join(temp_path, 'test_2.txt'), 'w+') as f: + f.close() + + self.assertEqual(QgsFileUtils.uniquePath(os.path.join(temp_path, 'test_2.txt')), os.path.join(temp_path, 'test_2_2.txt')) + self.assertEqual(QgsFileUtils.uniquePath(os.path.join(temp_path, 'test.txt')), os.path.join(temp_path, 'test_3.txt')) + self.assertEqual(QgsFileUtils.uniquePath(os.path.join(temp_path, 'test_1.txt')), os.path.join(temp_path, 'test_1.txt')) + if __name__ == '__main__': unittest.main() diff --git a/tests/src/python/test_qgssymbollayerutils.py b/tests/src/python/test_qgssymbollayerutils.py index f39be95950b..f9b9a396e0d 100644 --- a/tests/src/python/test_qgssymbollayerutils.py +++ b/tests/src/python/test_qgssymbollayerutils.py @@ -690,11 +690,62 @@ class PyQgsSymbolLayerUtils(unittest.TestCase): [10, 20, math.pi + math.pi / 2 + math.pi / 6, 72, 36, math.pi + math.pi / 2 + 0.5880031703261417], # Angle approx [10, 10, math.pi + math.pi / 2 + math.pi / 4, 10 * math.sqrt(2), 10 * math.sqrt(2), math.pi + math.pi / 2 + math.pi / 4], [10, 20, math.pi + math.pi / 2 + math.pi / 4, 20 * math.sqrt(2), 20 * math.sqrt(2), math.pi + math.pi / 2 + math.pi / 4], + + # Test out of range angles > 2 PI + + # First quadrant + [10, 10, math.pi * 2 + math.pi / 4, 10 * math.sqrt(2), 10 * math.sqrt(2), math.pi / 4], + [10, 20, math.pi * 2 + math.pi / 2, 20, 10, math.pi / 2], + [10, 20, math.pi * 2 + math.pi / 4, 20 * math.sqrt(2), 20 * math.sqrt(2), math.pi / 4], + [10, 20, math.pi * 2 + math.pi / 6, 36, 72, 0.5880031703261417], # Angle approx + + # Second quadrant + [10, 20, math.pi * 2 + math.pi / 2 + math.pi / 6, 72, 36, math.pi / 2 + 0.5880031703261417], # Angle approx + [10, 10, math.pi * 2 + math.pi / 2 + math.pi / 4, 10 * math.sqrt(2), 10 * math.sqrt(2), math.pi / 2 + math.pi / 4], + [10, 20, math.pi * 2 + math.pi / 2 + math.pi / 2, 10, 20, math.pi / 2 + math.pi / 2], + [10, 20, math.pi * 2 + math.pi / 2 + math.pi / 4, 20 * math.sqrt(2), 20 * math.sqrt(2), math.pi / 2 + math.pi / 4], + + # Third quadrant + [10, 20, math.pi * 2 + math.pi + math.pi / 6, 36, 72, math.pi + 0.5880031703261417], # Angle approx + [10, 10, math.pi * 2 + math.pi + math.pi / 4, 10 * math.sqrt(2), 10 * math.sqrt(2), math.pi + math.pi / 4], + [10, 20, math.pi * 2 + math.pi + math.pi / 2, 20, 10, math.pi + math.pi / 2], + [10, 20, math.pi * 2 + math.pi + math.pi / 4, 20 * math.sqrt(2), 20 * math.sqrt(2), math.pi + math.pi / 4], + + # Fourth quadrant + [10, 20, math.pi * 2 + math.pi + math.pi / 2 + math.pi / 6, 72, 36, math.pi + math.pi / 2 + 0.5880031703261417], # Angle approx + [10, 10, math.pi * 2 + math.pi + math.pi / 2 + math.pi / 4, 10 * math.sqrt(2), 10 * math.sqrt(2), math.pi + math.pi / 2 + math.pi / 4], + [10, 20, math.pi * 2 + math.pi + math.pi / 2 + math.pi / 4, 20 * math.sqrt(2), 20 * math.sqrt(2), math.pi + math.pi / 2 + math.pi / 4], + + # Test out of range angles < 0 + + # First quadrant + [10, 10, - math.pi * 2 + math.pi / 4, 10 * math.sqrt(2), 10 * math.sqrt(2), math.pi / 4], + [10, 20, - math.pi * 2 + math.pi / 2, 20, 10, math.pi / 2], + [10, 20, - math.pi * 2 + math.pi / 4, 20 * math.sqrt(2), 20 * math.sqrt(2), math.pi / 4], + [10, 20, - math.pi * 2 + math.pi / 6, 36, 72, 0.5880031703261417], # Angle approx + + # Second quadrant + [10, 20, - math.pi * 2 + math.pi / 2 + math.pi / 6, 72, 36, math.pi / 2 + 0.5880031703261417], # Angle approx + [10, 10, - math.pi * 2 + math.pi / 2 + math.pi / 4, 10 * math.sqrt(2), 10 * math.sqrt(2), math.pi / 2 + math.pi / 4], + [10, 20, - math.pi * 2 + math.pi / 2 + math.pi / 2, 10, 20, math.pi / 2 + math.pi / 2], + [10, 20, - math.pi * 2 + math.pi / 2 + math.pi / 4, 20 * math.sqrt(2), 20 * math.sqrt(2), math.pi / 2 + math.pi / 4], + + # Third quadrant + [10, 20, - math.pi * 2 + math.pi + math.pi / 6, 36, 72, math.pi + 0.5880031703261417], # Angle approx + [10, 10, - math.pi * 2 + math.pi + math.pi / 4, 10 * math.sqrt(2), 10 * math.sqrt(2), math.pi + math.pi / 4], + [10, 20, - math.pi * 2 + math.pi + math.pi / 2, 20, 10, math.pi + math.pi / 2], + [10, 20, - math.pi * 2 + math.pi + math.pi / 4, 20 * math.sqrt(2), 20 * math.sqrt(2), math.pi + math.pi / 4], + + # Fourth quadrant + [10, 20, - math.pi * 2 + math.pi + math.pi / 2 + math.pi / 6, 72, 36, math.pi + math.pi / 2 + 0.5880031703261417], # Angle approx + [10, 10, - math.pi * 2 + math.pi + math.pi / 2 + math.pi / 4, 10 * math.sqrt(2), 10 * math.sqrt(2), math.pi + math.pi / 2 + math.pi / 4], + [10, 20, - math.pi * 2 + math.pi + math.pi / 2 + math.pi / 4, 20 * math.sqrt(2), 20 * math.sqrt(2), math.pi + math.pi / 2 + math.pi / 4], + ] for width, height, angle, exp_width, exp_height, exp_angle in test_data: (res_size, res_angle) = QgsSymbolLayerUtils.tileSize(width, height, angle) - self.assertEqual(res_size.height(), int(exp_height)) + self.assertEqual(res_size.height(), int(exp_height), angle) self.assertEqual(res_size.width(), int(exp_width)) self.assertAlmostEqual(res_angle, exp_angle)