Skip to content

Commit

Permalink
Tests for tile size
Browse files Browse the repository at this point in the history
  • Loading branch information
elpaso committed Jan 11, 2023
1 parent 889a263 commit 0ddee0b
Show file tree
Hide file tree
Showing 5 changed files with 232 additions and 31 deletions.
Expand Up @@ -1002,6 +1002,10 @@ Evaluates a map of properties using the given ``context`` and returns a variant
Calculate the minimum size in pixels of a symbol tile given the symbol ``width`` and ``height`` and the grid rotation ``angle`` in radians.
The method makes approximations and can modify ``angle`` in order to generate the smallest possible tile.

.. note::

Angle must be >= 0 and < 2 * PI

.. versionadded:: 3.30
%End

Expand Down
11 changes: 3 additions & 8 deletions src/core/symbology/qgsfillsymbollayer.cpp
Expand Up @@ -409,7 +409,7 @@ void QgsSimpleFillSymbolLayer::toSld( QDomDocument &doc, QDomElement &element, c
const QgsSldExportContext context { props.value( QStringLiteral( "SldExportContext" ), QVariant::fromValue( QgsSldExportContext() ) ).value< QgsSldExportContext >() };


// Export to PNG (TODO: SVG)
// Export to PNG
bool exportOk { false };
if ( ! context.exportFilePath().isEmpty() && context.exportOptions().testFlag( Qgis::SldExportOption::Png ) && mBrush.style() != Qt::NoBrush )
{
Expand Down Expand Up @@ -2648,7 +2648,7 @@ QImage QgsLinePatternFillSymbolLayer::toTiledPattern() const

QSize size { static_cast<int>( distancePx ), static_cast<int>( distancePx ) };

if ( ! qgsDoubleNear( lineAngleRads, 0 ) )
if ( static_cast<int>( mLineAngle ) % 90 != 0 )
{
size = QSize( static_cast<int>( distancePx / std::sin( lineAngleRads ) ), static_cast<int>( distancePx / std::cos( lineAngleRads ) ) );
}
Expand Down Expand Up @@ -4416,12 +4416,7 @@ QImage QgsPointPatternFillSymbolLayer::toTiledPattern() const
int distanceXPx { static_cast<int>( QgsSymbolLayerUtils::rescaleUom( mDistanceX, mDistanceXUnit, {} ) ) };
int distanceYPx { static_cast<int>( QgsSymbolLayerUtils::rescaleUom( mDistanceY, mDistanceYUnit, {} ) ) };

QSize size { static_cast<int>( distanceXPx ), static_cast<int>( distanceYPx ) };

if ( ! qgsDoubleNear( angleRads, 0 ) )
{
size = QgsSymbolLayerUtils::tileSize( distanceXPx, distanceYPx, angleRads );
}
const QSize size { QgsSymbolLayerUtils::tileSize( distanceXPx, distanceYPx, angleRads ) };

QPixmap pixmap( size );
pixmap.fill( Qt::transparent );
Expand Down
211 changes: 188 additions & 23 deletions src/core/symbology/qgssymbollayerutils.cpp
Expand Up @@ -5101,13 +5101,118 @@ QgsStringMap QgsSymbolLayerUtils::evaluatePropertiesMap( const QMap<QString, Qgs
QSize QgsSymbolLayerUtils::tileSize( int width, int height, double &angleRad )
{

// Precondition
Q_ASSERT( angleRad >= 0 && angleRad < M_PI * 2 );

// tan with rational sin/cos
struct rationalTangent
{
int a;
int b;
double angle;
int p; // numerator
int q; // denominator
double angle; // "good" angle
};

#if 0

// This list is more granular (approx 1 degree steps) but some
// values can lead to huge tiles
// List of "good" angles from 0 to PI/2
static const QList<rationalTangent> __rationalTangents
{
{ 1, 57, 0.01754206006 },
{ 3, 86, 0.03486958155 },
{ 1, 19, 0.05258306161 },
{ 3, 43, 0.06965457373 },
{ 7, 80, 0.08727771295 },
{ 2, 19, 0.1048769387 },
{ 7, 57, 0.1221951707 },
{ 9, 64, 0.1397088743 },
{ 13, 82, 0.157228051 },
{ 3, 17, 0.174672199 },
{ 7, 36, 0.1920480172 },
{ 17, 80, 0.209385393 },
{ 3, 13, 0.2267988481 },
{ 1, 4, 0.2449786631 },
{ 26, 97, 0.2618852647 },
{ 27, 94, 0.2797041525 },
{ 26, 85, 0.2968446734 },
{ 13, 40, 0.3142318991 },
{ 21, 61, 0.3315541619 },
{ 4, 11, 0.3487710036 },
{ 38, 99, 0.3664967859 },
{ 40, 99, 0.383984624 },
{ 31, 73, 0.4015805401 },
{ 41, 92, 0.4192323938 },
{ 7, 15, 0.4366271598 },
{ 20, 41, 0.4538440015 },
{ 27, 53, 0.4711662643 },
{ 42, 79, 0.4886424026 },
{ 51, 92, 0.5061751436 },
{ 56, 97, 0.5235757641 },
{ 3, 5, 0.5404195003 },
{ 5, 8, 0.5585993153 },
{ 50, 77, 0.5759185996 },
{ 29, 43, 0.5933501462 },
{ 7, 10, 0.6107259644 },
{ 69, 95, 0.6281701124 },
{ 52, 69, 0.6458159195 },
{ 25, 32, 0.6632029927 },
{ 17, 21, 0.6805212247 },
{ 73, 87, 0.6981204504 },
{ 73, 84, 0.7154487784 },
{ 9, 10, 0.7328151018 },
{ 83, 89, 0.7505285818 },
{ 28, 29, 0.7678561033 },
{ 1, 1, 0.7853981634 },
{ 29, 28, 0.8029402235 },
{ 89, 83, 0.820267745 },
{ 10, 9, 0.837981225 },
{ 107, 93, 0.855284165 },
{ 87, 73, 0.8726758763 },
{ 121, 98, 0.8900374031 },
{ 32, 25, 0.9075933341 },
{ 69, 52, 0.9249804073 },
{ 128, 93, 0.9424647244 },
{ 10, 7, 0.9600703624 },
{ 43, 29, 0.9774461806 },
{ 77, 50, 0.9948777272 },
{ 8, 5, 1.012197011 },
{ 163, 98, 1.029475114 },
{ 168, 97, 1.047174539 },
{ 175, 97, 1.064668696 },
{ 126, 67, 1.082075603 },
{ 157, 80, 1.099534652 },
{ 203, 99, 1.117049384 },
{ 193, 90, 1.134452855 },
{ 146, 65, 1.151936673 },
{ 139, 59, 1.169382787 },
{ 99, 40, 1.186811703 },
{ 211, 81, 1.204257817 },
{ 272, 99, 1.221730164 },
{ 273, 94, 1.239188479 },
{ 277, 90, 1.25664606 },
{ 157, 48, 1.274088705 },
{ 279, 80, 1.291550147 },
{ 362, 97, 1.308990773 },
{ 373, 93, 1.326448578 },
{ 420, 97, 1.343823596 },
{ 207, 44, 1.361353157 },
{ 427, 83, 1.378810994 },
{ 414, 73, 1.396261926 },
{ 322, 51, 1.413716057 },
{ 185, 26, 1.431170275 },
{ 790, 97, 1.448623034 },
{ 333, 35, 1.466075711 },
{ 1063, 93, 1.483530284 },
{ 1330, 93, 1.500985147 },
{ 706, 37, 1.518436297 },
{ 315, 11, 1.535889876 },
{ 3953, 69, 1.553343002 },
};
#endif

// Optimized "good" angles list, it produces small tiles but
// it has approximately 10 degrees steps
static const QList<rationalTangent> rationalTangents
{
{ 1, 10, qDegreesToRadians( 5.71059 ) },
Expand All @@ -5124,42 +5229,102 @@ QSize QgsSymbolLayerUtils::tileSize( int width, int height, double &angleRad )
{ 10, 1, qDegreesToRadians( 84.2894 ) },
};

// TODO: clean angleRad
const int quadrant { static_cast<int>( angleRad / M_PI_2 ) };
Q_ASSERT( quadrant >= 0 && quadrant <= 3 );

QSize tileSize;

switch ( quadrant )
{
case 0:
{
break;
}
case 1:
{
angleRad -= M_PI / 2;
break;
}
case 2:
{
angleRad -= M_PI;
break;
}
case 3:
{
angleRad -= M_PI + M_PI_2;
break;
}
}

if ( qgsDoubleNear( angleRad, 0 ) )
{
angleRad = 0;
return QSize( width, height );
tileSize.setWidth( width );
tileSize.setHeight( height );
}

if ( qgsDoubleNear( angleRad, M_PI_2 ) )
else if ( qgsDoubleNear( angleRad, M_PI_2 ) )
{
angleRad = M_PI_2;
return QSize( height, width );
tileSize.setWidth( height );
tileSize.setHeight( width );
}
else
{

int rTanIdx = 0;

for ( int idx = 0; idx < rationalTangents.count(); ++idx )
{
const auto item = rationalTangents.at( idx );
if ( qgsDoubleNear( item.angle, angleRad ) || item.angle > angleRad )
{
angleRad = item.angle;
rTanIdx = idx;
break;
}
}

int rTanIdx = 0;
const rationalTangent bTan { rationalTangents.at( rTanIdx ) };
angleRad = bTan.angle;
const double k { bTan.q *height *width / std::cos( angleRad ) };
const int hcfH { std::gcd( bTan.p * height, bTan.q * width ) };
const int hcfW { std::gcd( bTan.q * height, bTan.p * width ) };
const int W1 { static_cast<int>( std::round( k / hcfW ) ) };
const int H1 { static_cast<int>( std::round( k / hcfH ) ) };
tileSize.setWidth( W1 );
tileSize.setHeight( H1 );
}

for ( int idx = 0; idx < rationalTangents.count(); ++idx )
switch ( quadrant )
{
const auto item = rationalTangents.at( idx );
if ( qgsDoubleNear( item.angle, angleRad ) || item.angle > angleRad )
case 0:
{
angleRad = item.angle;
rTanIdx = idx;
break;
}
case 1:
{
angleRad += M_PI / 2;
const int h { tileSize.height() };
tileSize.setHeight( tileSize.width() );
tileSize.setWidth( h );
break;
}
case 2:
{
angleRad += M_PI;
break;
}
case 3:
{
angleRad += M_PI + M_PI_2;
const int h { tileSize.height() };
tileSize.setHeight( tileSize.width() );
tileSize.setWidth( h );
break;
}
}

const rationalTangent bTan { rationalTangents.at( rTanIdx ) };
angleRad = bTan.angle;
const double k { bTan.b *height *width / std::cos( angleRad ) };
const int hcfH { std::gcd( bTan.a * height, bTan.b * width ) };
const int hcfW { std::gcd( bTan.b * height, bTan.a * width ) };
const int W1 { static_cast<int>( std::round( k / hcfW ) ) };
const int H1 { static_cast<int>( std::round( k / hcfH ) ) };

return QSize( W1, H1 );
return tileSize;

}
1 change: 1 addition & 0 deletions src/core/symbology/qgssymbollayerutils.h
Expand Up @@ -900,6 +900,7 @@ class CORE_EXPORT QgsSymbolLayerUtils
/**
* 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.
* 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
* \since QGIS 3.30
*/
static QSize tileSize( int width, int height, double &angleRad SIP_INOUT );
Expand Down
36 changes: 36 additions & 0 deletions tests/src/python/test_qgssymbollayerutils.py
Expand Up @@ -11,6 +11,7 @@
__copyright__ = 'Copyright 2016, The QGIS Project'

import qgis # NOQA
import math
from qgis.PyQt.QtCore import (
QSizeF,
QPointF,
Expand Down Expand Up @@ -662,6 +663,41 @@ def imageCheck(self, name, reference_image, image):
PyQgsSymbolLayerUtils.report += checker.report()
return result

def testTileSize(self):

test_data = [
# First quadrant
[10, 20, 0, 10, 20, 0],
[10, 20, math.pi, 10, 20, math.pi],
[10, 10, math.pi / 4, 10 * math.sqrt(2), 10 * math.sqrt(2), math.pi / 4],
[10, 20, math.pi / 2, 20, 10, math.pi / 2],
[10, 20, math.pi / 4, 20 * math.sqrt(2), 20 * math.sqrt(2), math.pi / 4],
[10, 20, math.pi / 6, 36, 72, 0.5880031703261417], # Angle approx

# Second quadrant
[10, 20, math.pi / 2 + math.pi / 6, 72, 36, math.pi / 2 + 0.5880031703261417], # Angle approx
[10, 10, 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, 10, 20, math.pi / 2 + math.pi / 2],
[10, 20, 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 + math.pi / 6, 36, 72, math.pi + 0.5880031703261417], # Angle approx
[10, 10, math.pi + math.pi / 4, 10 * math.sqrt(2), 10 * math.sqrt(2), math.pi + math.pi / 4],
[10, 20, math.pi + math.pi / 2, 20, 10, math.pi + math.pi / 2],
[10, 20, math.pi + math.pi / 4, 20 * math.sqrt(2), 20 * math.sqrt(2), math.pi + math.pi / 4],

# Fourth quadrant
[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],
]

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.width(), int(exp_width))
self.assertAlmostEqual(res_angle, exp_angle)


if __name__ == '__main__':
unittest.main()

0 comments on commit 0ddee0b

Please sign in to comment.