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