Merge pull request #3477 from nyalldawson/legend_col_align
Fixes to multicolumn legends
@ -265,17 +265,12 @@ void QgsLegendRenderer::setColumns( QList<Atom>& atomList )
|
|||||||
|
|
||||||
// Divide atoms to columns
|
// Divide atoms to columns
|
||||||
double totalHeight = 0;
|
double totalHeight = 0;
|
||||||
// bool first = true;
|
|
||||||
qreal maxAtomHeight = 0;
|
qreal maxAtomHeight = 0;
|
||||||
Q_FOREACH ( const Atom& atom, atomList )
|
Q_FOREACH ( const Atom& atom, atomList )
|
||||||
{
|
{
|
||||||
//if ( !first )
|
|
||||||
//{
|
|
||||||
totalHeight += spaceAboveAtom( atom );
|
totalHeight += spaceAboveAtom( atom );
|
||||||
//}
|
|
||||||
totalHeight += atom.size.height();
|
totalHeight += atom.size.height();
|
||||||
maxAtomHeight = qMax( atom.size.height(), maxAtomHeight );
|
maxAtomHeight = qMax( atom.size.height(), maxAtomHeight );
|
||||||
// first = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// We know height of each atom and we have to split them into columns
|
// We know height of each atom and we have to split them into columns
|
||||||
@ -283,31 +278,36 @@ void QgsLegendRenderer::setColumns( QList<Atom>& atomList )
|
|||||||
// We are using simple heuristic, brute fore appeared to be to slow,
|
// We are using simple heuristic, brute fore appeared to be to slow,
|
||||||
// the number of combinations is N = n!/(k!*(n-k)!) where n = atomsCount-1
|
// the number of combinations is N = n!/(k!*(n-k)!) where n = atomsCount-1
|
||||||
// and k = columnsCount-1
|
// and k = columnsCount-1
|
||||||
|
double maxColumnHeight = 0;
|
||||||
double avgColumnHeight = totalHeight / mSettings.columnCount();
|
|
||||||
int currentColumn = 0;
|
int currentColumn = 0;
|
||||||
int currentColumnAtomCount = 0; // number of atoms in current column
|
int currentColumnAtomCount = 0; // number of atoms in current column
|
||||||
double currentColumnHeight = 0;
|
double currentColumnHeight = 0;
|
||||||
double maxColumnHeight = 0;
|
|
||||||
double closedColumnsHeight = 0;
|
double closedColumnsHeight = 0;
|
||||||
// first = true; // first in column
|
|
||||||
for ( int i = 0; i < atomList.size(); i++ )
|
for ( int i = 0; i < atomList.size(); i++ )
|
||||||
{
|
{
|
||||||
Atom atom = atomList[i];
|
// Recalc average height for remaining columns including current
|
||||||
|
double avgColumnHeight = ( totalHeight - closedColumnsHeight ) / ( mSettings.columnCount() - currentColumn );
|
||||||
|
|
||||||
|
Atom atom = atomList.at( i );
|
||||||
double currentHeight = currentColumnHeight;
|
double currentHeight = currentColumnHeight;
|
||||||
//if ( !first )
|
if ( currentColumnAtomCount > 0 )
|
||||||
//{
|
currentHeight += spaceAboveAtom( atom );
|
||||||
currentHeight += spaceAboveAtom( atom );
|
|
||||||
//}
|
|
||||||
currentHeight += atom.size.height();
|
currentHeight += atom.size.height();
|
||||||
|
|
||||||
// Recalc average height for remaining columns including current
|
bool canCreateNewColumn = ( currentColumnAtomCount > 0 ) // do not leave empty column
|
||||||
avgColumnHeight = ( totalHeight - closedColumnsHeight ) / ( mSettings.columnCount() - currentColumn );
|
&& ( currentColumn < mSettings.columnCount() - 1 ); // must not exceed max number of columns
|
||||||
if (( currentHeight - avgColumnHeight ) > atom.size.height() / 2 // center of current atom is over average height
|
|
||||||
&& currentColumnAtomCount > 0 // do not leave empty column
|
bool shouldCreateNewColumn = ( currentHeight - avgColumnHeight ) > atom.size.height() / 2 // center of current atom is over average height
|
||||||
&& currentHeight > maxAtomHeight // no sense to make smaller columns than max atom height
|
&& currentColumnAtomCount > 0 // do not leave empty column
|
||||||
&& currentHeight > maxColumnHeight // no sense to make smaller columns than max column already created
|
&& currentHeight > maxAtomHeight // no sense to make smaller columns than max atom height
|
||||||
&& currentColumn < mSettings.columnCount() - 1 ) // must not exceed max number of columns
|
&& currentHeight > maxColumnHeight; // no sense to make smaller columns than max column already created
|
||||||
|
|
||||||
|
// also should create a new column if the number of items left < number of columns left
|
||||||
|
// in this case we should spread the remaining items out over the remaining columns
|
||||||
|
shouldCreateNewColumn |= ( atomList.size() - i < mSettings.columnCount() - currentColumn );
|
||||||
|
|
||||||
|
if ( canCreateNewColumn && shouldCreateNewColumn )
|
||||||
{
|
{
|
||||||
// New column
|
// New column
|
||||||
currentColumn++;
|
currentColumn++;
|
||||||
@ -322,11 +322,9 @@ void QgsLegendRenderer::setColumns( QList<Atom>& atomList )
|
|||||||
atomList[i].column = currentColumn;
|
atomList[i].column = currentColumn;
|
||||||
currentColumnAtomCount++;
|
currentColumnAtomCount++;
|
||||||
maxColumnHeight = qMax( currentColumnHeight, maxColumnHeight );
|
maxColumnHeight = qMax( currentColumnHeight, maxColumnHeight );
|
||||||
|
|
||||||
// first = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Alling labels of symbols for each layr/column to the same labelXOffset
|
// Align labels of symbols for each layr/column to the same labelXOffset
|
||||||
QMap<QString, qreal> maxSymbolWidth;
|
QMap<QString, qreal> maxSymbolWidth;
|
||||||
for ( int i = 0; i < atomList.size(); i++ )
|
for ( int i = 0; i < atomList.size(); i++ )
|
||||||
{
|
{
|
||||||
|
@ -118,6 +118,8 @@ class TestQgsLegendRenderer : public QObject
|
|||||||
void testThreeColumns();
|
void testThreeColumns();
|
||||||
void testFilterByMap();
|
void testFilterByMap();
|
||||||
void testFilterByMapSameSymbol();
|
void testFilterByMapSameSymbol();
|
||||||
|
void testColumns_data();
|
||||||
|
void testColumns();
|
||||||
void testRasterBorder();
|
void testRasterBorder();
|
||||||
void testFilterByPolygon();
|
void testFilterByPolygon();
|
||||||
void testFilterByExpression();
|
void testFilterByExpression();
|
||||||
@ -131,6 +133,7 @@ class TestQgsLegendRenderer : public QObject
|
|||||||
QgsVectorLayer* mVL3; // point
|
QgsVectorLayer* mVL3; // point
|
||||||
QgsRasterLayer* mRL;
|
QgsRasterLayer* mRL;
|
||||||
QString mReport;
|
QString mReport;
|
||||||
|
bool _testLegendColumns( int itemCount, int columnCount, const QString& testName );
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@ -459,6 +462,65 @@ void TestQgsLegendRenderer::testFilterByMapSameSymbol()
|
|||||||
QgsMapLayerRegistry::instance()->removeMapLayer( vl4 );
|
QgsMapLayerRegistry::instance()->removeMapLayer( vl4 );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool TestQgsLegendRenderer::_testLegendColumns( int itemCount, int columnCount, const QString& testName )
|
||||||
|
{
|
||||||
|
QgsFillSymbol* sym = new QgsFillSymbol();
|
||||||
|
sym->setColor( Qt::cyan );
|
||||||
|
|
||||||
|
QgsLayerTreeGroup* root = new QgsLayerTreeGroup();
|
||||||
|
|
||||||
|
QList< QgsVectorLayer* > layers;
|
||||||
|
for ( int i = 1; i <= itemCount; ++i )
|
||||||
|
{
|
||||||
|
QgsVectorLayer* vl = new QgsVectorLayer( "Polygon", QString( "Layer %1" ).arg( i ), "memory" );
|
||||||
|
QgsMapLayerRegistry::instance()->addMapLayer( vl );
|
||||||
|
vl->setRenderer( new QgsSingleSymbolRenderer( sym->clone() ) );
|
||||||
|
root->addLayer( vl );
|
||||||
|
layers << vl;
|
||||||
|
}
|
||||||
|
delete sym;
|
||||||
|
|
||||||
|
QgsLayerTreeModel legendModel( root );
|
||||||
|
QgsLegendSettings settings;
|
||||||
|
settings.setColumnCount( columnCount );
|
||||||
|
_setStandardTestFont( settings, "Bold" );
|
||||||
|
_renderLegend( testName, &legendModel, settings );
|
||||||
|
bool result = _verifyImage( testName, mReport );
|
||||||
|
|
||||||
|
Q_FOREACH ( QgsVectorLayer* l, layers )
|
||||||
|
{
|
||||||
|
QgsMapLayerRegistry::instance()->removeMapLayer( l );
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestQgsLegendRenderer::testColumns_data()
|
||||||
|
{
|
||||||
|
QTest::addColumn<QString>( "testName" );
|
||||||
|
QTest::addColumn<int>( "items" );
|
||||||
|
QTest::addColumn<int>( "columns" );
|
||||||
|
|
||||||
|
QTest::newRow( "2 items, 2 columns" ) << "legend_2_by_2" << 2 << 2;
|
||||||
|
QTest::newRow( "3 items, 2 columns" ) << "legend_3_by_2" << 3 << 2;
|
||||||
|
QTest::newRow( "4 items, 2 columns" ) << "legend_4_by_2" << 4 << 2;
|
||||||
|
QTest::newRow( "5 items, 2 columns" ) << "legend_5_by_2" << 5 << 2;
|
||||||
|
QTest::newRow( "3 items, 3 columns" ) << "legend_3_by_3" << 3 << 3;
|
||||||
|
QTest::newRow( "4 items, 3 columns" ) << "legend_4_by_3" << 4 << 3;
|
||||||
|
QTest::newRow( "5 items, 3 columns" ) << "legend_5_by_3" << 5 << 3;
|
||||||
|
QTest::newRow( "6 items, 3 columns" ) << "legend_6_by_3" << 6 << 3;
|
||||||
|
QTest::newRow( "7 items, 3 columns" ) << "legend_7_by_3" << 7 << 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestQgsLegendRenderer::testColumns()
|
||||||
|
{
|
||||||
|
//test rendering legend with different combinations of columns and items
|
||||||
|
|
||||||
|
QFETCH( QString, testName );
|
||||||
|
QFETCH( int, items );
|
||||||
|
QFETCH( int, columns );
|
||||||
|
QVERIFY( _testLegendColumns( items, columns, testName ) );
|
||||||
|
}
|
||||||
|
|
||||||
void TestQgsLegendRenderer::testRasterBorder()
|
void TestQgsLegendRenderer::testRasterBorder()
|
||||||
{
|
{
|
||||||
QString testName = "legend_raster_border";
|
QString testName = "legend_raster_border";
|
||||||
|
BIN
tests/testdata/control_images/legend/expected_legend_2_by_2/expected_legend_2_by_2.png
vendored
Normal file
After Width: | Height: | Size: 4.4 KiB |
BIN
tests/testdata/control_images/legend/expected_legend_2_by_2/expected_legend_2_by_2_mask.png
vendored
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
tests/testdata/control_images/legend/expected_legend_3_by_2/expected_legend_3_by_2.png
vendored
Normal file
After Width: | Height: | Size: 5.2 KiB |
BIN
tests/testdata/control_images/legend/expected_legend_3_by_2/expected_legend_3_by_2_mask.png
vendored
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
tests/testdata/control_images/legend/expected_legend_3_by_3/expected_legend_3_by_3.png
vendored
Normal file
After Width: | Height: | Size: 5.7 KiB |
BIN
tests/testdata/control_images/legend/expected_legend_3_by_3/expected_legend_3_by_3_mask.png
vendored
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
tests/testdata/control_images/legend/expected_legend_4_by_2/expected_legend_4_by_2.png
vendored
Normal file
After Width: | Height: | Size: 5.3 KiB |
BIN
tests/testdata/control_images/legend/expected_legend_4_by_2/expected_legend_4_by_2_mask.png
vendored
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
tests/testdata/control_images/legend/expected_legend_4_by_3/expected_legend_4_by_3.png
vendored
Normal file
After Width: | Height: | Size: 6.4 KiB |
BIN
tests/testdata/control_images/legend/expected_legend_4_by_3/expected_legend_4_by_3_mask.png
vendored
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
tests/testdata/control_images/legend/expected_legend_5_by_2/expected_legend_5_by_2.png
vendored
Normal file
After Width: | Height: | Size: 6.5 KiB |
BIN
tests/testdata/control_images/legend/expected_legend_5_by_2/expected_legend_5_by_2_mask.png
vendored
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
tests/testdata/control_images/legend/expected_legend_5_by_3/expected_legend_5_by_3.png
vendored
Normal file
After Width: | Height: | Size: 6.7 KiB |
BIN
tests/testdata/control_images/legend/expected_legend_5_by_3/expected_legend_5_by_3_mask.png
vendored
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
tests/testdata/control_images/legend/expected_legend_6_by_3/expected_legend_6_by_3.png
vendored
Normal file
After Width: | Height: | Size: 7.3 KiB |
BIN
tests/testdata/control_images/legend/expected_legend_6_by_3/expected_legend_6_by_3_mask.png
vendored
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
tests/testdata/control_images/legend/expected_legend_7_by_3/expected_legend_7_by_3.png
vendored
Normal file
After Width: | Height: | Size: 8.1 KiB |
BIN
tests/testdata/control_images/legend/expected_legend_7_by_3/expected_legend_7_by_3_mask.png
vendored
Normal file
After Width: | Height: | Size: 2.5 KiB |