Skip to content

Commit 648b779

Browse files
authored
Merge pull request #3477 from nyalldawson/legend_col_align
Fixes to multicolumn legends
2 parents 4d0453e + e3313fa commit 648b779

20 files changed

+84
-24
lines changed

src/core/qgslegendrenderer.cpp

Lines changed: 22 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -265,49 +265,49 @@ void QgsLegendRenderer::setColumns( QList<Atom>& atomList )
265265

266266
// Divide atoms to columns
267267
double totalHeight = 0;
268-
// bool first = true;
269268
qreal maxAtomHeight = 0;
270269
Q_FOREACH ( const Atom& atom, atomList )
271270
{
272-
//if ( !first )
273-
//{
274271
totalHeight += spaceAboveAtom( atom );
275-
//}
276272
totalHeight += atom.size.height();
277273
maxAtomHeight = qMax( atom.size.height(), maxAtomHeight );
278-
// first = false;
279274
}
280275

281276
// We know height of each atom and we have to split them into columns
282277
// minimizing max column height. It is sort of bin packing problem, NP-hard.
283278
// We are using simple heuristic, brute fore appeared to be to slow,
284279
// the number of combinations is N = n!/(k!*(n-k)!) where n = atomsCount-1
285280
// and k = columnsCount-1
286-
287-
double avgColumnHeight = totalHeight / mSettings.columnCount();
281+
double maxColumnHeight = 0;
288282
int currentColumn = 0;
289283
int currentColumnAtomCount = 0; // number of atoms in current column
290284
double currentColumnHeight = 0;
291-
double maxColumnHeight = 0;
292285
double closedColumnsHeight = 0;
293-
// first = true; // first in column
286+
294287
for ( int i = 0; i < atomList.size(); i++ )
295288
{
296-
Atom atom = atomList[i];
289+
// Recalc average height for remaining columns including current
290+
double avgColumnHeight = ( totalHeight - closedColumnsHeight ) / ( mSettings.columnCount() - currentColumn );
291+
292+
Atom atom = atomList.at( i );
297293
double currentHeight = currentColumnHeight;
298-
//if ( !first )
299-
//{
300-
currentHeight += spaceAboveAtom( atom );
301-
//}
294+
if ( currentColumnAtomCount > 0 )
295+
currentHeight += spaceAboveAtom( atom );
302296
currentHeight += atom.size.height();
303297

304-
// Recalc average height for remaining columns including current
305-
avgColumnHeight = ( totalHeight - closedColumnsHeight ) / ( mSettings.columnCount() - currentColumn );
306-
if (( currentHeight - avgColumnHeight ) > atom.size.height() / 2 // center of current atom is over average height
307-
&& currentColumnAtomCount > 0 // do not leave empty column
308-
&& currentHeight > maxAtomHeight // no sense to make smaller columns than max atom height
309-
&& currentHeight > maxColumnHeight // no sense to make smaller columns than max column already created
310-
&& currentColumn < mSettings.columnCount() - 1 ) // must not exceed max number of columns
298+
bool canCreateNewColumn = ( currentColumnAtomCount > 0 ) // do not leave empty column
299+
&& ( currentColumn < mSettings.columnCount() - 1 ); // must not exceed max number of columns
300+
301+
bool shouldCreateNewColumn = ( currentHeight - avgColumnHeight ) > atom.size.height() / 2 // center of current atom is over average height
302+
&& currentColumnAtomCount > 0 // do not leave empty column
303+
&& currentHeight > maxAtomHeight // no sense to make smaller columns than max atom height
304+
&& currentHeight > maxColumnHeight; // no sense to make smaller columns than max column already created
305+
306+
// also should create a new column if the number of items left < number of columns left
307+
// in this case we should spread the remaining items out over the remaining columns
308+
shouldCreateNewColumn |= ( atomList.size() - i < mSettings.columnCount() - currentColumn );
309+
310+
if ( canCreateNewColumn && shouldCreateNewColumn )
311311
{
312312
// New column
313313
currentColumn++;
@@ -322,11 +322,9 @@ void QgsLegendRenderer::setColumns( QList<Atom>& atomList )
322322
atomList[i].column = currentColumn;
323323
currentColumnAtomCount++;
324324
maxColumnHeight = qMax( currentColumnHeight, maxColumnHeight );
325-
326-
// first = false;
327325
}
328326

329-
// Alling labels of symbols for each layr/column to the same labelXOffset
327+
// Align labels of symbols for each layr/column to the same labelXOffset
330328
QMap<QString, qreal> maxSymbolWidth;
331329
for ( int i = 0; i < atomList.size(); i++ )
332330
{

tests/src/core/testqgslegendrenderer.cpp

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,8 @@ class TestQgsLegendRenderer : public QObject
118118
void testThreeColumns();
119119
void testFilterByMap();
120120
void testFilterByMapSameSymbol();
121+
void testColumns_data();
122+
void testColumns();
121123
void testRasterBorder();
122124
void testFilterByPolygon();
123125
void testFilterByExpression();
@@ -131,6 +133,7 @@ class TestQgsLegendRenderer : public QObject
131133
QgsVectorLayer* mVL3; // point
132134
QgsRasterLayer* mRL;
133135
QString mReport;
136+
bool _testLegendColumns( int itemCount, int columnCount, const QString& testName );
134137
};
135138

136139

@@ -459,6 +462,65 @@ void TestQgsLegendRenderer::testFilterByMapSameSymbol()
459462
QgsMapLayerRegistry::instance()->removeMapLayer( vl4 );
460463
}
461464

