QGIS API Documentation  2.8.2-Wien
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Macros Groups Pages
qgslegendrenderer.cpp
Go to the documentation of this file.
1 /***************************************************************************
2  qgslegendrenderer.cpp
3  --------------------------------------
4  Date : July 2014
5  Copyright : (C) 2014 by Martin Dobias
6  Email : wonder dot sk at gmail dot com
7  ***************************************************************************
8  * *
9  * This program is free software; you can redistribute it and/or modify *
10  * it under the terms of the GNU General Public License as published by *
11  * the Free Software Foundation; either version 2 of the License, or *
12  * (at your option) any later version. *
13  * *
14  ***************************************************************************/
15 
16 #include "qgslegendrenderer.h"
17 
18 #include "qgscomposerlegenditem.h"
19 #include "qgslayertree.h"
20 #include "qgslayertreemodel.h"
22 #include "qgslegendmodel.h"
23 #include "qgsmaplayerlegend.h"
24 #include "qgsmaplayerregistry.h"
25 #include "qgssymbolv2.h"
26 #include "qgsvectorlayer.h"
27 
28 #include <QPainter>
29 
30 
31 
33  : mLegendModel( legendModel )
34  , mSettings( settings )
35 {
36 }
37 
39 {
40  return paintAndDetermineSize( 0 );
41 }
42 
43 void QgsLegendRenderer::drawLegend( QPainter* painter )
44 {
45  paintAndDetermineSize( painter );
46 }
47 
48 
49 QSizeF QgsLegendRenderer::paintAndDetermineSize( QPainter* painter )
50 {
51  QSizeF size( 0, 0 );
52  QgsLayerTreeGroup* rootGroup = mLegendModel->rootGroup();
53  if ( !rootGroup ) return size;
54 
55  QList<Atom> atomList = createAtomList( rootGroup, mSettings.splitLayer() );
56 
57  setColumns( atomList );
58 
59  qreal maxColumnWidth = 0;
60  if ( mSettings.equalColumnWidth() )
61  {
62  foreach ( Atom atom, atomList )
63  {
64  maxColumnWidth = qMax( atom.size.width(), maxColumnWidth );
65  }
66  }
67 
68  //calculate size of title
69  QSizeF titleSize = drawTitle();
70  //add title margin to size of title text
71  titleSize.rwidth() += mSettings.boxSpace() * 2.0;
72  double columnTop = mSettings.boxSpace() + titleSize.height() + mSettings.style( QgsComposerLegendStyle::Title ).margin( QgsComposerLegendStyle::Bottom );
73 
74  QPointF point( mSettings.boxSpace(), columnTop );
75  bool firstInColumn = true;
76  double columnMaxHeight = 0;
77  qreal columnWidth = 0;
78  int column = 0;
79  foreach ( Atom atom, atomList )
80  {
81  if ( atom.column > column )
82  {
83  // Switch to next column
84  if ( mSettings.equalColumnWidth() )
85  {
86  point.rx() += mSettings.columnSpace() + maxColumnWidth;
87  }
88  else
89  {
90  point.rx() += mSettings.columnSpace() + columnWidth;
91  }
92  point.ry() = columnTop;
93  columnWidth = 0;
94  column++;
95  firstInColumn = true;
96  }
97  if ( !firstInColumn )
98  {
99  point.ry() += spaceAboveAtom( atom );
100  }
101 
102  QSizeF atomSize = drawAtom( atom, painter, point );
103  columnWidth = qMax( atomSize.width(), columnWidth );
104 
105  point.ry() += atom.size.height();
106  columnMaxHeight = qMax( point.y() - columnTop, columnMaxHeight );
107 
108  firstInColumn = false;
109  }
110  point.rx() += columnWidth + mSettings.boxSpace();
111 
112  size.rheight() = columnTop + columnMaxHeight + mSettings.boxSpace();
113  size.rwidth() = point.x();
114  if ( !mSettings.title().isEmpty() )
115  {
116  size.rwidth() = qMax( titleSize.width(), size.width() );
117  }
118 
119  // override the size if it was set by the user
120  if ( mLegendSize.isValid() )
121  {
122  qreal w = qMax( size.width(), mLegendSize.width() );
123  qreal h = qMax( size.height(), mLegendSize.height() );
124  size = QSizeF( w, h );
125  }
126 
127  // Now we have set the correct total item width and can draw the title centered
128  if ( !mSettings.title().isEmpty() )
129  {
130  if ( mSettings.titleAlignment() == Qt::AlignLeft )
131  {
132  point.rx() = mSettings.boxSpace();
133  }
134  else if ( mSettings.titleAlignment() == Qt::AlignHCenter )
135  {
136  point.rx() = size.width() / 2;
137  }
138  else
139  {
140  point.rx() = size.width() - mSettings.boxSpace();
141  }
142  point.ry() = mSettings.boxSpace();
143  drawTitle( painter, point, mSettings.titleAlignment(), size.width() );
144  }
145 
146  return size;
147 }
148 
149 
150 QList<QgsLegendRenderer::Atom> QgsLegendRenderer::createAtomList( QgsLayerTreeGroup* parentGroup, bool splitLayer )
151 {
152  QList<Atom> atoms;
153 
154  if ( !parentGroup ) return atoms;
155 
156  foreach ( QgsLayerTreeNode* node, parentGroup->children() )
157  {
158  if ( QgsLayerTree::isGroup( node ) )
159  {
160  QgsLayerTreeGroup* nodeGroup = QgsLayerTree::toGroup( node );
161 
162  // Group subitems
163  QList<Atom> groupAtoms = createAtomList( nodeGroup, splitLayer );
164 
165  if ( nodeLegendStyle( nodeGroup ) != QgsComposerLegendStyle::Hidden )
166  {
167  Nucleon nucleon;
168  nucleon.item = node;
169  nucleon.size = drawGroupTitle( nodeGroup );
170 
171  if ( groupAtoms.size() > 0 )
172  {
173  // Add internal space between this group title and the next nucleon
174  groupAtoms[0].size.rheight() += spaceAboveAtom( groupAtoms[0] );
175  // Prepend this group title to the first atom
176  groupAtoms[0].nucleons.prepend( nucleon );
177  groupAtoms[0].size.rheight() += nucleon.size.height();
178  groupAtoms[0].size.rwidth() = qMax( nucleon.size.width(), groupAtoms[0].size.width() );
179  }
180  else
181  {
182  // no subitems, append new atom
183  Atom atom;
184  atom.nucleons.append( nucleon );
185  atom.size.rwidth() += nucleon.size.width();
186  atom.size.rheight() += nucleon.size.height();
187  atom.size.rwidth() = qMax( nucleon.size.width(), atom.size.width() );
188  groupAtoms.append( atom );
189  }
190  }
191  atoms.append( groupAtoms );
192  }
193  else if ( QgsLayerTree::isLayer( node ) )
194  {
195  QgsLayerTreeLayer* nodeLayer = QgsLayerTree::toLayer( node );
196 
197  Atom atom;
198 
199  if ( nodeLegendStyle( nodeLayer ) != QgsComposerLegendStyle::Hidden )
200  {
201  Nucleon nucleon;
202  nucleon.item = node;
203  nucleon.size = drawLayerTitle( nodeLayer );
204  atom.nucleons.append( nucleon );
205  atom.size.rwidth() = nucleon.size.width();
206  atom.size.rheight() = nucleon.size.height();
207  }
208 
209  QList<QgsLayerTreeModelLegendNode*> legendNodes = mLegendModel->layerLegendNodes( nodeLayer );
210 
211  // workaround for the issue that "filtering by map" does not remove layer nodes that have no symbols present
212  // on the map. We explicitly skip such layers here. In future ideally that should be handled directly
213  // in the layer tree model
214  if ( legendNodes.isEmpty() && mLegendModel->legendFilterByMap() )
215  continue;
216 
217  QList<Atom> layerAtoms;
218 
219  for ( int j = 0; j < legendNodes.count(); j++ )
220  {
221  QgsLayerTreeModelLegendNode* legendNode = legendNodes.at( j );
222 
223  Nucleon symbolNucleon = drawSymbolItem( legendNode );
224 
225  if ( !mSettings.splitLayer() || j == 0 )
226  {
227  // append to layer atom
228  // the width is not correct at this moment, we must align all symbol labels
229  atom.size.rwidth() = qMax( symbolNucleon.size.width(), atom.size.width() );
230  // Add symbol space only if there is already title or another item above
231  if ( atom.nucleons.size() > 0 )
232  {
233  // TODO: for now we keep Symbol and SymbolLabel Top margin in sync
234  atom.size.rheight() += mSettings.style( QgsComposerLegendStyle::Symbol ).margin( QgsComposerLegendStyle::Top );
235  }
236  atom.size.rheight() += symbolNucleon.size.height();
237  atom.nucleons.append( symbolNucleon );
238  }
239  else
240  {
241  Atom symbolAtom;
242  symbolAtom.nucleons.append( symbolNucleon );
243  symbolAtom.size.rwidth() = symbolNucleon.size.width();
244  symbolAtom.size.rheight() = symbolNucleon.size.height();
245  layerAtoms.append( symbolAtom );
246  }
247  }
248  layerAtoms.prepend( atom );
249  atoms.append( layerAtoms );
250  }
251  }
252 
253  return atoms;
254 }
255 
256 
257 void QgsLegendRenderer::setColumns( QList<Atom>& atomList )
258 {
259  if ( mSettings.columnCount() == 0 ) return;
260 
261  // Divide atoms to columns
262  double totalHeight = 0;
263  // bool first = true;
264  qreal maxAtomHeight = 0;
265  foreach ( Atom atom, atomList )
266  {
267  //if ( !first )
268  //{
269  totalHeight += spaceAboveAtom( atom );
270  //}
271  totalHeight += atom.size.height();
272  maxAtomHeight = qMax( atom.size.height(), maxAtomHeight );
273  // first = false;
274  }
275 
276  // We know height of each atom and we have to split them into columns
277  // minimizing max column height. It is sort of bin packing problem, NP-hard.
278  // We are using simple heuristic, brute fore appeared to be to slow,
279  // the number of combinations is N = n!/(k!*(n-k)!) where n = atomsCount-1
280  // and k = columnsCount-1
281 
282  double avgColumnHeight = totalHeight / mSettings.columnCount();
283  int currentColumn = 0;
284  int currentColumnAtomCount = 0; // number of atoms in current column
285  double currentColumnHeight = 0;
286  double maxColumnHeight = 0;
287  double closedColumnsHeight = 0;
288  // first = true; // first in column
289  for ( int i = 0; i < atomList.size(); i++ )
290  {
291  Atom atom = atomList[i];
292  double currentHeight = currentColumnHeight;
293  //if ( !first )
294  //{
295  currentHeight += spaceAboveAtom( atom );
296  //}
297  currentHeight += atom.size.height();
298 
299  // Recalc average height for remaining columns including current
300  avgColumnHeight = ( totalHeight - closedColumnsHeight ) / ( mSettings.columnCount() - currentColumn );
301  if (( 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  && currentColumn < mSettings.columnCount() - 1 ) // must not exceed max number of columns
306  {
307  // New column
308  currentColumn++;
309  currentColumnAtomCount = 0;
310  closedColumnsHeight += currentColumnHeight;
311  currentColumnHeight = atom.size.height();
312  }
313  else
314  {
315  currentColumnHeight = currentHeight;
316  }
317  atomList[i].column = currentColumn;
318  currentColumnAtomCount++;
319  maxColumnHeight = qMax( currentColumnHeight, maxColumnHeight );
320 
321  // first = false;
322  }
323 
324  // Alling labels of symbols for each layr/column to the same labelXOffset
325  QMap<QString, qreal> maxSymbolWidth;
326  for ( int i = 0; i < atomList.size(); i++ )
327  {
328  Atom& atom = atomList[i];
329  for ( int j = 0; j < atom.nucleons.size(); j++ )
330  {
331  if ( QgsLayerTreeModelLegendNode* legendNode = qobject_cast<QgsLayerTreeModelLegendNode*>( atom.nucleons[j].item ) )
332  {
333  QString key = QString( "%1-%2" ).arg(( qulonglong )legendNode->layerNode() ).arg( atom.column );
334  maxSymbolWidth[key] = qMax( atom.nucleons[j].symbolSize.width(), maxSymbolWidth[key] );
335  }
336  }
337  }
338  for ( int i = 0; i < atomList.size(); i++ )
339  {
340  Atom& atom = atomList[i];
341  for ( int j = 0; j < atom.nucleons.size(); j++ )
342  {
343  if ( QgsLayerTreeModelLegendNode* legendNode = qobject_cast<QgsLayerTreeModelLegendNode*>( atom.nucleons[j].item ) )
344  {
345  QString key = QString( "%1-%2" ).arg(( qulonglong )legendNode->layerNode() ).arg( atom.column );
348  atom.nucleons[j].labelXOffset = maxSymbolWidth[key] + space;
349  atom.nucleons[j].size.rwidth() = maxSymbolWidth[key] + space + atom.nucleons[j].labelSize.width();
350  }
351  }
352  }
353 }
354 
355 
356 QSizeF QgsLegendRenderer::drawTitle( QPainter* painter, QPointF point, Qt::AlignmentFlag halignment, double legendWidth )
357 {
358  QSizeF size( 0, 0 );
359  if ( mSettings.title().isEmpty() )
360  {
361  return size;
362  }
363 
364  QStringList lines = mSettings.splitStringForWrapping( mSettings.title() );
365  double y = point.y();
366 
367  if ( painter )
368  {
369  painter->setPen( mSettings.fontColor() );
370  }
371 
372  //calculate width and left pos of rectangle to draw text into
373  double textBoxWidth;
374  double textBoxLeft;
375  switch ( halignment )
376  {
377  case Qt::AlignHCenter:
378  textBoxWidth = ( qMin(( double ) point.x(), legendWidth - point.x() ) - mSettings.boxSpace() ) * 2.0;
379  textBoxLeft = point.x() - textBoxWidth / 2.;
380  break;
381  case Qt::AlignRight:
382  textBoxLeft = mSettings.boxSpace();
383  textBoxWidth = point.x() - mSettings.boxSpace();
384  break;
385  case Qt::AlignLeft:
386  default:
387  textBoxLeft = point.x();
388  textBoxWidth = legendWidth - point.x() - mSettings.boxSpace();
389  break;
390  }
391 
392  QFont titleFont = mSettings.style( QgsComposerLegendStyle::Title ).font();
393 
394  for ( QStringList::Iterator titlePart = lines.begin(); titlePart != lines.end(); ++titlePart )
395  {
396  //last word is not drawn if rectangle width is exactly text width, so add 1
397  //TODO - correctly calculate size of italicized text, since QFontMetrics does not
398  qreal width = mSettings.textWidthMillimeters( titleFont, *titlePart ) + 1;
399  qreal height = mSettings.fontAscentMillimeters( titleFont ) + mSettings.fontDescentMillimeters( titleFont );
400 
401  QRectF r( textBoxLeft, y, textBoxWidth, height );
402 
403  if ( painter )
404  {
405  mSettings.drawText( painter, r, *titlePart, titleFont, halignment, Qt::AlignVCenter, Qt::TextDontClip );
406  }
407 
408  //update max width of title
409  size.rwidth() = qMax( width, size.rwidth() );
410 
411  y += height;
412  if ( titlePart != lines.end() )
413  {
414  y += mSettings.lineSpacing();
415  }
416  }
417  size.rheight() = y - point.y();
418 
419  return size;
420 }
421 
422 
423 double QgsLegendRenderer::spaceAboveAtom( Atom atom )
424 {
425  if ( atom.nucleons.size() == 0 ) return 0;
426 
427  Nucleon nucleon = atom.nucleons.first();
428 
429  if ( QgsLayerTreeGroup* nodeGroup = qobject_cast<QgsLayerTreeGroup*>( nucleon.item ) )
430  {
431  return mSettings.style( nodeLegendStyle( nodeGroup ) ).margin( QgsComposerLegendStyle::Top );
432  }
433  else if ( QgsLayerTreeLayer* nodeLayer = qobject_cast<QgsLayerTreeLayer*>( nucleon.item ) )
434  {
435  return mSettings.style( nodeLegendStyle( nodeLayer ) ).margin( QgsComposerLegendStyle::Top );
436  }
437  else if ( qobject_cast<QgsLayerTreeModelLegendNode*>( nucleon.item ) )
438  {
439  // TODO: use Symbol or SymbolLabel Top margin
441  }
442 
443  return 0;
444 }
445 
446 
447 // Draw atom and expand its size (using actual nucleons labelXOffset)
448 QSizeF QgsLegendRenderer::drawAtom( Atom atom, QPainter* painter, QPointF point )
449 {
450  bool first = true;
451  QSizeF size = QSizeF( atom.size );
452  foreach ( Nucleon nucleon, atom.nucleons )
453  {
454  if ( QgsLayerTreeGroup* groupItem = qobject_cast<QgsLayerTreeGroup*>( nucleon.item ) )
455  {
458  {
459  if ( !first )
460  {
461  point.ry() += mSettings.style( s ).margin( QgsComposerLegendStyle::Top );
462  }
463  drawGroupTitle( groupItem, painter, point );
464  }
465  }
466  else if ( QgsLayerTreeLayer* layerItem = qobject_cast<QgsLayerTreeLayer*>( nucleon.item ) )
467  {
470  {
471  if ( !first )
472  {
473  point.ry() += mSettings.style( s ).margin( QgsComposerLegendStyle::Top );
474  }
475  drawLayerTitle( layerItem, painter, point );
476  }
477  }
478  else if ( QgsLayerTreeModelLegendNode* legendNode = qobject_cast<QgsLayerTreeModelLegendNode*>( nucleon.item ) )
479  {
480  if ( !first )
481  {
483  }
484 
485  Nucleon symbolNucleon = drawSymbolItem( legendNode, painter, point, nucleon.labelXOffset );
486  // expand width, it may be wider because of labelXOffset
487  size.rwidth() = qMax( symbolNucleon.size.width(), size.width() );
488  }
489  point.ry() += nucleon.size.height();
490  first = false;
491  }
492  return size;
493 }
494 
495 
496 QgsLegendRenderer::Nucleon QgsLegendRenderer::drawSymbolItem( QgsLayerTreeModelLegendNode* symbolItem, QPainter* painter, QPointF point, double labelXOffset )
497 {
499  ctx.painter = painter;
500  ctx.point = point;
501  ctx.labelXOffset = labelXOffset;
502 
503  QgsLayerTreeModelLegendNode::ItemMetrics im = symbolItem->draw( mSettings, painter ? &ctx : 0 );
504 
505  Nucleon nucleon;
506  nucleon.item = symbolItem;
507  nucleon.symbolSize = im.symbolSize;
508  nucleon.labelSize = im.labelSize;
509  //QgsDebugMsg( QString( "symbol height = %1 label height = %2").arg( symbolSize.height()).arg( labelSize.height() ));
510  double width = qMax(( double ) im.symbolSize.width(), labelXOffset ) + im.labelSize.width();
511  double height = qMax( im.symbolSize.height(), im.labelSize.height() );
512  nucleon.size = QSizeF( width, height );
513  return nucleon;
514 }
515 
516 
517 QSizeF QgsLegendRenderer::drawLayerTitle( QgsLayerTreeLayer* nodeLayer, QPainter* painter, QPointF point )
518 {
519  QSizeF size( 0, 0 );
520  QModelIndex idx = mLegendModel->node2index( nodeLayer );
521 
522  //Let the user omit the layer title item by having an empty layer title string
523  if ( mLegendModel->data( idx, Qt::DisplayRole ).toString().isEmpty() ) return size;
524 
525  double y = point.y();
526 
527  if ( painter ) painter->setPen( mSettings.fontColor() );
528 
529  QFont layerFont = mSettings.style( nodeLegendStyle( nodeLayer ) ).font();
530 
531  QStringList lines = mSettings.splitStringForWrapping( mLegendModel->data( idx, Qt::DisplayRole ).toString() );
532  for ( QStringList::Iterator layerItemPart = lines.begin(); layerItemPart != lines.end(); ++layerItemPart )
533  {
534  y += mSettings.fontAscentMillimeters( layerFont );
535  if ( painter ) mSettings.drawText( painter, point.x(), y, *layerItemPart, layerFont );
536  qreal width = mSettings.textWidthMillimeters( layerFont, *layerItemPart );
537  size.rwidth() = qMax( width, size.width() );
538  if ( layerItemPart != lines.end() )
539  {
540  y += mSettings.lineSpacing();
541  }
542  }
543  size.rheight() = y - point.y();
544 
545  return size;
546 }
547 
548 
549 QSizeF QgsLegendRenderer::drawGroupTitle( QgsLayerTreeGroup* nodeGroup, QPainter* painter, QPointF point )
550 {
551  QSizeF size( 0, 0 );
552  QModelIndex idx = mLegendModel->node2index( nodeGroup );
553 
554  double y = point.y();
555 
556  if ( painter ) painter->setPen( mSettings.fontColor() );
557 
558  QFont groupFont = mSettings.style( nodeLegendStyle( nodeGroup ) ).font();
559 
560  QStringList lines = mSettings.splitStringForWrapping( mLegendModel->data( idx, Qt::DisplayRole ).toString() );
561  for ( QStringList::Iterator groupPart = lines.begin(); groupPart != lines.end(); ++groupPart )
562  {
563  y += mSettings.fontAscentMillimeters( groupFont );
564  if ( painter ) mSettings.drawText( painter, point.x(), y, *groupPart, groupFont );
565  qreal width = mSettings.textWidthMillimeters( groupFont, *groupPart );
566  size.rwidth() = qMax( width, size.width() );
567  if ( groupPart != lines.end() )
568  {
569  y += mSettings.lineSpacing();
570  }
571  }
572  size.rheight() = y - point.y();
573  return size;
574 }
575 
576 
577 
579 {
580  QString style = node->customProperty( "legend/title-style" ).toString();
581  if ( style == "hidden" )
583  else if ( style == "group" )
585  else if ( style == "subgroup" )
587 
588  // use a default otherwise
589  if ( QgsLayerTree::isGroup( node ) )
591  else if ( QgsLayerTree::isLayer( node ) )
592  {
593  QList<QgsLayerTreeModelLegendNode*> legendNodes = model->layerLegendNodes( QgsLayerTree::toLayer( node ) );
594  if ( legendNodes.count() == 1 && legendNodes[0]->isEmbeddedInParent() )
597  }
598 
599  return QgsComposerLegendStyle::Undefined; // should not happen, only if corrupted project file
600 }
601 
603 {
604  return nodeLegendStyle( node, mLegendModel );
605 }
606 
608 {
609  QString str;
610  switch ( style )
611  {
612  case QgsComposerLegendStyle::Hidden: str = "hidden"; break;
613  case QgsComposerLegendStyle::Group: str = "group"; break;
614  case QgsComposerLegendStyle::Subgroup: str = "subgroup"; break;
615  default: break; // nothing
616  }
617 
618  if ( !str.isEmpty() )
619  node->setCustomProperty( "legend/title-style", str );
620  else
621  node->removeCustomProperty( "legend/title-style" );
622 }