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