465+
bool TestQgsLegendRenderer::_testLegendColumns( int itemCount, int columnCount, const QString& testName )
466+
{
467+
QgsFillSymbol* sym = new QgsFillSymbol();
468+
sym->setColor( Qt::cyan );
469+
470+
QgsLayerTreeGroup* root = new QgsLayerTreeGroup();
471+
472+
QList< QgsVectorLayer* > layers;
473+
for ( int i = 1; i <= itemCount; ++i )
474+
{
475+
QgsVectorLayer* vl = new QgsVectorLayer( "Polygon", QString( "Layer %1" ).arg( i ), "memory" );
476+
QgsMapLayerRegistry::instance()->addMapLayer( vl );
477+
vl->setRenderer( new QgsSingleSymbolRenderer( sym->clone() ) );
478+
root->addLayer( vl );
479+
layers << vl;
480+
}
481+
delete sym;
482+
483+
QgsLayerTreeModel legendModel( root );
484+
QgsLegendSettings settings;
485+
settings.setColumnCount( columnCount );
486+
_setStandardTestFont( settings, "Bold" );
487+
_renderLegend( testName, &legendModel, settings );
488+
bool result = _verifyImage( testName, mReport );
489+
490+
Q_FOREACH ( QgsVectorLayer* l, layers )
491+
{
492+
QgsMapLayerRegistry::instance()->removeMapLayer( l );
493+
}
494+
return result;
495+
}
496+
497+
void TestQgsLegendRenderer::testColumns_data()
498+
{
499+
QTest::addColumn<QString>( "testName" );
500+
QTest::addColumn<int>( "items" );
501+
QTest::addColumn<int>( "columns" );
502+
503+
QTest::newRow( "2 items, 2 columns" ) << "legend_2_by_2" << 2 << 2;
504+
QTest::newRow( "3 items, 2 columns" ) << "legend_3_by_2" << 3 << 2;
505+
QTest::newRow( "4 items, 2 columns" ) << "legend_4_by_2" << 4 << 2;
506+
QTest::newRow( "5 items, 2 columns" ) << "legend_5_by_2" << 5 << 2;
507+
QTest::newRow( "3 items, 3 columns" ) << "legend_3_by_3" << 3 << 3;
508+
QTest::newRow( "4 items, 3 columns" ) << "legend_4_by_3" << 4 << 3;
509+
QTest::newRow( "5 items, 3 columns" ) << "legend_5_by_3" << 5 << 3;
510+
QTest::newRow( "6 items, 3 columns" ) << "legend_6_by_3" << 6 << 3;
511+
QTest::newRow( "7 items, 3 columns" ) << "legend_7_by_3" << 7 << 3;
512+
}
513+
514+
void TestQgsLegendRenderer::testColumns()
515+
{
516+
//test rendering legend with different combinations of columns and items
517+
518+
QFETCH( QString, testName );
519+
QFETCH( int, items );
520+
QFETCH( int, columns );
521+
QVERIFY( _testLegendColumns( items, columns, testName ) );
522+
}
523+
462524
void TestQgsLegendRenderer::testRasterBorder()
463525
{
464526
QString testName = "legend_raster_border";
Loading
Loading
Loading
Loading
Loading
Loading
Loading
Loading
Loading
Loading
Loading
Loading
Loading
Loading
Loading
Loading
Loading

0 commit comments

Comments
 (0)