QGIS API Documentation  3.18.1-Zürich (202f1bf7e5)
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 : qgis::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 : qgis::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 )
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 
287  // Group subitems
288  QList<LegendComponentGroup> subgroups = createComponentGroupList( nodeGroup, context );
289  bool hasSubItems = !subgroups.empty();
290 
291  if ( nodeLegendStyle( nodeGroup ) != QgsLegendStyle::Hidden )
292  {
293  LegendComponent component;
294  component.item = node;
295  component.size = drawGroupTitle( nodeGroup, context );
296 
297  if ( !subgroups.isEmpty() )
298  {
299  // Add internal space between this group title and the next component
300  subgroups[0].size.rheight() += spaceAboveGroup( subgroups[0] );
301  // Prepend this group title to the first group
302  subgroups[0].components.prepend( component );
303  subgroups[0].size.rheight() += component.size.height();
304  subgroups[0].size.rwidth() = std::max( component.size.width(), subgroups[0].size.width() );
305  if ( nodeGroup->customProperty( QStringLiteral( "legend/column-break" ) ).toInt() )
306  subgroups[0].placeColumnBreakBeforeGroup = true;
307  }
308  else
309  {
310  // no subitems, create new group
311  LegendComponentGroup group;
312  group.placeColumnBreakBeforeGroup = nodeGroup->customProperty( QStringLiteral( "legend/column-break" ) ).toInt();
313  group.components.append( component );
314  group.size.rwidth() += component.size.width();
315  group.size.rheight() += component.size.height();
316  group.size.rwidth() = std::max( component.size.width(), group.size.width() );
317  subgroups.append( group );
318  }
319  }
320 
321  if ( hasSubItems ) //leave away groups without content
322  {
323  componentGroups.append( subgroups );
324  }
325 
326  }
327  else if ( QgsLayerTree::isLayer( node ) )
328  {
329  QgsLayerTreeLayer *nodeLayer = QgsLayerTree::toLayer( node );
330 
331  bool allowColumnSplit = false;
332  switch ( nodeLayer->legendSplitBehavior() )
333  {
335  allowColumnSplit = mSettings.splitLayer();
336  break;
338  allowColumnSplit = true;
339  break;
341  allowColumnSplit = false;
342  break;
343  }
344 
345  LegendComponentGroup group;
346  group.placeColumnBreakBeforeGroup = nodeLayer->customProperty( QStringLiteral( "legend/column-break" ) ).toInt();
347 
348  if ( nodeLegendStyle( nodeLayer ) != QgsLegendStyle::Hidden )
349  {
350  LegendComponent component;
351  component.item = node;
352  component.size = drawLayerTitle( nodeLayer, context );
353  group.components.append( component );
354  group.size.rwidth() = component.size.width();
355  group.size.rheight() = component.size.height();
356  }
357 
358  QList<QgsLayerTreeModelLegendNode *> legendNodes = mLegendModel->layerLegendNodes( nodeLayer );
359 
360  // workaround for the issue that "filtering by map" does not remove layer nodes that have no symbols present
361  // on the map. We explicitly skip such layers here. In future ideally that should be handled directly
362  // in the layer tree model
363  if ( legendNodes.isEmpty() && mLegendModel->legendFilterMapSettings() )
364  continue;
365 
366  QList<LegendComponentGroup> layerGroups;
367  layerGroups.reserve( legendNodes.count() );
368 
369  bool groupIsLayerGroup = true;
370 
371  for ( int j = 0; j < legendNodes.count(); j++ )
372  {
373  QgsLayerTreeModelLegendNode *legendNode = legendNodes.at( j );
374 
375  LegendComponent symbolComponent = drawSymbolItem( legendNode, context, ColumnContext(), 0 );
376 
377  const bool forceBreak = legendNode->columnBreak();
378 
379  if ( !allowColumnSplit || j == 0 )
380  {
381  if ( forceBreak )
382  {
383  if ( groupIsLayerGroup )
384  layerGroups.prepend( group );
385  else
386  layerGroups.append( group );
387 
388  group = LegendComponentGroup();
389  group.placeColumnBreakBeforeGroup = true;
390  groupIsLayerGroup = false;
391  }
392 
393  // append to layer group
394  // the width is not correct at this moment, we must align all symbol labels
395  group.size.rwidth() = std::max( symbolComponent.size.width(), group.size.width() );
396  // Add symbol space only if there is already title or another item above
397  if ( !group.components.isEmpty() )
398  {
399  // TODO: for now we keep Symbol and SymbolLabel Top margin in sync
400  group.size.rheight() += mSettings.style( QgsLegendStyle::Symbol ).margin( QgsLegendStyle::Top );
401  }
402  group.size.rheight() += symbolComponent.size.height();
403  group.components.append( symbolComponent );
404  }
405  else
406  {
407  if ( group.size.height() > 0 )
408  {
409  if ( groupIsLayerGroup )
410  layerGroups.prepend( group );
411  else
412  layerGroups.append( group );
413  group = LegendComponentGroup();
414  groupIsLayerGroup = false;
415  }
416  LegendComponentGroup symbolGroup;
417  symbolGroup.placeColumnBreakBeforeGroup = forceBreak;
418  symbolGroup.components.append( symbolComponent );
419  symbolGroup.size.rwidth() = symbolComponent.size.width();
420  symbolGroup.size.rheight() = symbolComponent.size.height();
421  layerGroups.append( symbolGroup );
422  }
423  }
424  if ( group.size.height() > 0 )
425  {
426  if ( groupIsLayerGroup )
427  layerGroups.prepend( group );
428  else
429  layerGroups.append( group );
430  }
431  componentGroups.append( layerGroups );
432  }
433  }
434 
435  return componentGroups;
436 }
437 
438 
439 int QgsLegendRenderer::setColumns( QList<LegendComponentGroup> &componentGroups )
440 {
441  // Divide groups to columns
442  double totalHeight = 0;
443  qreal maxGroupHeight = 0;
444  int forcedColumnBreaks = 0;
445  double totalSpaceAboveGroups = 0;
446  for ( const LegendComponentGroup &group : qgis::as_const( componentGroups ) )
447  {
448  totalHeight += spaceAboveGroup( group );
449  totalSpaceAboveGroups += spaceAboveGroup( group );
450  totalHeight += group.size.height();
451  maxGroupHeight = std::max( group.size.height(), maxGroupHeight );
452 
453  if ( group.placeColumnBreakBeforeGroup )
454  forcedColumnBreaks++;
455  }
456  double averageGroupHeight = ( totalHeight - totalSpaceAboveGroups ) / componentGroups.size();
457 
458  if ( mSettings.columnCount() == 0 && forcedColumnBreaks == 0 )
459  return 0;
460 
461  // the target number of columns allowed is dictated by the number of forced column
462  // breaks OR the manually set column count (whichever is greater!)
463  const int targetNumberColumns = std::max( forcedColumnBreaks + 1, mSettings.columnCount() );
464  const int numberAutoPlacedBreaks = targetNumberColumns - forcedColumnBreaks - 1;
465 
466  // We know height of each group and we have to split them into columns
467  // minimizing max column height. It is sort of bin packing problem, NP-hard.
468  // We are using simple heuristic, brute fore appeared to be to slow,
469  // the number of combinations is N = n!/(k!*(n-k)!) where n = groupCount-1
470  // and k = columnsCount-1
471  double maxColumnHeight = 0;
472  int currentColumn = 0;
473  int currentColumnGroupCount = 0; // number of groups in current column
474  double currentColumnHeight = 0;
475  double closedColumnsHeight = 0;
476  int autoPlacedBreaks = 0;
477 
478  // Calculate the expected average space between items
479  double averageSpaceAboveGroups = 0;
480  if ( componentGroups.size() > targetNumberColumns )
481  averageSpaceAboveGroups = totalSpaceAboveGroups / ( componentGroups.size() );
482  // Correct the totalHeight using the number of columns because the first item
483  // in each column does not get any space above it
484  totalHeight -= targetNumberColumns * averageSpaceAboveGroups;
485 
486  for ( int i = 0; i < componentGroups.size(); i++ )
487  {
488  LegendComponentGroup group = componentGroups.at( i );
489  double currentHeight = currentColumnHeight;
490  if ( currentColumnGroupCount > 0 )
491  currentHeight += spaceAboveGroup( group );
492  currentHeight += group.size.height();
493 
494  int numberRemainingGroups = componentGroups.size() - i;
495 
496  // Recalc average height for remaining columns including current
497  int numberRemainingColumns = numberAutoPlacedBreaks + 1 - autoPlacedBreaks;
498  double avgColumnHeight = ( currentHeight + numberRemainingGroups * averageGroupHeight + ( numberRemainingGroups - numberRemainingColumns - 1 ) * averageSpaceAboveGroups ) / numberRemainingColumns;
499  // Round up to the next full number of groups to put in one column
500  // This ensures that earlier columns contain more elements than later columns
501  int averageGroupsPerColumn = std::ceil( avgColumnHeight / ( averageGroupHeight + averageSpaceAboveGroups ) );
502  avgColumnHeight = averageGroupsPerColumn * ( averageGroupHeight + averageSpaceAboveGroups ) - averageSpaceAboveGroups;
503 
504  bool canCreateNewColumn = ( currentColumnGroupCount > 0 ) // do not leave empty column
505  && ( currentColumn < targetNumberColumns - 1 ) // must not exceed max number of columns
506  && ( autoPlacedBreaks < numberAutoPlacedBreaks );
507 
508  bool shouldCreateNewColumn = currentHeight > avgColumnHeight // current group height is greater than expected group height
509  && currentColumnGroupCount > 0 // do not leave empty column
510  && currentHeight > maxGroupHeight // no sense to make smaller columns than max group height
511  && currentHeight > maxColumnHeight; // no sense to make smaller columns than max column already created
512 
513  shouldCreateNewColumn |= group.placeColumnBreakBeforeGroup;
514  canCreateNewColumn |= group.placeColumnBreakBeforeGroup;
515 
516  // also should create a new column if the number of items left < number of columns left
517  // in this case we should spread the remaining items out over the remaining columns
518  shouldCreateNewColumn |= ( componentGroups.size() - i < targetNumberColumns - currentColumn );
519 
520  if ( canCreateNewColumn && shouldCreateNewColumn )
521  {
522  // New column
523  currentColumn++;
524  if ( !group.placeColumnBreakBeforeGroup )
525  autoPlacedBreaks++;
526  currentColumnGroupCount = 0;
527  closedColumnsHeight += currentColumnHeight;
528  currentColumnHeight = group.size.height();
529  }
530  else
531  {
532  currentColumnHeight = currentHeight;
533  }
534  componentGroups[i].column = currentColumn;
535  currentColumnGroupCount++;
536  maxColumnHeight = std::max( currentColumnHeight, maxColumnHeight );
537  }
538 
539  // Align labels of symbols for each layer/column to the same labelXOffset
540  QMap<QString, qreal> maxSymbolWidth;
541  for ( int i = 0; i < componentGroups.size(); i++ )
542  {
543  LegendComponentGroup &group = componentGroups[i];
544  for ( int j = 0; j < group.components.size(); j++ )
545  {
546  if ( QgsLayerTreeModelLegendNode *legendNode = qobject_cast<QgsLayerTreeModelLegendNode *>( group.components.at( j ).item ) )
547  {
548  QString key = QStringLiteral( "%1-%2" ).arg( reinterpret_cast< qulonglong >( legendNode->layerNode() ) ).arg( group.column );
549  maxSymbolWidth[key] = std::max( group.components.at( j ).symbolSize.width(), maxSymbolWidth[key] );
550  }
551  }
552  }
553  for ( int i = 0; i < componentGroups.size(); i++ )
554  {
555  LegendComponentGroup &group = componentGroups[i];
556  for ( int j = 0; j < group.components.size(); j++ )
557  {
558  if ( QgsLayerTreeModelLegendNode *legendNode = qobject_cast<QgsLayerTreeModelLegendNode *>( group.components.at( j ).item ) )
559  {
560  QString key = QStringLiteral( "%1-%2" ).arg( reinterpret_cast< qulonglong >( legendNode->layerNode() ) ).arg( group.column );
561  double space = mSettings.style( QgsLegendStyle::Symbol ).margin( QgsLegendStyle::Right ) +
563  group.components[j].labelXOffset = maxSymbolWidth[key] + space;
564  group.components[j].maxSiblingSymbolWidth = maxSymbolWidth[key];
565  group.components[j].size.rwidth() = maxSymbolWidth[key] + space + group.components.at( j ).labelSize.width();
566  }
567  }
568  }
569  return targetNumberColumns;
570 }
571 
572 QSizeF QgsLegendRenderer::drawTitle( QgsRenderContext &context, double top, Qt::AlignmentFlag halignment, double legendWidth )
573 {
574  QSizeF size( 0, 0 );
575  if ( mSettings.title().isEmpty() )
576  {
577  return size;
578  }
579 
580  QStringList lines = mSettings.splitStringForWrapping( mSettings.title() );
581  double y = top;
582 
583  if ( auto *lPainter = context.painter() )
584  {
585  lPainter->setPen( mSettings.fontColor() );
586  }
587 
588  //calculate width and left pos of rectangle to draw text into
589  double textBoxWidth;
590  double textBoxLeft;
591  widthAndOffsetForTitleText( halignment, legendWidth, textBoxWidth, textBoxLeft );
592 
593  QFont titleFont = mSettings.style( QgsLegendStyle::Title ).font();
594 
595  for ( QStringList::Iterator titlePart = lines.begin(); titlePart != lines.end(); ++titlePart )
596  {
597  //last word is not drawn if rectangle width is exactly text width, so add 1
598  //TODO - correctly calculate size of italicized text, since QFontMetrics does not
599  qreal width = mSettings.textWidthMillimeters( titleFont, *titlePart ) + 1;
600  qreal height = mSettings.fontAscentMillimeters( titleFont ) + mSettings.fontDescentMillimeters( titleFont );
601 
602  QRectF r( textBoxLeft, y, textBoxWidth, height );
603 
604  if ( context.painter() )
605  {
606  mSettings.drawText( context.painter(), r, *titlePart, titleFont, halignment, Qt::AlignVCenter, Qt::TextDontClip );
607  }
608 
609  //update max width of title
610  size.rwidth() = std::max( width, size.rwidth() );
611 
612  y += height;
613  if ( titlePart != ( lines.end() - 1 ) )
614  {
615  y += mSettings.lineSpacing();
616  }
617  }
618  size.rheight() = y - top;
619 
620  return size;
621 }
622 
623 
624 double QgsLegendRenderer::spaceAboveGroup( const LegendComponentGroup &group )
625 {
626  if ( group.components.isEmpty() ) return 0;
627 
628  LegendComponent component = group.components.first();
629 
630  if ( QgsLayerTreeGroup *nodeGroup = qobject_cast<QgsLayerTreeGroup *>( component.item ) )
631  {
632  return mSettings.style( nodeLegendStyle( nodeGroup ) ).margin( QgsLegendStyle::Top );
633  }
634  else if ( QgsLayerTreeLayer *nodeLayer = qobject_cast<QgsLayerTreeLayer *>( component.item ) )
635  {
636  return mSettings.style( nodeLegendStyle( nodeLayer ) ).margin( QgsLegendStyle::Top );
637  }
638  else if ( qobject_cast<QgsLayerTreeModelLegendNode *>( component.item ) )
639  {
640  // TODO: use Symbol or SymbolLabel Top margin
642  }
643 
644  return 0;
645 }
646 
647 QSizeF QgsLegendRenderer::drawGroup( const LegendComponentGroup &group, QgsRenderContext &context, ColumnContext columnContext, double top )
648 {
649  bool first = true;
650  QSizeF size = QSizeF( group.size );
651  double currentY = top;
652  for ( const LegendComponent &component : qgis::as_const( group.components ) )
653  {
654  if ( QgsLayerTreeGroup *groupItem = qobject_cast<QgsLayerTreeGroup *>( component.item ) )
655  {
656  QgsLegendStyle::Style s = nodeLegendStyle( groupItem );
657  if ( s != QgsLegendStyle::Hidden )
658  {
659  if ( !first )
660  {
661  currentY += mSettings.style( s ).margin( QgsLegendStyle::Top );
662  }
663  QSizeF groupSize;
664  groupSize = drawGroupTitle( groupItem, context, columnContext, currentY );
665  size.rwidth() = std::max( groupSize.width(), size.width() );
666  }
667  }
668  else if ( QgsLayerTreeLayer *layerItem = qobject_cast<QgsLayerTreeLayer *>( component.item ) )
669  {
670  QgsLegendStyle::Style s = nodeLegendStyle( layerItem );
671  if ( s != QgsLegendStyle::Hidden )
672  {
673  if ( !first )
674  {
675  currentY += mSettings.style( s ).margin( QgsLegendStyle::Top );
676  }
677  QSizeF subGroupSize;
678  subGroupSize = drawLayerTitle( layerItem, context, columnContext, currentY );
679  size.rwidth() = std::max( subGroupSize.width(), size.width() );
680  }
681  }
682  else if ( QgsLayerTreeModelLegendNode *legendNode = qobject_cast<QgsLayerTreeModelLegendNode *>( component.item ) )
683  {
684  if ( !first )
685  {
686  currentY += mSettings.style( QgsLegendStyle::Symbol ).margin( QgsLegendStyle::Top );
687  }
688 
689  LegendComponent symbolComponent = drawSymbolItem( legendNode, context, columnContext, currentY, component.maxSiblingSymbolWidth );
690  // expand width, it may be wider because of label offsets
691  size.rwidth() = std::max( symbolComponent.size.width(), size.width() );
692  }
693  currentY += component.size.height();
694  first = false;
695  }
696  return size;
697 }
698 
699 QgsLegendRenderer::LegendComponent QgsLegendRenderer::drawSymbolItem( QgsLayerTreeModelLegendNode *symbolItem, QgsRenderContext &context, ColumnContext columnContext, double top, double maxSiblingSymbolWidth )
700 {
702  ctx.context = &context;
703 
704  // add a layer expression context scope
705  QgsExpressionContextScope *layerScope = nullptr;
706  if ( symbolItem->layerNode()->layer() )
707  {
708  layerScope = QgsExpressionContextUtils::layerScope( symbolItem->layerNode()->layer() );
709  context.expressionContext().appendScope( layerScope );
710  }
711 
712  ctx.painter = context.painter();
714  ctx.point = QPointF( columnContext.left, top );
715  ctx.labelXOffset = maxSiblingSymbolWidth;
717 
718  ctx.top = top;
719 
720  ctx.columnLeft = columnContext.left;
721  ctx.columnRight = columnContext.right;
722 
723  switch ( mSettings.symbolAlignment() )
724  {
725  case Qt::AlignLeft:
726  default:
728  break;
729 
730  case Qt::AlignRight:
732  break;
733  }
734 
735  ctx.maxSiblingSymbolWidth = maxSiblingSymbolWidth;
736 
737  if ( const QgsSymbolLegendNode *symbolNode = dynamic_cast< const QgsSymbolLegendNode * >( symbolItem ) )
738  ctx.patchShape = symbolNode->patchShape();
739 
740  ctx.patchSize = symbolItem->userPatchSize();
741 
742  QgsLayerTreeModelLegendNode::ItemMetrics im = symbolItem->draw( mSettings, &ctx );
743 
744  if ( layerScope )
745  delete context.expressionContext().popScope();
746 
747  LegendComponent component;
748  component.item = symbolItem;
749  component.symbolSize = im.symbolSize;
750  component.labelSize = im.labelSize;
751  //QgsDebugMsg( QStringLiteral( "symbol height = %1 label height = %2").arg( symbolSize.height()).arg( labelSize.height() ));
752  // NOTE -- we hard code left/right margins below, because those are the only ones exposed for use currently.
753  // ideally we could (should?) expose all these margins as settings, and then adapt the below to respect the current symbol/text alignment
754  // and consider the correct margin sides...
755  double width = std::max( static_cast< double >( im.symbolSize.width() ), maxSiblingSymbolWidth )
759  + im.labelSize.width();
760 
761  double height = std::max( im.symbolSize.height(), im.labelSize.height() );
762  component.size = QSizeF( width, height );
763  return component;
764 }
765 
766 QSizeF QgsLegendRenderer::drawLayerTitle( QgsLayerTreeLayer *nodeLayer, QgsRenderContext &context, ColumnContext columnContext, double top )
767 {
768  QSizeF size( 0, 0 );
769  QModelIndex idx = mLegendModel->node2index( nodeLayer );
770  QString titleString = mLegendModel->data( idx, Qt::DisplayRole ).toString();
771  //Let the user omit the layer title item by having an empty layer title string
772  if ( titleString.isEmpty() )
773  return size;
774 
775  double y = top;
776 
777  if ( auto *lPainter = context.painter() )
778  lPainter->setPen( mSettings.layerFontColor() );
779 
780  QFont layerFont = mSettings.style( nodeLegendStyle( nodeLayer ) ).font();
781 
782  QgsExpressionContextScope *layerScope = nullptr;
783  if ( nodeLayer->layer() )
784  {
785  layerScope = QgsExpressionContextUtils::layerScope( nodeLayer->layer() );
786  context.expressionContext().appendScope( layerScope );
787  }
788 
789  const QStringList lines = mSettings.evaluateItemText( titleString, context.expressionContext() );
790  int i = 0;
791 
792  const double sideMargin = mSettings.style( nodeLegendStyle( nodeLayer ) ).margin( QgsLegendStyle::Left );
793  for ( QStringList::ConstIterator layerItemPart = lines.constBegin(); layerItemPart != lines.constEnd(); ++layerItemPart )
794  {
795  y += mSettings.fontAscentMillimeters( layerFont );
796  if ( QPainter *destPainter = context.painter() )
797  {
798  double x = columnContext.left + sideMargin;
799  if ( mSettings.style( nodeLegendStyle( nodeLayer ) ).alignment() != Qt::AlignLeft )
800  {
801  const double labelWidth = mSettings.textWidthMillimeters( layerFont, *layerItemPart );
802  if ( mSettings.style( nodeLegendStyle( nodeLayer ) ).alignment() == Qt::AlignRight )
803  x = columnContext.right - labelWidth - sideMargin;
804  else if ( mSettings.style( nodeLegendStyle( nodeLayer ) ).alignment() == Qt::AlignHCenter )
805  x = columnContext.left + ( columnContext.right - columnContext.left - labelWidth ) / 2;
806  }
807  mSettings.drawText( destPainter, x, y, *layerItemPart, layerFont );
808  }
809  qreal width = mSettings.textWidthMillimeters( layerFont, *layerItemPart ) + sideMargin *
810  ( mSettings.style( nodeLegendStyle( nodeLayer ) ).alignment() == Qt::AlignHCenter ? 2 : 1 );
811  size.rwidth() = std::max( width, size.width() );
812  if ( layerItemPart != ( lines.end() - 1 ) )
813  {
814  y += mSettings.lineSpacing();
815  }
816  i++;
817  }
818  size.rheight() = y - top;
819  size.rheight() += mSettings.style( nodeLegendStyle( nodeLayer ) ).margin( QgsLegendStyle::Side::Bottom );
820 
821  if ( layerScope )
822  delete context.expressionContext().popScope();
823 
824  return size;
825 }
826 
827 QSizeF QgsLegendRenderer::drawGroupTitle( QgsLayerTreeGroup *nodeGroup, QgsRenderContext &context, ColumnContext columnContext, double top )
828 {
829  QSizeF size( 0, 0 );
830  QModelIndex idx = mLegendModel->node2index( nodeGroup );
831 
832  double y = top;
833 
834  if ( auto *lPainter = context.painter() )
835  lPainter->setPen( mSettings.fontColor() );
836 
837  QFont groupFont = mSettings.style( nodeLegendStyle( nodeGroup ) ).font();
838 
839  const double sideMargin = mSettings.style( nodeLegendStyle( nodeGroup ) ).margin( QgsLegendStyle::Left );
840 
841  const QStringList lines = mSettings.evaluateItemText( mLegendModel->data( idx, Qt::DisplayRole ).toString(), context.expressionContext() );
842  for ( QStringList::ConstIterator groupPart = lines.constBegin(); groupPart != lines.constEnd(); ++groupPart )
843  {
844  y += mSettings.fontAscentMillimeters( groupFont );
845 
846  if ( QPainter *destPainter = context.painter() )
847  {
848  double x = columnContext.left + sideMargin;
849  if ( mSettings.style( nodeLegendStyle( nodeGroup ) ).alignment() != Qt::AlignLeft )
850  {
851  const double labelWidth = mSettings.textWidthMillimeters( groupFont, *groupPart );
852  if ( mSettings.style( nodeLegendStyle( nodeGroup ) ).alignment() == Qt::AlignRight )
853  x = columnContext.right - labelWidth - sideMargin;
854  else if ( mSettings.style( nodeLegendStyle( nodeGroup ) ).alignment() == Qt::AlignHCenter )
855  x = columnContext.left + ( columnContext.right - columnContext.left - labelWidth ) / 2;
856  }
857  mSettings.drawText( destPainter, x, y, *groupPart, groupFont );
858  }
859  qreal width = mSettings.textWidthMillimeters( groupFont, *groupPart ) + sideMargin * ( mSettings.style( nodeLegendStyle( nodeGroup ) ).alignment() == Qt::AlignHCenter ? 2 : 1 );
860  size.rwidth() = std::max( width, size.width() );
861  if ( groupPart != ( lines.end() - 1 ) )
862  {
863  y += mSettings.lineSpacing();
864  }
865  }
866  size.rheight() = y - top + mSettings.style( nodeLegendStyle( nodeGroup ) ).margin( QgsLegendStyle::Bottom );
867  return size;
868 }
869 
871 {
872  QString style = node->customProperty( QStringLiteral( "legend/title-style" ) ).toString();
873  if ( style == QLatin1String( "hidden" ) )
874  return QgsLegendStyle::Hidden;
875  else if ( style == QLatin1String( "group" ) )
876  return QgsLegendStyle::Group;
877  else if ( style == QLatin1String( "subgroup" ) )
879 
880  // use a default otherwise
881  if ( QgsLayerTree::isGroup( node ) )
882  return QgsLegendStyle::Group;
883  else if ( QgsLayerTree::isLayer( node ) )
884  {
885  if ( model->legendNodeEmbeddedInParent( QgsLayerTree::toLayer( node ) ) )
886  return QgsLegendStyle::Hidden;
888  }
889 
890  return QgsLegendStyle::Undefined; // should not happen, only if corrupted project file
891 }
892 
894 {
895  return nodeLegendStyle( node, mLegendModel );
896 }
897 
899 {
900  QString str;
901  switch ( style )
902  {
904  str = QStringLiteral( "hidden" );
905  break;
907  str = QStringLiteral( "group" );
908  break;
910  str = QStringLiteral( "subgroup" );
911  break;
912  default:
913  break; // nothing
914  }
915 
916  if ( !str.isEmpty() )
917  node->setCustomProperty( QStringLiteral( "legend/title-style" ), str );
918  else
919  node->removeCustomProperty( QStringLiteral( "legend/title-style" ) );
920 }
921 
923 {
924  paintAndDetermineSize( context );
925 }
926 
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.
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 Q_NOWARN_DEPRECATED_POP
Definition: qgis.h:798
#define Q_NOWARN_DEPRECATED_PUSH
Definition: qgis.h:797
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.