QGIS API Documentation  3.23.0-Master (dd0cd13a00)
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 )
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  double closedColumnsHeight = 0;
492  int autoPlacedBreaks = 0;
493 
494  // Calculate the expected average space between items
495  double averageSpaceAboveGroups = 0;
496  if ( componentGroups.size() > targetNumberColumns )
497  averageSpaceAboveGroups = totalSpaceAboveGroups / ( componentGroups.size() );
498  // Correct the totalHeight using the number of columns because the first item
499  // in each column does not get any space above it
500  totalHeight -= targetNumberColumns * averageSpaceAboveGroups;
501 
502  for ( int i = 0; i < componentGroups.size(); i++ )
503  {
504  LegendComponentGroup group = componentGroups.at( i );
505  double currentHeight = currentColumnHeight;
506  if ( currentColumnGroupCount > 0 )
507  currentHeight += spaceAboveGroup( group );
508  currentHeight += group.size.height();
509 
510  int numberRemainingGroups = componentGroups.size() - i;
511 
512  // Recalc average height for remaining columns including current
513  int numberRemainingColumns = numberAutoPlacedBreaks + 1 - autoPlacedBreaks;
514  double avgColumnHeight = ( currentHeight + numberRemainingGroups * averageGroupHeight + ( numberRemainingGroups - numberRemainingColumns - 1 ) * averageSpaceAboveGroups ) / numberRemainingColumns;
515  // Round up to the next full number of groups to put in one column
516  // This ensures that earlier columns contain more elements than later columns
517  int averageGroupsPerColumn = std::ceil( avgColumnHeight / ( averageGroupHeight + averageSpaceAboveGroups ) );
518  avgColumnHeight = averageGroupsPerColumn * ( averageGroupHeight + averageSpaceAboveGroups ) - averageSpaceAboveGroups;
519 
520  bool canCreateNewColumn = ( currentColumnGroupCount > 0 ) // do not leave empty column
521  && ( currentColumn < targetNumberColumns - 1 ) // must not exceed max number of columns
522  && ( autoPlacedBreaks < numberAutoPlacedBreaks );
523 
524  bool shouldCreateNewColumn = currentHeight > avgColumnHeight // current group height is greater than expected group height
525  && currentColumnGroupCount > 0 // do not leave empty column
526  && currentHeight > maxGroupHeight // no sense to make smaller columns than max group height
527  && currentHeight > maxColumnHeight; // no sense to make smaller columns than max column already created
528 
529  shouldCreateNewColumn |= group.placeColumnBreakBeforeGroup;
530  canCreateNewColumn |= group.placeColumnBreakBeforeGroup;
531 
532  // also should create a new column if the number of items left < number of columns left
533  // in this case we should spread the remaining items out over the remaining columns
534  shouldCreateNewColumn |= ( componentGroups.size() - i < targetNumberColumns - currentColumn );
535 
536  if ( canCreateNewColumn && shouldCreateNewColumn )
537  {
538  // New column
539  currentColumn++;
540  if ( !group.placeColumnBreakBeforeGroup )
541  autoPlacedBreaks++;
542  currentColumnGroupCount = 0;
543  closedColumnsHeight += currentColumnHeight;
544  currentColumnHeight = group.size.height();
545  }
546  else
547  {
548  currentColumnHeight = currentHeight;
549  }
550  componentGroups[i].column = currentColumn;
551  currentColumnGroupCount++;
552  maxColumnHeight = std::max( currentColumnHeight, maxColumnHeight );
553  }
554 
555  // Align labels of symbols for each layer/column to the same labelXOffset
556  QMap<QString, qreal> maxSymbolWidth;
557  for ( int i = 0; i < componentGroups.size(); i++ )
558  {
559  LegendComponentGroup &group = componentGroups[i];
560  for ( int j = 0; j < group.components.size(); j++ )
561  {
562  if ( QgsLayerTreeModelLegendNode *legendNode = qobject_cast<QgsLayerTreeModelLegendNode *>( group.components.at( j ).item ) )
563  {
564  QString key = QStringLiteral( "%1-%2" ).arg( reinterpret_cast< qulonglong >( legendNode->layerNode() ) ).arg( group.column );
565  maxSymbolWidth[key] = std::max( group.components.at( j ).symbolSize.width(), maxSymbolWidth[key] );
566  }
567  }
568  }
569  for ( int i = 0; i < componentGroups.size(); i++ )
570  {
571  LegendComponentGroup &group = componentGroups[i];
572  for ( int j = 0; j < group.components.size(); j++ )
573  {
574  if ( QgsLayerTreeModelLegendNode *legendNode = qobject_cast<QgsLayerTreeModelLegendNode *>( group.components.at( j ).item ) )
575  {
576  QString key = QStringLiteral( "%1-%2" ).arg( reinterpret_cast< qulonglong >( legendNode->layerNode() ) ).arg( group.column );
577  double space = mSettings.style( QgsLegendStyle::Symbol ).margin( QgsLegendStyle::Right ) +
579  group.components[j].labelXOffset = maxSymbolWidth[key] + space;
580  group.components[j].maxSiblingSymbolWidth = maxSymbolWidth[key];
581  group.components[j].size.rwidth() = maxSymbolWidth[key] + space + group.components.at( j ).labelSize.width();
582  }
583  }
584  }
585  return targetNumberColumns;
586 }
587 
588 QSizeF QgsLegendRenderer::drawTitle( QgsRenderContext &context, double top, Qt::AlignmentFlag halignment, double legendWidth )
589 {
590  QSizeF size( 0, 0 );
591  if ( mSettings.title().isEmpty() )
592  {
593  return size;
594  }
595 
596  QStringList lines = mSettings.splitStringForWrapping( mSettings.title() );
597  double y = top;
598 
599  if ( auto *lPainter = context.painter() )
600  {
601  lPainter->setPen( mSettings.fontColor() );
602  }
603 
604  //calculate width and left pos of rectangle to draw text into
605  double textBoxWidth;
606  double textBoxLeft;
607  widthAndOffsetForTitleText( halignment, legendWidth, textBoxWidth, textBoxLeft );
608 
609  QFont titleFont = mSettings.style( QgsLegendStyle::Title ).font();
610 
611  for ( QStringList::Iterator titlePart = lines.begin(); titlePart != lines.end(); ++titlePart )
612  {
613  //last word is not drawn if rectangle width is exactly text width, so add 1
614  //TODO - correctly calculate size of italicized text, since QFontMetrics does not
615  qreal width = mSettings.textWidthMillimeters( titleFont, *titlePart ) + 1;
616  qreal height = mSettings.fontAscentMillimeters( titleFont ) + mSettings.fontDescentMillimeters( titleFont );
617 
618  QRectF r( textBoxLeft, y, textBoxWidth, height );
619 
620  if ( context.painter() )
621  {
622  mSettings.drawText( context.painter(), r, *titlePart, titleFont, halignment, Qt::AlignVCenter, Qt::TextDontClip );
623  }
624 
625  //update max width of title
626  size.rwidth() = std::max( width, size.rwidth() );
627 
628  y += height;
629  if ( titlePart != ( lines.end() - 1 ) )
630  {
631  y += mSettings.lineSpacing();
632  }
633  }
634  size.rheight() = y - top;
635 
636  return size;
637 }
638 
639 
640 double QgsLegendRenderer::spaceAboveGroup( const LegendComponentGroup &group )
641 {
642  if ( group.components.isEmpty() ) return 0;
643 
644  LegendComponent component = group.components.first();
645 
646  if ( QgsLayerTreeGroup *nodeGroup = qobject_cast<QgsLayerTreeGroup *>( component.item ) )
647  {
648  return mSettings.style( nodeLegendStyle( nodeGroup ) ).margin( QgsLegendStyle::Top );
649  }
650  else if ( QgsLayerTreeLayer *nodeLayer = qobject_cast<QgsLayerTreeLayer *>( component.item ) )
651  {
652  return mSettings.style( nodeLegendStyle( nodeLayer ) ).margin( QgsLegendStyle::Top );
653  }
654  else if ( qobject_cast<QgsLayerTreeModelLegendNode *>( component.item ) )
655  {
656  // TODO: use Symbol or SymbolLabel Top margin
658  }
659 
660  return 0;
661 }
662 
663 QSizeF QgsLegendRenderer::drawGroup( const LegendComponentGroup &group, QgsRenderContext &context, ColumnContext columnContext, double top )
664 {
665  bool first = true;
666  QSizeF size = QSizeF( group.size );
667  double currentY = top;
668  for ( const LegendComponent &component : std::as_const( group.components ) )
669  {
670  if ( QgsLayerTreeGroup *groupItem = qobject_cast<QgsLayerTreeGroup *>( component.item ) )
671  {
672  QgsLegendStyle::Style s = nodeLegendStyle( groupItem );
673  if ( s != QgsLegendStyle::Hidden )
674  {
675  if ( !first )
676  {
677  currentY += mSettings.style( s ).margin( QgsLegendStyle::Top );
678  }
679  QSizeF groupSize;
680  ColumnContext columnContextForItem = columnContext;
681  double indentWidth = component.indent;
682  if ( s == QgsLegendStyle::Subgroup )
683  {
684  // Remove indent - the subgroup items should be indented, not the subgroup title
685  indentWidth -= mSettings.style( QgsLegendStyle::Subgroup ).indent( );
686  }
687  else
688  {
689  // Remove indent - the group items should be indented, not the group title
690  indentWidth -= mSettings.style( QgsLegendStyle::Group ).indent( );
691  }
692  if ( mSettings.style( QgsLegendStyle::SymbolLabel ).alignment() == Qt::AlignLeft )
693  {
694  columnContextForItem.left += indentWidth;
695  }
696  if ( mSettings.style( QgsLegendStyle::SymbolLabel ).alignment() == Qt::AlignRight )
697  {
698  columnContextForItem.right -= indentWidth;
699  }
700  groupSize = drawGroupTitle( groupItem, context, columnContextForItem, currentY );
701  size.rwidth() = std::max( groupSize.width(), size.width() );
702  }
703  }
704  else if ( QgsLayerTreeLayer *layerItem = qobject_cast<QgsLayerTreeLayer *>( component.item ) )
705  {
706  QgsLegendStyle::Style s = nodeLegendStyle( layerItem );
707  if ( s != QgsLegendStyle::Hidden )
708  {
709  if ( !first )
710  {
711  currentY += mSettings.style( s ).margin( QgsLegendStyle::Top );
712  }
713  QSizeF subGroupSize;
714 
715  ColumnContext columnContextForItem = columnContext;
716  double indentWidth = component.indent;
717  columnContextForItem.left += indentWidth;
718  subGroupSize = drawLayerTitle( layerItem, context, columnContextForItem, currentY );
719  size.rwidth() = std::max( subGroupSize.width(), size.width() );
720  }
721  }
722  else if ( QgsLayerTreeModelLegendNode *legendNode = qobject_cast<QgsLayerTreeModelLegendNode *>( component.item ) )
723  {
724  if ( !first )
725  {
726  currentY += mSettings.style( QgsLegendStyle::Symbol ).margin( QgsLegendStyle::Top );
727  }
728 
729  ColumnContext columnContextForItem = columnContext;
730  double indentWidth = 0;
731  indentWidth = component.indent;
732  if ( mSettings.style( QgsLegendStyle::SymbolLabel ).alignment() == Qt::AlignLeft )
733  {
734  columnContextForItem.left += indentWidth;
735  }
736  if ( mSettings.style( QgsLegendStyle::SymbolLabel ).alignment() == Qt::AlignRight )
737  {
738  columnContextForItem.right -= indentWidth;
739  }
740 
741  LegendComponent symbolComponent = drawSymbolItem( legendNode, context, columnContextForItem, currentY, component.maxSiblingSymbolWidth );
742  // expand width, it may be wider because of label offsets
743  size.rwidth() = std::max( symbolComponent.size.width() + indentWidth, size.width() );
744  }
745  currentY += component.size.height();
746  first = false;
747  }
748  return size;
749 }
750 
751 QgsLegendRenderer::LegendComponent QgsLegendRenderer::drawSymbolItem( QgsLayerTreeModelLegendNode *symbolItem, QgsRenderContext &context, ColumnContext columnContext, double top, double maxSiblingSymbolWidth )
752 {
754  ctx.context = &context;
755 
756  // add a layer expression context scope
757  QgsExpressionContextScope *layerScope = nullptr;
758  if ( symbolItem->layerNode()->layer() )
759  {
760  layerScope = QgsExpressionContextUtils::layerScope( symbolItem->layerNode()->layer() );
761  context.expressionContext().appendScope( layerScope );
762  }
763 
764  ctx.painter = context.painter();
766  ctx.point = QPointF( columnContext.left, top );
767  ctx.labelXOffset = maxSiblingSymbolWidth;
769 
770  ctx.top = top;
771 
772  ctx.columnLeft = columnContext.left;
773  ctx.columnRight = columnContext.right;
774 
775  switch ( mSettings.symbolAlignment() )
776  {
777  case Qt::AlignLeft:
778  default:
780  break;
781 
782  case Qt::AlignRight:
784  break;
785  }
786 
787  ctx.maxSiblingSymbolWidth = maxSiblingSymbolWidth;
788 
789  if ( const QgsSymbolLegendNode *symbolNode = dynamic_cast< const QgsSymbolLegendNode * >( symbolItem ) )
790  ctx.patchShape = symbolNode->patchShape();
791 
792  ctx.patchSize = symbolItem->userPatchSize();
793 
794  QgsLayerTreeModelLegendNode::ItemMetrics im = symbolItem->draw( mSettings, &ctx );
795 
796  if ( layerScope )
797  delete context.expressionContext().popScope();
798 
799  LegendComponent component;
800  component.item = symbolItem;
801  component.symbolSize = im.symbolSize;
802  component.labelSize = im.labelSize;
803  //QgsDebugMsg( QStringLiteral( "symbol height = %1 label height = %2").arg( symbolSize.height()).arg( labelSize.height() ));
804  // NOTE -- we hard code left/right margins below, because those are the only ones exposed for use currently.
805  // ideally we could (should?) expose all these margins as settings, and then adapt the below to respect the current symbol/text alignment
806  // and consider the correct margin sides...
807  double width = std::max( static_cast< double >( im.symbolSize.width() ), maxSiblingSymbolWidth )
811  + im.labelSize.width();
812 
813  double height = std::max( im.symbolSize.height(), im.labelSize.height() );
814  component.size = QSizeF( width, height );
815  return component;
816 }
817 
818 QSizeF QgsLegendRenderer::drawLayerTitle( QgsLayerTreeLayer *nodeLayer, QgsRenderContext &context, ColumnContext columnContext, double top )
819 {
820  QSizeF size( 0, 0 );
821  QModelIndex idx = mLegendModel->node2index( nodeLayer );
822  QString titleString = mLegendModel->data( idx, Qt::DisplayRole ).toString();
823  //Let the user omit the layer title item by having an empty layer title string
824  if ( titleString.isEmpty() )
825  return size;
826 
827  double y = top;
828 
829  if ( auto *lPainter = context.painter() )
830  lPainter->setPen( mSettings.layerFontColor() );
831 
832  QFont layerFont = mSettings.style( nodeLegendStyle( nodeLayer ) ).font();
833 
834  QgsExpressionContextScope *layerScope = nullptr;
835  if ( nodeLayer->layer() )
836  {
837  layerScope = QgsExpressionContextUtils::layerScope( nodeLayer->layer() );
838  context.expressionContext().appendScope( layerScope );
839  }
840 
841  const QStringList lines = mSettings.evaluateItemText( titleString, context.expressionContext() );
842  int i = 0;
843 
844  const double sideMargin = mSettings.style( nodeLegendStyle( nodeLayer ) ).margin( QgsLegendStyle::Left );
845  for ( QStringList::ConstIterator layerItemPart = lines.constBegin(); layerItemPart != lines.constEnd(); ++layerItemPart )
846  {
847  y += mSettings.fontAscentMillimeters( layerFont );
848  if ( QPainter *destPainter = context.painter() )
849  {
850  double x = columnContext.left + sideMargin;
851  if ( mSettings.style( nodeLegendStyle( nodeLayer ) ).alignment() != Qt::AlignLeft )
852  {
853  const double labelWidth = mSettings.textWidthMillimeters( layerFont, *layerItemPart );
854  if ( mSettings.style( nodeLegendStyle( nodeLayer ) ).alignment() == Qt::AlignRight )
855  x = columnContext.right - labelWidth - sideMargin;
856  else if ( mSettings.style( nodeLegendStyle( nodeLayer ) ).alignment() == Qt::AlignHCenter )
857  x = columnContext.left + ( columnContext.right - columnContext.left - labelWidth ) / 2;
858  }
859  mSettings.drawText( destPainter, x, y, *layerItemPart, layerFont );
860  }
861  qreal width = mSettings.textWidthMillimeters( layerFont, *layerItemPart ) + sideMargin *
862  ( mSettings.style( nodeLegendStyle( nodeLayer ) ).alignment() == Qt::AlignHCenter ? 2 : 1 );
863  size.rwidth() = std::max( width, size.width() );
864  if ( layerItemPart != ( lines.end() - 1 ) )
865  {
866  y += mSettings.lineSpacing();
867  }
868  i++;
869  }
870  size.rheight() = y - top;
871  size.rheight() += mSettings.style( nodeLegendStyle( nodeLayer ) ).margin( QgsLegendStyle::Side::Bottom );
872 
873  if ( layerScope )
874  delete context.expressionContext().popScope();
875 
876  return size;
877 }
878 
879 QSizeF QgsLegendRenderer::drawGroupTitle( QgsLayerTreeGroup *nodeGroup, QgsRenderContext &context, ColumnContext columnContext, double top )
880 {
881  QSizeF size( 0, 0 );
882  QModelIndex idx = mLegendModel->node2index( nodeGroup );
883 
884  double y = top;
885 
886  if ( auto *lPainter = context.painter() )
887  lPainter->setPen( mSettings.fontColor() );
888 
889  QFont groupFont = mSettings.style( nodeLegendStyle( nodeGroup ) ).font();
890 
891  const double sideMargin = mSettings.style( nodeLegendStyle( nodeGroup ) ).margin( QgsLegendStyle::Left );
892 
893  const QStringList lines = mSettings.evaluateItemText( mLegendModel->data( idx, Qt::DisplayRole ).toString(), context.expressionContext() );
894  for ( QStringList::ConstIterator groupPart = lines.constBegin(); groupPart != lines.constEnd(); ++groupPart )
895  {
896  y += mSettings.fontAscentMillimeters( groupFont );
897 
898  if ( QPainter *destPainter = context.painter() )
899  {
900  double x = columnContext.left + sideMargin;
901  if ( mSettings.style( nodeLegendStyle( nodeGroup ) ).alignment() != Qt::AlignLeft )
902  {
903  const double labelWidth = mSettings.textWidthMillimeters( groupFont, *groupPart );
904  if ( mSettings.style( nodeLegendStyle( nodeGroup ) ).alignment() == Qt::AlignRight )
905  x = columnContext.right - labelWidth - sideMargin;
906  else if ( mSettings.style( nodeLegendStyle( nodeGroup ) ).alignment() == Qt::AlignHCenter )
907  x = columnContext.left + ( columnContext.right - columnContext.left - labelWidth ) / 2;
908  }
909  mSettings.drawText( destPainter, x, y, *groupPart, groupFont );
910  }
911  qreal width = mSettings.textWidthMillimeters( groupFont, *groupPart ) + sideMargin * ( mSettings.style( nodeLegendStyle( nodeGroup ) ).alignment() == Qt::AlignHCenter ? 2 : 1 );
912  size.rwidth() = std::max( width, size.width() );
913  if ( groupPart != ( lines.end() - 1 ) )
914  {
915  y += mSettings.lineSpacing();
916  }
917  }
918  size.rheight() = y - top + mSettings.style( nodeLegendStyle( nodeGroup ) ).margin( QgsLegendStyle::Bottom );
919  return size;
920 }
921 
923 {
924  QString style = node->customProperty( QStringLiteral( "legend/title-style" ) ).toString();
925  if ( style == QLatin1String( "hidden" ) )
926  return QgsLegendStyle::Hidden;
927  else if ( style == QLatin1String( "group" ) )
928  return QgsLegendStyle::Group;
929  else if ( style == QLatin1String( "subgroup" ) )
931 
932  // use a default otherwise
933  if ( QgsLayerTree::isGroup( node ) )
934  return QgsLegendStyle::Group;
935  else if ( QgsLayerTree::isLayer( node ) )
936  {
937  if ( model->legendNodeEmbeddedInParent( QgsLayerTree::toLayer( node ) ) )
938  return QgsLegendStyle::Hidden;
940  }
941 
942  return QgsLegendStyle::Undefined; // should not happen, only if corrupted project file
943 }
944 
946 {
947  return nodeLegendStyle( node, mLegendModel );
948 }
949 
951 {
952  QString str;
953  switch ( style )
954  {
956  str = QStringLiteral( "hidden" );
957  break;
959  str = QStringLiteral( "group" );
960  break;
962  str = QStringLiteral( "subgroup" );
963  break;
964  default:
965  break; // nothing
966  }
967 
968  if ( !str.isEmpty() )
969  node->setCustomProperty( QStringLiteral( "legend/title-style" ), str );
970  else
971  node->removeCustomProperty( QStringLiteral( "legend/title-style" ) );
972 }
973 
975 {
976  paintAndDetermineSize( context );
977 }
978 
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.
double indent()
Returns the indent (in mm) of a group or subgroup.
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)
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:2000
#define Q_NOWARN_DEPRECATED_PUSH
Definition: qgis.h:1999
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.