QGIS API Documentation  3.10.0-A Coruña (6c816b4204)
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 "qgslayertree.h"
19 #include "qgslayertreemodel.h"
21 #include "qgslegendstyle.h"
22 #include "qgsmaplayerlegend.h"
23 #include "qgssymbol.h"
24 #include "qgsrendercontext.h"
25 #include "qgsvectorlayer.h"
27 
28 #include <QJsonObject>
29 #include <QPainter>
30 
31 
32 
34  : mLegendModel( legendModel )
35  , mSettings( settings )
36 {
37 }
38 
40 {
41  return paintAndDetermineSize( renderContext );
42 }
43 
44 void QgsLegendRenderer::drawLegend( QPainter *painter )
45 {
46  paintAndDetermineSize( painter );
47 }
48 
49 void QgsLegendRenderer::exportLegendToJson( const QgsRenderContext &context, QJsonObject &json )
50 {
51  QgsLayerTreeGroup *rootGroup = mLegendModel->rootGroup();
52  if ( !rootGroup )
53  return;
54 
55  json[QStringLiteral( "title" )] = mSettings.title();
56  exportLegendToJson( context, rootGroup, json );
57 }
58 
59 void QgsLegendRenderer::exportLegendToJson( const QgsRenderContext &context, QgsLayerTreeGroup *nodeGroup, QJsonObject &json )
60 {
61  QJsonArray nodes;
62  const QList<QgsLayerTreeNode *> childNodes = nodeGroup->children();
63  for ( QgsLayerTreeNode *node : childNodes )
64  {
65  if ( QgsLayerTree::isGroup( node ) )
66  {
67  QgsLayerTreeGroup *nodeGroup = QgsLayerTree::toGroup( node );
68  const QModelIndex idx = mLegendModel->node2index( nodeGroup );
69  const QString text = mLegendModel->data( idx, Qt::DisplayRole ).toString();
70 
71  QJsonObject group;
72  group[ QStringLiteral( "type" ) ] = QStringLiteral( "group" );
73  group[ QStringLiteral( "title" ) ] = text;
74  exportLegendToJson( context, nodeGroup, group );
75  nodes.append( group );
76  }
77  else if ( QgsLayerTree::isLayer( node ) )
78  {
79  QJsonObject group;
80  group[ QStringLiteral( "type" ) ] = QStringLiteral( "layer" );
81 
82  QgsLayerTreeLayer *nodeLayer = QgsLayerTree::toLayer( node );
83 
84  QString text;
85  if ( nodeLegendStyle( nodeLayer ) != QgsLegendStyle::Hidden )
86  {
87  const QModelIndex idx = mLegendModel->node2index( nodeLayer );
88  text = mLegendModel->data( idx, Qt::DisplayRole ).toString();
89  }
90 
91  QList<QgsLayerTreeModelLegendNode *> legendNodes = mLegendModel->layerLegendNodes( nodeLayer );
92 
93  if ( legendNodes.isEmpty() && mLegendModel->legendFilterMapSettings() )
94  continue;
95 
96  if ( legendNodes.count() == 1 )
97  {
98  legendNodes.at( 0 )->exportToJson( mSettings, context, group );
99  nodes.append( group );
100  }
101  else if ( legendNodes.count() > 1 )
102  {
103  QJsonArray symbols;
104  for ( int j = 0; j < legendNodes.count(); j++ )
105  {
106  QgsLayerTreeModelLegendNode *legendNode = legendNodes.at( j );
107  QJsonObject symbol;
108  legendNode->exportToJson( mSettings, context, symbol );
109  symbols.append( symbol );
110  }
111  group[ QStringLiteral( "title" ) ] = text;
112  group[ QStringLiteral( "symbols" ) ] = symbols;
113  nodes.append( group );
114  }
115  }
116  }
117 
118  json[QStringLiteral( "nodes" )] = nodes;
119 }
120 
121 QSizeF QgsLegendRenderer::paintAndDetermineSize( QPainter *painter )
122 {
123  return paintAndDetermineSizeInternal( nullptr, painter );
124 }
125 
126 QSizeF QgsLegendRenderer::paintAndDetermineSizeInternal( QgsRenderContext *context, QPainter *painter )
127 {
128  QSizeF size( 0, 0 );
129  QgsLayerTreeGroup *rootGroup = mLegendModel->rootGroup();
130  if ( !rootGroup )
131  return size;
132 
133  // temporarily remove painter from context -- we don't need to actually draw anything yet. But we DO need
134  // to send the full render context so that an expression context is available during the size calculation
135  QPainter *prevPainter = context ? context->painter() : nullptr;
136  if ( context )
137  context->setPainter( nullptr );
138 
139  QList<LegendComponentGroup> componentGroups = createComponentGroupList( rootGroup, mSettings.splitLayer(), context );
140 
141  setColumns( componentGroups );
142 
143  QMap< int, double > maxColumnWidths;
144  qreal maxEqualColumnWidth = 0;
145  // another iteration -- this one is required to calculate the maximum item width for each
146  // column. Unfortunately, we can't trust the component group widths at this stage, as they are minimal widths
147  // only. When actually rendering a symbol node, the text is aligned according to the WIDEST
148  // symbol in a column. So that means we can't possibly determine the exact size of legend components
149  // until now. BUUUUUUUUUUUUT. Because everything sucks, we can't even start the actual render of items
150  // at the same time we calculate this -- legend items REQUIRE the REAL width of the columns in order to
151  // correctly align right or center-aligned symbols/text. Bah -- A triple iteration it is!
152  for ( const LegendComponentGroup &group : qgis::as_const( componentGroups ) )
153  {
154  const QSizeF actualSize = drawGroup( group, context, ColumnContext() );
155  maxEqualColumnWidth = std::max( actualSize.width(), maxEqualColumnWidth );
156  maxColumnWidths[ group.column ] = std::max( actualSize.width(), maxColumnWidths.value( group.column, 0 ) );
157  }
158  if ( context )
159  context->setPainter( prevPainter );
160 
161  if ( mSettings.columnCount() < 2 )
162  {
163  // single column - use the full available width
164  maxEqualColumnWidth = std::max( maxEqualColumnWidth, mLegendSize.width() - 2 * mSettings.boxSpace() );
165  maxColumnWidths[ 0 ] = maxEqualColumnWidth;
166  }
167 
168  //calculate size of title
169  QSizeF titleSize = drawTitle();
170  //add title margin to size of title text
171  titleSize.rwidth() += mSettings.boxSpace() * 2.0;
172  double columnTop = mSettings.boxSpace() + titleSize.height() + mSettings.style( QgsLegendStyle::Title ).margin( QgsLegendStyle::Bottom );
173 
174  bool firstInColumn = true;
175  double columnMaxHeight = 0;
176  qreal columnWidth = 0;
177  int column = -1;
178  ColumnContext columnContext;
179  columnContext.left = mSettings.boxSpace();
180  columnContext.right = std::max( mLegendSize.width() - mSettings.boxSpace(), mSettings.boxSpace() );
181  double currentY = columnTop;
182 
183  for ( const LegendComponentGroup &group : qgis::as_const( componentGroups ) )
184  {
185  if ( group.column > column )
186  {
187  // Switch to next column
188  columnContext.left = group.column > 0 ? columnContext.right + mSettings.columnSpace() : mSettings.boxSpace();
189  columnWidth = mSettings.equalColumnWidth() ? maxEqualColumnWidth : maxColumnWidths.value( group.column );
190  columnContext.right = columnContext.left + columnWidth;
191  currentY = columnTop;
192  column++;
193  firstInColumn = true;
194  }
195  if ( !firstInColumn )
196  {
197  currentY += spaceAboveGroup( group );
198  }
199 
200  if ( context )
201  drawGroup( group, context, columnContext, currentY );
202  else if ( painter )
203  drawGroup( group, columnContext, painter, currentY );
204 
205  currentY += group.size.height();
206  columnMaxHeight = std::max( currentY - columnTop, columnMaxHeight );
207 
208  firstInColumn = false;
209  }
210  const double totalWidth = columnContext.right + mSettings.boxSpace();
211 
212  size.rheight() = columnTop + columnMaxHeight + mSettings.boxSpace();
213  size.rwidth() = totalWidth;
214  if ( !mSettings.title().isEmpty() )
215  {
216  size.rwidth() = std::max( titleSize.width(), size.width() );
217  }
218 
219  // override the size if it was set by the user
220  if ( mLegendSize.isValid() )
221  {
222  qreal w = std::max( size.width(), mLegendSize.width() );
223  qreal h = std::max( size.height(), mLegendSize.height() );
224  size = QSizeF( w, h );
225  }
226 
227  // Now we have set the correct total item width and can draw the title centered
228  if ( !mSettings.title().isEmpty() )
229  {
230  if ( context )
231  drawTitle( context, mSettings.boxSpace(), mSettings.titleAlignment(), size.width() );
232  else
233  drawTitle( painter, mSettings.boxSpace(), mSettings.titleAlignment(), size.width() );
234  }
235 
236  return size;
237 }
238 
239 void QgsLegendRenderer::widthAndOffsetForTitleText( const Qt::AlignmentFlag halignment, const double legendWidth, double &textBoxWidth, double &textBoxLeft )
240 {
241  switch ( halignment )
242  {
243  default:
244  textBoxLeft = mSettings.boxSpace();
245  textBoxWidth = legendWidth - 2 * mSettings.boxSpace();
246  break;
247 
248  case Qt::AlignHCenter:
249  {
250  // not sure on this logic, I just moved it -- don't blame me for it being totally obscure!
251  const double centerX = legendWidth / 2;
252  textBoxWidth = ( std::min( static_cast< double >( centerX ), legendWidth - centerX ) - mSettings.boxSpace() ) * 2.0;
253  textBoxLeft = centerX - textBoxWidth / 2.;
254  break;
255  }
256  }
257 }
258 
259 QList<QgsLegendRenderer::LegendComponentGroup> QgsLegendRenderer::createComponentGroupList( QgsLayerTreeGroup *parentGroup, bool splitLayer, QgsRenderContext *context )
260 {
261  QList<LegendComponentGroup> componentGroups;
262 
263  if ( !parentGroup )
264  return componentGroups;
265 
266  const QList<QgsLayerTreeNode *> childNodes = parentGroup->children();
267  for ( QgsLayerTreeNode *node : childNodes )
268  {
269  if ( QgsLayerTree::isGroup( node ) )
270  {
271  QgsLayerTreeGroup *nodeGroup = QgsLayerTree::toGroup( node );
272 
273  // Group subitems
274  QList<LegendComponentGroup> subgroups = createComponentGroupList( nodeGroup, splitLayer, context );
275  bool hasSubItems = !subgroups.empty();
276 
277  if ( nodeLegendStyle( nodeGroup ) != QgsLegendStyle::Hidden )
278  {
279  LegendComponent component;
280  component.item = node;
281  component.size = drawGroupTitle( nodeGroup );
282 
283  if ( !subgroups.isEmpty() )
284  {
285  // Add internal space between this group title and the next component
286  subgroups[0].size.rheight() += spaceAboveGroup( subgroups[0] );
287  // Prepend this group title to the first group
288  subgroups[0].components.prepend( component );
289  subgroups[0].size.rheight() += component.size.height();
290  subgroups[0].size.rwidth() = std::max( component.size.width(), subgroups[0].size.width() );
291  }
292  else
293  {
294  // no subitems, create new group
295  LegendComponentGroup group;
296  group.components.append( component );
297  group.size.rwidth() += component.size.width();
298  group.size.rheight() += component.size.height();
299  group.size.rwidth() = std::max( component.size.width(), group.size.width() );
300  subgroups.append( group );
301  }
302  }
303 
304  if ( hasSubItems ) //leave away groups without content
305  {
306  componentGroups.append( subgroups );
307  }
308 
309  }
310  else if ( QgsLayerTree::isLayer( node ) )
311  {
312  QgsLayerTreeLayer *nodeLayer = QgsLayerTree::toLayer( node );
313 
314  LegendComponentGroup group;
315 
316  if ( nodeLegendStyle( nodeLayer ) != QgsLegendStyle::Hidden )
317  {
318  LegendComponent component;
319  component.item = node;
320  component.size = drawLayerTitle( nodeLayer );
321  group.components.append( component );
322  group.size.rwidth() = component.size.width();
323  group.size.rheight() = component.size.height();
324  }
325 
326  QList<QgsLayerTreeModelLegendNode *> legendNodes = mLegendModel->layerLegendNodes( nodeLayer );
327 
328  // workaround for the issue that "filtering by map" does not remove layer nodes that have no symbols present
329  // on the map. We explicitly skip such layers here. In future ideally that should be handled directly
330  // in the layer tree model
331  if ( legendNodes.isEmpty() && mLegendModel->legendFilterMapSettings() )
332  continue;
333 
334  QList<LegendComponentGroup> layerGroups;
335  layerGroups.reserve( legendNodes.count() );
336 
337  for ( int j = 0; j < legendNodes.count(); j++ )
338  {
339  QgsLayerTreeModelLegendNode *legendNode = legendNodes.at( j );
340 
341  LegendComponent symbolComponent = drawSymbolItem( legendNode, context, ColumnContext(), 0 );
342 
343  if ( !mSettings.splitLayer() || j == 0 )
344  {
345  // append to layer group
346  // the width is not correct at this moment, we must align all symbol labels
347  group.size.rwidth() = std::max( symbolComponent.size.width(), group.size.width() );
348  // Add symbol space only if there is already title or another item above
349  if ( !group.components.isEmpty() )
350  {
351  // TODO: for now we keep Symbol and SymbolLabel Top margin in sync
352  group.size.rheight() += mSettings.style( QgsLegendStyle::Symbol ).margin( QgsLegendStyle::Top );
353  }
354  group.size.rheight() += symbolComponent.size.height();
355  group.components.append( symbolComponent );
356  }
357  else
358  {
359  LegendComponentGroup symbolGroup;
360  symbolGroup.components.append( symbolComponent );
361  symbolGroup.size.rwidth() = symbolComponent.size.width();
362  symbolGroup.size.rheight() = symbolComponent.size.height();
363  layerGroups.append( symbolGroup );
364  }
365  }
366  layerGroups.prepend( group );
367  componentGroups.append( layerGroups );
368  }
369  }
370 
371  return componentGroups;
372 }
373 
374 
375 void QgsLegendRenderer::setColumns( QList<LegendComponentGroup> &componentGroups )
376 {
377  if ( mSettings.columnCount() == 0 )
378  return;
379 
380  // Divide groups to columns
381  double totalHeight = 0;
382  qreal maxGroupHeight = 0;
383  for ( const LegendComponentGroup &group : qgis::as_const( componentGroups ) )
384  {
385  totalHeight += spaceAboveGroup( group );
386  totalHeight += group.size.height();
387  maxGroupHeight = std::max( group.size.height(), maxGroupHeight );
388  }
389 
390  // We know height of each group and we have to split them into columns
391  // minimizing max column height. It is sort of bin packing problem, NP-hard.
392  // We are using simple heuristic, brute fore appeared to be to slow,
393  // the number of combinations is N = n!/(k!*(n-k)!) where n = groupCount-1
394  // and k = columnsCount-1
395  double maxColumnHeight = 0;
396  int currentColumn = 0;
397  int currentColumnGroupCount = 0; // number of groups in current column
398  double currentColumnHeight = 0;
399  double closedColumnsHeight = 0;
400 
401  for ( int i = 0; i < componentGroups.size(); i++ )
402  {
403  // Recalc average height for remaining columns including current
404  double avgColumnHeight = ( totalHeight - closedColumnsHeight ) / ( mSettings.columnCount() - currentColumn );
405 
406  LegendComponentGroup group = componentGroups.at( i );
407  double currentHeight = currentColumnHeight;
408  if ( currentColumnGroupCount > 0 )
409  currentHeight += spaceAboveGroup( group );
410  currentHeight += group.size.height();
411 
412  bool canCreateNewColumn = ( currentColumnGroupCount > 0 ) // do not leave empty column
413  && ( currentColumn < mSettings.columnCount() - 1 ); // must not exceed max number of columns
414 
415  bool shouldCreateNewColumn = ( currentHeight - avgColumnHeight ) > group.size.height() / 2 // center of current group is over average height
416  && currentColumnGroupCount > 0 // do not leave empty column
417  && currentHeight > maxGroupHeight // no sense to make smaller columns than max group height
418  && currentHeight > maxColumnHeight; // no sense to make smaller columns than max column already created
419 
420  // also should create a new column if the number of items left < number of columns left
421  // in this case we should spread the remaining items out over the remaining columns
422  shouldCreateNewColumn |= ( componentGroups.size() - i < mSettings.columnCount() - currentColumn );
423 
424  if ( canCreateNewColumn && shouldCreateNewColumn )
425  {
426  // New column
427  currentColumn++;
428  currentColumnGroupCount = 0;
429  closedColumnsHeight += currentColumnHeight;
430  currentColumnHeight = group.size.height();
431  }
432  else
433  {
434  currentColumnHeight = currentHeight;
435  }
436  componentGroups[i].column = currentColumn;
437  currentColumnGroupCount++;
438  maxColumnHeight = std::max( currentColumnHeight, maxColumnHeight );
439  }
440 
441  // Align labels of symbols for each layr/column to the same labelXOffset
442  QMap<QString, qreal> maxSymbolWidth;
443  for ( int i = 0; i < componentGroups.size(); i++ )
444  {
445  LegendComponentGroup &group = componentGroups[i];
446  for ( int j = 0; j < group.components.size(); j++ )
447  {
448  if ( QgsLayerTreeModelLegendNode *legendNode = qobject_cast<QgsLayerTreeModelLegendNode *>( group.components.at( j ).item ) )
449  {
450  QString key = QStringLiteral( "%1-%2" ).arg( reinterpret_cast< qulonglong >( legendNode->layerNode() ) ).arg( group.column );
451  maxSymbolWidth[key] = std::max( group.components.at( j ).symbolSize.width(), maxSymbolWidth[key] );
452  }
453  }
454  }
455  for ( int i = 0; i < componentGroups.size(); i++ )
456  {
457  LegendComponentGroup &group = componentGroups[i];
458  for ( int j = 0; j < group.components.size(); j++ )
459  {
460  if ( QgsLayerTreeModelLegendNode *legendNode = qobject_cast<QgsLayerTreeModelLegendNode *>( group.components.at( j ).item ) )
461  {
462  QString key = QStringLiteral( "%1-%2" ).arg( reinterpret_cast< qulonglong >( legendNode->layerNode() ) ).arg( group.column );
463  double space = mSettings.style( QgsLegendStyle::Symbol ).margin( QgsLegendStyle::Right ) +
465  group.components[j].labelXOffset = maxSymbolWidth[key] + space;
466  group.components[j].maxSiblingSymbolWidth = maxSymbolWidth[key];
467  group.components[j].size.rwidth() = maxSymbolWidth[key] + space + group.components.at( j ).labelSize.width();
468  }
469  }
470  }
471 }
472 
473 QSizeF QgsLegendRenderer::drawTitle( QPainter *painter, double top, Qt::AlignmentFlag halignment, double legendWidth )
474 {
475  return drawTitleInternal( nullptr, painter, top, halignment, legendWidth );
476 }
477 
478 QSizeF QgsLegendRenderer::drawTitleInternal( QgsRenderContext *context, QPainter *painter, const double top, Qt::AlignmentFlag halignment, double legendWidth )
479 {
480  QSizeF size( 0, 0 );
481  if ( mSettings.title().isEmpty() )
482  {
483  return size;
484  }
485 
486  QStringList lines = mSettings.splitStringForWrapping( mSettings.title() );
487  double y = top;
488 
489  if ( context && context->painter() )
490  {
491  context->painter()->setPen( mSettings.fontColor() );
492  }
493  else if ( painter )
494  {
495  painter->setPen( mSettings.fontColor() );
496  }
497 
498  //calculate width and left pos of rectangle to draw text into
499  double textBoxWidth;
500  double textBoxLeft;
501  widthAndOffsetForTitleText( halignment, legendWidth, textBoxWidth, textBoxLeft );
502 
503  QFont titleFont = mSettings.style( QgsLegendStyle::Title ).font();
504 
505  for ( QStringList::Iterator titlePart = lines.begin(); titlePart != lines.end(); ++titlePart )
506  {
507  //last word is not drawn if rectangle width is exactly text width, so add 1
508  //TODO - correctly calculate size of italicized text, since QFontMetrics does not
509  qreal width = mSettings.textWidthMillimeters( titleFont, *titlePart ) + 1;
510  qreal height = mSettings.fontAscentMillimeters( titleFont ) + mSettings.fontDescentMillimeters( titleFont );
511 
512  QRectF r( textBoxLeft, y, textBoxWidth, height );
513 
514  if ( context && context->painter() )
515  {
516  mSettings.drawText( context->painter(), r, *titlePart, titleFont, halignment, Qt::AlignVCenter, Qt::TextDontClip );
517  }
518  else if ( painter )
519  {
520  mSettings.drawText( painter, r, *titlePart, titleFont, halignment, Qt::AlignVCenter, Qt::TextDontClip );
521  }
522 
523  //update max width of title
524  size.rwidth() = std::max( width, size.rwidth() );
525 
526  y += height;
527  if ( titlePart != ( lines.end() - 1 ) )
528  {
529  y += mSettings.lineSpacing();
530  }
531  }
532  size.rheight() = y - top;
533 
534  return size;
535 }
536 
537 
538 double QgsLegendRenderer::spaceAboveGroup( const LegendComponentGroup &group )
539 {
540  if ( group.components.isEmpty() ) return 0;
541 
542  LegendComponent component = group.components.first();
543 
544  if ( QgsLayerTreeGroup *nodeGroup = qobject_cast<QgsLayerTreeGroup *>( component.item ) )
545  {
546  return mSettings.style( nodeLegendStyle( nodeGroup ) ).margin( QgsLegendStyle::Top );
547  }
548  else if ( QgsLayerTreeLayer *nodeLayer = qobject_cast<QgsLayerTreeLayer *>( component.item ) )
549  {
550  return mSettings.style( nodeLegendStyle( nodeLayer ) ).margin( QgsLegendStyle::Top );
551  }
552  else if ( qobject_cast<QgsLayerTreeModelLegendNode *>( component.item ) )
553  {
554  // TODO: use Symbol or SymbolLabel Top margin
556  }
557 
558  return 0;
559 }
560 
561 QSizeF QgsLegendRenderer::drawGroup( const LegendComponentGroup &group, ColumnContext columnContext, QPainter *painter, double top )
562 {
563  return drawGroupInternal( group, nullptr, columnContext, painter, top );
564 }
565 
566 QSizeF QgsLegendRenderer::drawGroupInternal( const LegendComponentGroup &group, QgsRenderContext *context, ColumnContext columnContext, QPainter *painter, const double top )
567 {
568  bool first = true;
569  QSizeF size = QSizeF( group.size );
570  double currentY = top;
571  for ( const LegendComponent &component : qgis::as_const( group.components ) )
572  {
573  if ( QgsLayerTreeGroup *groupItem = qobject_cast<QgsLayerTreeGroup *>( component.item ) )
574  {
575  QgsLegendStyle::Style s = nodeLegendStyle( groupItem );
576  if ( s != QgsLegendStyle::Hidden )
577  {
578  if ( !first )
579  {
580  currentY += mSettings.style( s ).margin( QgsLegendStyle::Top );
581  }
582  QSizeF groupSize;
583  if ( context )
584  groupSize = drawGroupTitle( groupItem, context, columnContext, currentY );
585  else
586  groupSize = drawGroupTitle( groupItem, columnContext, painter, currentY );
587  size.rwidth() = std::max( groupSize.width(), size.width() );
588  }
589  }
590  else if ( QgsLayerTreeLayer *layerItem = qobject_cast<QgsLayerTreeLayer *>( component.item ) )
591  {
592  QgsLegendStyle::Style s = nodeLegendStyle( layerItem );
593  if ( s != QgsLegendStyle::Hidden )
594  {
595  if ( !first )
596  {
597  currentY += mSettings.style( s ).margin( QgsLegendStyle::Top );
598  }
599  QSizeF subGroupSize;
600  if ( context )
601  subGroupSize = drawLayerTitle( layerItem, context, columnContext, currentY );
602  else
603  subGroupSize = drawLayerTitle( layerItem, columnContext, painter, currentY );
604  size.rwidth() = std::max( subGroupSize.width(), size.width() );
605  }
606  }
607  else if ( QgsLayerTreeModelLegendNode *legendNode = qobject_cast<QgsLayerTreeModelLegendNode *>( component.item ) )
608  {
609  if ( !first )
610  {
611  currentY += mSettings.style( QgsLegendStyle::Symbol ).margin( QgsLegendStyle::Top );
612  }
613 
614  LegendComponent symbolComponent = context ? drawSymbolItem( legendNode, context, columnContext, currentY, component.maxSiblingSymbolWidth )
615  : drawSymbolItem( legendNode, columnContext, painter, currentY, component.maxSiblingSymbolWidth );
616  // expand width, it may be wider because of label offsets
617  size.rwidth() = std::max( symbolComponent.size.width(), size.width() );
618  }
619  currentY += component.size.height();
620  first = false;
621  }
622  return size;
623 }
624 
625 QgsLegendRenderer::LegendComponent QgsLegendRenderer::drawSymbolItem( QgsLayerTreeModelLegendNode *symbolItem, ColumnContext columnContext, QPainter *painter, double top, double maxSiblingSymbolWidth )
626 {
627  return drawSymbolItemInternal( symbolItem, columnContext, nullptr, painter, top, maxSiblingSymbolWidth );
628 }
629 
630 QgsLegendRenderer::LegendComponent QgsLegendRenderer::drawSymbolItemInternal( QgsLayerTreeModelLegendNode *symbolItem, ColumnContext columnContext, QgsRenderContext *context, QPainter *painter, double top, double maxSiblingSymbolWidth )
631 {
633  ctx.context = context;
634 
635  // add a layer expression context scope
636  QgsExpressionContextScope *layerScope = nullptr;
637  if ( context && symbolItem->layerNode()->layer() )
638  {
639  layerScope = QgsExpressionContextUtils::layerScope( symbolItem->layerNode()->layer() );
640  context->expressionContext().appendScope( layerScope );
641  }
642 
643  ctx.painter = context ? context->painter() : painter;
645  ctx.point = QPointF( columnContext.left, top );
646  ctx.labelXOffset = maxSiblingSymbolWidth;
648 
649  ctx.top = top;
650  ctx.columnLeft = columnContext.left;
651  ctx.columnRight = columnContext.right;
652  ctx.maxSiblingSymbolWidth = maxSiblingSymbolWidth;
653 
654  QgsLayerTreeModelLegendNode::ItemMetrics im = symbolItem->draw( mSettings, context ? &ctx
655  : ( painter ? &ctx : nullptr ) );
656 
657  if ( layerScope )
658  delete context->expressionContext().popScope();
659 
660  LegendComponent component;
661  component.item = symbolItem;
662  component.symbolSize = im.symbolSize;
663  component.labelSize = im.labelSize;
664  //QgsDebugMsg( QStringLiteral( "symbol height = %1 label height = %2").arg( symbolSize.height()).arg( labelSize.height() ));
665  // NOTE -- we hard code left/right margins below, because those are the only ones exposed for use currently.
666  // ideally we could (should?) expose all these margins as settings, and then adapt the below to respect the current symbol/text alignment
667  // and consider the correct margin sides...
668  double width = std::max( static_cast< double >( im.symbolSize.width() ), maxSiblingSymbolWidth )
671  + im.labelSize.width();
672 
673  double height = std::max( im.symbolSize.height(), im.labelSize.height() );
674  component.size = QSizeF( width, height );
675  return component;
676 }
677 
678 QSizeF QgsLegendRenderer::drawLayerTitle( QgsLayerTreeLayer *nodeLayer, ColumnContext columnContext, QPainter *painter, double top )
679 {
680  return drawLayerTitleInternal( nodeLayer, columnContext, nullptr, painter, top );
681 }
682 
683 QSizeF QgsLegendRenderer::drawLayerTitleInternal( QgsLayerTreeLayer *nodeLayer, ColumnContext columnContext, QgsRenderContext *context, QPainter *painter, const double top )
684 {
685  QSizeF size( 0, 0 );
686  QModelIndex idx = mLegendModel->node2index( nodeLayer );
687 
688  //Let the user omit the layer title item by having an empty layer title string
689  if ( mLegendModel->data( idx, Qt::DisplayRole ).toString().isEmpty() )
690  return size;
691 
692  double y = top;
693 
694  if ( context && context->painter() )
695  context->painter()->setPen( mSettings.layerFontColor() );
696  else if ( painter )
697  painter->setPen( mSettings.layerFontColor() );
698 
699  QFont layerFont = mSettings.style( nodeLegendStyle( nodeLayer ) ).font();
700 
701  QgsExpressionContextScope *layerScope = nullptr;
702  if ( context && nodeLayer->layer() )
703  {
704  layerScope = QgsExpressionContextUtils::layerScope( nodeLayer->layer() );
705  context->expressionContext().appendScope( layerScope );
706  }
707 
708  QgsExpressionContext tempContext;
709 
710  const QStringList lines = mSettings.evaluateItemText( mLegendModel->data( idx, Qt::DisplayRole ).toString(),
711  context ? context->expressionContext() : tempContext );
712  int i = 0;
713  for ( QStringList::ConstIterator layerItemPart = lines.constBegin(); layerItemPart != lines.constEnd(); ++layerItemPart )
714  {
715  y += mSettings.fontAscentMillimeters( layerFont );
716  if ( QPainter *destPainter = context && context->painter() ? context->painter() : painter )
717  {
718  double x = columnContext.left;
719  if ( mSettings.style( nodeLegendStyle( nodeLayer ) ).alignment() != Qt::AlignLeft )
720  {
721  const double labelWidth = mSettings.textWidthMillimeters( layerFont, *layerItemPart );
722  if ( mSettings.style( nodeLegendStyle( nodeLayer ) ).alignment() == Qt::AlignRight )
723  x = columnContext.right - labelWidth;
724  else if ( mSettings.style( nodeLegendStyle( nodeLayer ) ).alignment() == Qt::AlignHCenter )
725  x = columnContext.left + ( columnContext.right - columnContext.left - labelWidth ) / 2;
726  }
727  mSettings.drawText( destPainter, x, y, *layerItemPart, layerFont );
728  }
729  qreal width = mSettings.textWidthMillimeters( layerFont, *layerItemPart );
730  size.rwidth() = std::max( width, size.width() );
731  if ( layerItemPart != ( lines.end() - 1 ) )
732  {
733  y += mSettings.lineSpacing();
734  }
735  i++;
736  }
737  size.rheight() = y - top;
738  size.rheight() += mSettings.style( nodeLegendStyle( nodeLayer ) ).margin( QgsLegendStyle::Side::Bottom );
739 
740  if ( layerScope )
741  delete context->expressionContext().popScope();
742 
743  return size;
744 }
745 
746 QSizeF QgsLegendRenderer::drawGroupTitle( QgsLayerTreeGroup *nodeGroup, ColumnContext columnContext, QPainter *painter, double top )
747 {
748  return drawGroupTitleInternal( nodeGroup, columnContext, nullptr, painter, top );
749 }
750 
751 QSizeF QgsLegendRenderer::drawGroupTitleInternal( QgsLayerTreeGroup *nodeGroup, ColumnContext columnContext, QgsRenderContext *context, QPainter *painter, const double top )
752 {
753  QSizeF size( 0, 0 );
754  QModelIndex idx = mLegendModel->node2index( nodeGroup );
755 
756  double y = top;
757 
758  if ( context && context->painter() )
759  context->painter()->setPen( mSettings.fontColor() );
760  else if ( painter )
761  painter->setPen( mSettings.fontColor() );
762 
763  QFont groupFont = mSettings.style( nodeLegendStyle( nodeGroup ) ).font();
764 
765  QgsExpressionContext tempContext;
766 
767  const QStringList lines = mSettings.evaluateItemText( mLegendModel->data( idx, Qt::DisplayRole ).toString(),
768  context ? context->expressionContext() : tempContext );
769  for ( QStringList::ConstIterator groupPart = lines.constBegin(); groupPart != lines.constEnd(); ++groupPart )
770  {
771  y += mSettings.fontAscentMillimeters( groupFont );
772 
773  if ( QPainter *destPainter = context && context->painter() ? context->painter() : painter )
774  {
775  double x = columnContext.left;
776  if ( mSettings.style( nodeLegendStyle( nodeGroup ) ).alignment() != Qt::AlignLeft )
777  {
778  const double labelWidth = mSettings.textWidthMillimeters( groupFont, *groupPart );
779  if ( mSettings.style( nodeLegendStyle( nodeGroup ) ).alignment() == Qt::AlignRight )
780  x = columnContext.right - labelWidth;
781  else if ( mSettings.style( nodeLegendStyle( nodeGroup ) ).alignment() == Qt::AlignHCenter )
782  x = columnContext.left + ( columnContext.right - columnContext.left - labelWidth ) / 2;
783  }
784  mSettings.drawText( destPainter, x, y, *groupPart, groupFont );
785  }
786  qreal width = mSettings.textWidthMillimeters( groupFont, *groupPart );
787  size.rwidth() = std::max( width, size.width() );
788  if ( groupPart != ( lines.end() - 1 ) )
789  {
790  y += mSettings.lineSpacing();
791  }
792  }
793  size.rheight() = y - top + mSettings.style( nodeLegendStyle( nodeGroup ) ).margin( QgsLegendStyle::Bottom );
794  return size;
795 }
796 
798 {
799  QString style = node->customProperty( QStringLiteral( "legend/title-style" ) ).toString();
800  if ( style == QLatin1String( "hidden" ) )
801  return QgsLegendStyle::Hidden;
802  else if ( style == QLatin1String( "group" ) )
803  return QgsLegendStyle::Group;
804  else if ( style == QLatin1String( "subgroup" ) )
806 
807  // use a default otherwise
808  if ( QgsLayerTree::isGroup( node ) )
809  return QgsLegendStyle::Group;
810  else if ( QgsLayerTree::isLayer( node ) )
811  {
812  if ( model->legendNodeEmbeddedInParent( QgsLayerTree::toLayer( node ) ) )
813  return QgsLegendStyle::Hidden;
815  }
816 
817  return QgsLegendStyle::Undefined; // should not happen, only if corrupted project file
818 }
819 
821 {
822  return nodeLegendStyle( node, mLegendModel );
823 }
824 
826 {
827  QString str;
828  switch ( style )
829  {
831  str = QStringLiteral( "hidden" );
832  break;
834  str = QStringLiteral( "group" );
835  break;
837  str = QStringLiteral( "subgroup" );
838  break;
839  default:
840  break; // nothing
841  }
842 
843  if ( !str.isEmpty() )
844  node->setCustomProperty( QStringLiteral( "legend/title-style" ), str );
845  else
846  node->removeCustomProperty( QStringLiteral( "legend/title-style" ) );
847 }
848 
849 QSizeF QgsLegendRenderer::drawTitle( QgsRenderContext *rendercontext, double top, Qt::AlignmentFlag halignment, double legendWidth )
850 {
851  return drawTitleInternal( rendercontext, nullptr, top, halignment, legendWidth );
852 }
853 
854 QSizeF QgsLegendRenderer::drawGroup( const LegendComponentGroup &group, QgsRenderContext *rendercontext, ColumnContext columnContext, double top )
855 {
856  return drawGroupInternal( group, rendercontext, columnContext, nullptr, top );
857 }
858 
859 QgsLegendRenderer::LegendComponent QgsLegendRenderer::drawSymbolItem( QgsLayerTreeModelLegendNode *symbolItem, QgsRenderContext *rendercontext, ColumnContext columnContext, double top, double maxSiblingSymbolWidth )
860 {
861  return drawSymbolItemInternal( symbolItem, columnContext, rendercontext, nullptr, top, maxSiblingSymbolWidth );
862 }
863 
864 QSizeF QgsLegendRenderer::drawLayerTitle( QgsLayerTreeLayer *nodeLayer, QgsRenderContext *rendercontext, ColumnContext columnContext, double top )
865 {
866  return drawLayerTitleInternal( nodeLayer, columnContext, rendercontext, nullptr, top );
867 }
868 
869 QSizeF QgsLegendRenderer::drawGroupTitle( QgsLayerTreeGroup *nodeGroup, QgsRenderContext *rendercontext, ColumnContext columnContext, double top )
870 {
871  return drawGroupTitleInternal( nodeGroup, columnContext, rendercontext, nullptr, top );
872 }
873 
875 {
876  paintAndDetermineSize( &context );
877 }
878 
879 QSizeF QgsLegendRenderer::paintAndDetermineSize( QgsRenderContext *context )
880 {
881  return paintAndDetermineSizeInternal( context, nullptr );
882 }
static void setNodeLegendStyle(QgsLayerTreeNode *node, QgsLegendStyle::Style style)
Sets the style of a node.
Q_NOWARN_DEPRECATED_POP QgsRenderContext * context
Render context, if available.
Layer tree group node serves as a container for layers and further groups.
void drawText(QPainter *p, double x, double y, const QString &text, const QFont &font) const
Draws Text.
static QgsLayerTreeLayer * toLayer(QgsLayerTreeNode *node)
Cast node to a layer.
Definition: qgslayertree.h:75
double columnRight
Right side of current legend column.
static bool isGroup(QgsLayerTreeNode *node)
Check whether the node is a valid group node.
Definition: qgslayertree.h:43
int columnCount() const
double fontAscentMillimeters(const QFont &font) const
Returns the font ascent in Millimeters (considers upscaling and downscaling with FONT_WORKAROUND_SCAL...
QStringList evaluateItemText(const QString &text, const QgsExpressionContext &context) const
Splits a string using the wrap char taking into account handling empty wrap char which means no wrapp...
static QgsLayerTreeGroup * toGroup(QgsLayerTreeNode *node)
Cast node to a group.
Definition: qgslayertree.h:64
void drawLegend(QPainter *painter)
Draws the legend with given painter.
QStringList splitStringForWrapping(const QString &stringToSplt) const
Splits a string using the wrap char taking into account handling empty wrap char which means no wrapp...
QgsLayerTreeModelLegendNode * legendNodeEmbeddedInParent(QgsLayerTreeLayer *nodeLayer) const
Returns legend node that may be embedded in parent (i.e.
double margin(Side side)
Returns the margin (in mm) for the specified side of the component.
QFont font() const
Returns the font used for rendering this legend component.
#define Q_NOWARN_DEPRECATED_PUSH
Definition: qgis.h:649
QColor layerFontColor() const
Returns layer font color, defaults to fontColor()
QgsLayerTreeLayer * layerNode() const
Returns pointer to the parent layer node.
double textWidthMillimeters(const QFont &font, const QString &text) const
Returns the font width in millimeters (considers upscaling and downscaling with FONT_WORKAROUND_SCALE...
Should not happen, only if corrupted project file.
QModelIndex node2index(QgsLayerTreeNode *node) const
Returns index for a given node. If the node does not belong to the layer tree, the result is undefine...
QgsLegendRenderer(QgsLayerTreeModel *legendModel, const QgsLegendSettings &settings)
Constructor for QgsLegendRenderer.
The QgsLayerTreeModel class is model implementation for Qt item views framework.
QgsLegendStyle style(QgsLegendStyle::Style s) const
Returns style.
static QgsLegendStyle::Style nodeLegendStyle(QgsLayerTreeNode *node, QgsLayerTreeModel *model)
Returns the style for the given node, within the specified model.
QList< QgsLayerTreeNode * > children()
Gets list of children of the node. Children are owned by the parent.
Q_DECL_DEPRECATED double labelXOffset
Offset from the left side where label should start.
Expression contexts are used to encapsulate the parameters around which a QgsExpression should be eva...
The QgsLegendSettings class stores the appearance and layout settings for legend drawing with QgsLege...
QVariant customProperty(const QString &key, const QVariant &defaultValue=QVariant()) const
Read a custom property from layer. Properties are stored in a map and saved in project file...
Symbol icon (excluding label)
void setPainter(QPainter *p)
Sets the destination QPainter for the render operation.
static bool isLayer(const QgsLayerTreeNode *node)
Check whether the node is a valid layer node.
Definition: qgslayertree.h:53
This class is a base class for nodes in a layer tree.
Legend subgroup title.
Qt::AlignmentFlag titleAlignment() const
Returns the alignment of the legend title.
double maxSiblingSymbolWidth
Largest symbol width, considering all other sibling legend components associated with the current com...
double boxSpace() const
Single scope for storing variables and functions for use within a QgsExpressionContext.
void removeCustomProperty(const QString &key)
Remove a custom property from layer. Properties are stored in a map and saved in project file...
QgsLayerTreeModelLegendNode * legendNode(const QString &rule, QgsLayerTreeModel &model)
Qt::Alignment alignment() const
Returns the alignment for the legend component.
Style
Component of legends which can be styled.
virtual ItemMetrics draw(const QgsLegendSettings &settings, ItemContext *ctx)
Entry point called from QgsLegendRenderer to do the rendering.
QgsExpressionContext & expressionContext()
Gets the expression context.
const QgsMapSettings * legendFilterMapSettings() const
Returns the current map settings used for the current legend filter (or nullptr if none is enabled) ...
QgsMapLayer * layer() const
Returns the map layer associated with this node.
#define Q_NOWARN_DEPRECATED_POP
Definition: qgis.h:650
double top
Top y-position of legend item.
void exportToJson(const QgsLegendSettings &settings, const QgsRenderContext &context, QJsonObject &json)
Entry point called from QgsLegendRenderer to do the rendering in a JSON object.
Special style, item is hidden including margins around.
Contains information about the context of a rendering operation.
QPainter * painter()
Returns the destination QPainter for the render operation.
QgsLayerTreeModel * legendModel(const QgsWmsRenderContext &context, QgsLayerTree &tree)
QgsLayerTree * rootGroup() const
Returns pointer to the root node of the layer tree. Always a non nullptr value.
void exportLegendToJson(const QgsRenderContext &context, QJsonObject &json)
Renders the legend in a json object.
bool equalColumnWidth() const
void appendScope(QgsExpressionContextScope *scope)
Appends a scope to the end of the context.
double columnSpace() const
double columnLeft
Left side of current legend column.
QVariant data(const QModelIndex &index, int role=Qt::DisplayRole) const override
The QgsLegendRendererItem class is abstract interface for legend items returned from QgsMapLayerLegen...
Q_DECL_DEPRECATED QPointF point
Top-left corner of the legend item.
QList< QgsLayerTreeModelLegendNode * > layerLegendNodes(QgsLayerTreeLayer *nodeLayer, bool skipNodeEmbeddedInParent=false)
Returns filtered list of active legend nodes attached to a particular layer node (by default it retur...
QSizeF minimumSize(QgsRenderContext *renderContext=nullptr)
Runs the layout algorithm and returns the minimum size required for the legend.
QgsExpressionContextScope * popScope()
Removes the last scope from the expression context and return it.
static QgsExpressionContextScope * layerScope(const QgsMapLayer *layer)
Creates a new scope which contains variables and functions relating to a QgsMapLayer.
Symbol label (excluding icon)
double lineSpacing() const
QColor fontColor() const
bool splitLayer() const
double fontDescentMillimeters(const QFont &font) const
Returns the font descent in Millimeters (considers upscaling and downscaling with FONT_WORKAROUND_SCA...
Legend group title.
void setCustomProperty(const QString &key, const QVariant &value)
Sets a custom property for the node. Properties are stored in a map and saved in project file...
Layer tree node points to a map layer.
QString title() const