Branch data Line data Source code
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"
20 : : #include "qgslayertreemodellegendnode.h"
21 : : #include "qgslegendstyle.h"
22 : : #include "qgsmaplayerlegend.h"
23 : : #include "qgssymbol.h"
24 : : #include "qgsrendercontext.h"
25 : : #include "qgsvectorlayer.h"
26 : : #include "qgsexpressioncontextutils.h"
27 : :
28 : : #include <QJsonObject>
29 : : #include <QPainter>
30 : :
31 : :
32 : :
33 : 0 : QgsLegendRenderer::QgsLegendRenderer( QgsLayerTreeModel *legendModel, const QgsLegendSettings &settings )
34 : 0 : : mLegendModel( legendModel )
35 : 0 : , mSettings( settings )
36 : : {
37 : 0 : }
38 : :
39 : 0 : QSizeF QgsLegendRenderer::minimumSize( QgsRenderContext *renderContext )
40 : : {
41 : 0 : std::unique_ptr< QgsRenderContext > tmpContext;
42 : :
43 : 0 : if ( !renderContext )
44 : : {
45 : : // QGIS 4.0 - make render context mandatory
46 : : Q_NOWARN_DEPRECATED_PUSH
47 : 0 : tmpContext.reset( new QgsRenderContext( QgsRenderContext::fromQPainter( nullptr ) ) );
48 : 0 : tmpContext->setRendererScale( mSettings.mapScale() );
49 : 0 : tmpContext->setMapToPixel( QgsMapToPixel( 1 / ( mSettings.mmPerMapUnit() * tmpContext->scaleFactor() ) ) );
50 : 0 : renderContext = tmpContext.get();
51 : : Q_NOWARN_DEPRECATED_POP
52 : 0 : }
53 : :
54 : 0 : QgsScopedRenderContextPainterSwap nullPainterSwap( *renderContext, nullptr );
55 : 0 : return paintAndDetermineSize( *renderContext );
56 : 0 : }
57 : :
58 : 0 : void QgsLegendRenderer::drawLegend( QPainter *painter )
59 : : {
60 : : Q_NOWARN_DEPRECATED_PUSH
61 : 0 : QgsRenderContext context = QgsRenderContext::fromQPainter( painter );
62 : 0 : QgsScopedRenderContextScaleToMm scaleToMm( context );
63 : :
64 : 0 : context.setRendererScale( mSettings.mapScale() );
65 : 0 : context.setMapToPixel( QgsMapToPixel( 1 / ( mSettings.mmPerMapUnit() * context.scaleFactor() ) ) );
66 : : Q_NOWARN_DEPRECATED_POP
67 : :
68 : 0 : paintAndDetermineSize( context );
69 : 0 : }
70 : :
71 : 0 : QJsonObject QgsLegendRenderer::exportLegendToJson( const QgsRenderContext &context )
72 : : {
73 : 0 : QJsonObject json;
74 : :
75 : 0 : QgsLayerTreeGroup *rootGroup = mLegendModel->rootGroup();
76 : 0 : if ( !rootGroup )
77 : 0 : return json;
78 : :
79 : 0 : json = exportLegendToJson( context, rootGroup );
80 : 0 : json[QStringLiteral( "title" )] = mSettings.title();
81 : 0 : return json;
82 : 0 : }
83 : :
84 : 0 : QJsonObject QgsLegendRenderer::exportLegendToJson( const QgsRenderContext &context, QgsLayerTreeGroup *nodeGroup )
85 : : {
86 : 0 : QJsonObject json;
87 : 0 : QJsonArray nodes;
88 : 0 : const QList<QgsLayerTreeNode *> childNodes = nodeGroup->children();
89 : 0 : for ( QgsLayerTreeNode *node : childNodes )
90 : : {
91 : 0 : if ( QgsLayerTree::isGroup( node ) )
92 : : {
93 : 0 : QgsLayerTreeGroup *nodeGroup = QgsLayerTree::toGroup( node );
94 : 0 : const QModelIndex idx = mLegendModel->node2index( nodeGroup );
95 : 0 : const QString text = mLegendModel->data( idx, Qt::DisplayRole ).toString();
96 : :
97 : 0 : QJsonObject group = exportLegendToJson( context, nodeGroup );
98 : 0 : group[ QStringLiteral( "type" ) ] = QStringLiteral( "group" );
99 : 0 : group[ QStringLiteral( "title" ) ] = text;
100 : 0 : nodes.append( group );
101 : 0 : }
102 : 0 : else if ( QgsLayerTree::isLayer( node ) )
103 : : {
104 : 0 : QgsLayerTreeLayer *nodeLayer = QgsLayerTree::toLayer( node );
105 : :
106 : 0 : QString text;
107 : 0 : if ( nodeLegendStyle( nodeLayer ) != QgsLegendStyle::Hidden )
108 : : {
109 : 0 : const QModelIndex idx = mLegendModel->node2index( nodeLayer );
110 : 0 : text = mLegendModel->data( idx, Qt::DisplayRole ).toString();
111 : 0 : }
112 : :
113 : 0 : QList<QgsLayerTreeModelLegendNode *> legendNodes = mLegendModel->layerLegendNodes( nodeLayer );
114 : :
115 : 0 : if ( legendNodes.isEmpty() && mLegendModel->legendFilterMapSettings() )
116 : 0 : continue;
117 : :
118 : 0 : if ( legendNodes.count() == 1 )
119 : : {
120 : 0 : QJsonObject group = legendNodes.at( 0 )->exportToJson( mSettings, context );
121 : 0 : group[ QStringLiteral( "type" ) ] = QStringLiteral( "layer" );
122 : 0 : nodes.append( group );
123 : 0 : }
124 : 0 : else if ( legendNodes.count() > 1 )
125 : : {
126 : 0 : QJsonObject group;
127 : 0 : group[ QStringLiteral( "type" ) ] = QStringLiteral( "layer" );
128 : 0 : group[ QStringLiteral( "title" ) ] = text;
129 : :
130 : 0 : QJsonArray symbols;
131 : 0 : for ( int j = 0; j < legendNodes.count(); j++ )
132 : : {
133 : 0 : QgsLayerTreeModelLegendNode *legendNode = legendNodes.at( j );
134 : 0 : QJsonObject symbol = legendNode->exportToJson( mSettings, context );
135 : 0 : symbols.append( symbol );
136 : 0 : }
137 : 0 : group[ QStringLiteral( "symbols" ) ] = symbols;
138 : :
139 : 0 : nodes.append( group );
140 : 0 : }
141 : 0 : }
142 : : }
143 : :
144 : 0 : json[QStringLiteral( "nodes" )] = nodes;
145 : 0 : return json;
146 : 0 : }
147 : :
148 : 0 : QSizeF QgsLegendRenderer::paintAndDetermineSize( QgsRenderContext &context )
149 : : {
150 : 0 : QSizeF size( 0, 0 );
151 : 0 : QgsLayerTreeGroup *rootGroup = mLegendModel->rootGroup();
152 : 0 : if ( !rootGroup )
153 : 0 : 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 : 0 : QgsScopedRenderContextPainterSwap noPainter( context, nullptr );
158 : :
159 : 0 : QList<LegendComponentGroup> componentGroups = createComponentGroupList( rootGroup, context );
160 : :
161 : 0 : const int columnCount = setColumns( componentGroups );
162 : :
163 : 0 : QMap< int, double > maxColumnWidths;
164 : 0 : 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 : 0 : for ( const LegendComponentGroup &group : std::as_const( componentGroups ) )
173 : : {
174 : 0 : const QSizeF actualSize = drawGroup( group, context, ColumnContext() );
175 : 0 : maxEqualColumnWidth = std::max( actualSize.width(), maxEqualColumnWidth );
176 : 0 : maxColumnWidths[ group.column ] = std::max( actualSize.width(), maxColumnWidths.value( group.column, 0 ) );
177 : : }
178 : :
179 : 0 : if ( columnCount == 1 )
180 : : {
181 : : // single column - use the full available width
182 : 0 : maxEqualColumnWidth = std::max( maxEqualColumnWidth, mLegendSize.width() - 2 * mSettings.boxSpace() );
183 : 0 : maxColumnWidths[ 0 ] = maxEqualColumnWidth;
184 : 0 : }
185 : :
186 : : //calculate size of title
187 : 0 : QSizeF titleSize = drawTitle( context, 0 );
188 : : //add title margin to size of title text
189 : 0 : titleSize.rwidth() += mSettings.boxSpace() * 2.0;
190 : 0 : double columnTop = mSettings.boxSpace() + titleSize.height() + mSettings.style( QgsLegendStyle::Title ).margin( QgsLegendStyle::Bottom );
191 : :
192 : 0 : noPainter.reset();
193 : :
194 : 0 : bool firstInColumn = true;
195 : 0 : double columnMaxHeight = 0;
196 : 0 : qreal columnWidth = 0;
197 : 0 : int column = -1;
198 : 0 : ColumnContext columnContext;
199 : 0 : columnContext.left = mSettings.boxSpace();
200 : 0 : columnContext.right = std::max( mLegendSize.width() - mSettings.boxSpace(), mSettings.boxSpace() );
201 : 0 : double currentY = columnTop;
202 : :
203 : 0 : for ( const LegendComponentGroup &group : std::as_const( componentGroups ) )
204 : : {
205 : 0 : if ( group.column > column )
206 : : {
207 : : // Switch to next column
208 : 0 : columnContext.left = group.column > 0 ? columnContext.right + mSettings.columnSpace() : mSettings.boxSpace();
209 : 0 : columnWidth = mSettings.equalColumnWidth() ? maxEqualColumnWidth : maxColumnWidths.value( group.column );
210 : 0 : columnContext.right = columnContext.left + columnWidth;
211 : 0 : currentY = columnTop;
212 : 0 : column++;
213 : 0 : firstInColumn = true;
214 : 0 : }
215 : 0 : if ( !firstInColumn )
216 : : {
217 : 0 : currentY += spaceAboveGroup( group );
218 : 0 : }
219 : :
220 : 0 : drawGroup( group, context, columnContext, currentY );
221 : :
222 : 0 : currentY += group.size.height();
223 : 0 : columnMaxHeight = std::max( currentY - columnTop, columnMaxHeight );
224 : :
225 : 0 : firstInColumn = false;
226 : : }
227 : 0 : const double totalWidth = columnContext.right + mSettings.boxSpace();
228 : :
229 : 0 : size.rheight() = columnTop + columnMaxHeight + mSettings.boxSpace();
230 : 0 : size.rwidth() = totalWidth;
231 : 0 : if ( !mSettings.title().isEmpty() )
232 : : {
233 : 0 : size.rwidth() = std::max( titleSize.width(), size.width() );
234 : 0 : }
235 : :
236 : : // override the size if it was set by the user
237 : 0 : if ( mLegendSize.isValid() )
238 : : {
239 : 0 : qreal w = std::max( size.width(), mLegendSize.width() );
240 : 0 : qreal h = std::max( size.height(), mLegendSize.height() );
241 : 0 : size = QSizeF( w, h );
242 : 0 : }
243 : :
244 : : // Now we have set the correct total item width and can draw the title centered
245 : 0 : if ( !mSettings.title().isEmpty() )
246 : : {
247 : 0 : drawTitle( context, mSettings.boxSpace(), mSettings.titleAlignment(), size.width() );
248 : 0 : }
249 : :
250 : : return size;
251 : 0 : }
252 : :
253 : 0 : void QgsLegendRenderer::widthAndOffsetForTitleText( const Qt::AlignmentFlag halignment, const double legendWidth, double &textBoxWidth, double &textBoxLeft )
254 : : {
255 : 0 : switch ( halignment )
256 : : {
257 : : default:
258 : 0 : textBoxLeft = mSettings.boxSpace();
259 : 0 : textBoxWidth = legendWidth - 2 * mSettings.boxSpace();
260 : 0 : 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 : 0 : const double centerX = legendWidth / 2;
266 : 0 : textBoxWidth = ( std::min( static_cast< double >( centerX ), legendWidth - centerX ) - mSettings.boxSpace() ) * 2.0;
267 : 0 : textBoxLeft = centerX - textBoxWidth / 2.;
268 : 0 : break;
269 : : }
270 : : }
271 : 0 : }
272 : :
273 : 0 : QList<QgsLegendRenderer::LegendComponentGroup> QgsLegendRenderer::createComponentGroupList( QgsLayerTreeGroup *parentGroup, QgsRenderContext &context )
274 : : {
275 : 0 : QList<LegendComponentGroup> componentGroups;
276 : :
277 : 0 : if ( !parentGroup )
278 : 0 : return componentGroups;
279 : :
280 : 0 : const QList<QgsLayerTreeNode *> childNodes = parentGroup->children();
281 : 0 : for ( QgsLayerTreeNode *node : childNodes )
282 : : {
283 : 0 : if ( QgsLayerTree::isGroup( node ) )
284 : : {
285 : 0 : QgsLayerTreeGroup *nodeGroup = QgsLayerTree::toGroup( node );
286 : :
287 : : // Group subitems
288 : 0 : QList<LegendComponentGroup> subgroups = createComponentGroupList( nodeGroup, context );
289 : 0 : bool hasSubItems = !subgroups.empty();
290 : :
291 : 0 : if ( nodeLegendStyle( nodeGroup ) != QgsLegendStyle::Hidden )
292 : : {
293 : 0 : LegendComponent component;
294 : 0 : component.item = node;
295 : 0 : component.size = drawGroupTitle( nodeGroup, context );
296 : :
297 : 0 : if ( !subgroups.isEmpty() )
298 : : {
299 : : // Add internal space between this group title and the next component
300 : 0 : subgroups[0].size.rheight() += spaceAboveGroup( subgroups[0] );
301 : : // Prepend this group title to the first group
302 : 0 : subgroups[0].components.prepend( component );
303 : 0 : subgroups[0].size.rheight() += component.size.height();
304 : 0 : subgroups[0].size.rwidth() = std::max( component.size.width(), subgroups[0].size.width() );
305 : 0 : if ( nodeGroup->customProperty( QStringLiteral( "legend/column-break" ) ).toInt() )
306 : 0 : subgroups[0].placeColumnBreakBeforeGroup = true;
307 : 0 : }
308 : : else
309 : : {
310 : : // no subitems, create new group
311 : 0 : LegendComponentGroup group;
312 : 0 : group.placeColumnBreakBeforeGroup = nodeGroup->customProperty( QStringLiteral( "legend/column-break" ) ).toInt();
313 : 0 : group.components.append( component );
314 : 0 : group.size.rwidth() += component.size.width();
315 : 0 : group.size.rheight() += component.size.height();
316 : 0 : group.size.rwidth() = std::max( component.size.width(), group.size.width() );
317 : 0 : subgroups.append( group );
318 : 0 : }
319 : 0 : }
320 : :
321 : 0 : if ( hasSubItems ) //leave away groups without content
322 : : {
323 : 0 : componentGroups.append( subgroups );
324 : 0 : }
325 : :
326 : 0 : }
327 : 0 : else if ( QgsLayerTree::isLayer( node ) )
328 : : {
329 : 0 : QgsLayerTreeLayer *nodeLayer = QgsLayerTree::toLayer( node );
330 : :
331 : 0 : bool allowColumnSplit = false;
332 : 0 : switch ( nodeLayer->legendSplitBehavior() )
333 : : {
334 : : case QgsLayerTreeLayer::UseDefaultLegendSetting:
335 : 0 : allowColumnSplit = mSettings.splitLayer();
336 : 0 : break;
337 : : case QgsLayerTreeLayer::AllowSplittingLegendNodesOverMultipleColumns:
338 : 0 : allowColumnSplit = true;
339 : 0 : break;
340 : : case QgsLayerTreeLayer::PreventSplittingLegendNodesOverMultipleColumns:
341 : 0 : allowColumnSplit = false;
342 : 0 : break;
343 : : }
344 : :
345 : 0 : LegendComponentGroup group;
346 : 0 : group.placeColumnBreakBeforeGroup = nodeLayer->customProperty( QStringLiteral( "legend/column-break" ) ).toInt();
347 : :
348 : 0 : if ( nodeLegendStyle( nodeLayer ) != QgsLegendStyle::Hidden )
349 : : {
350 : 0 : LegendComponent component;
351 : 0 : component.item = node;
352 : 0 : component.size = drawLayerTitle( nodeLayer, context );
353 : 0 : group.components.append( component );
354 : 0 : group.size.rwidth() = component.size.width();
355 : 0 : group.size.rheight() = component.size.height();
356 : 0 : }
357 : :
358 : 0 : 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 : 0 : if ( legendNodes.isEmpty() && mLegendModel->legendFilterMapSettings() )
364 : 0 : continue;
365 : :
366 : 0 : QList<LegendComponentGroup> layerGroups;
367 : 0 : layerGroups.reserve( legendNodes.count() );
368 : :
369 : 0 : bool groupIsLayerGroup = true;
370 : :
371 : 0 : for ( int j = 0; j < legendNodes.count(); j++ )
372 : : {
373 : 0 : QgsLayerTreeModelLegendNode *legendNode = legendNodes.at( j );
374 : :
375 : 0 : LegendComponent symbolComponent = drawSymbolItem( legendNode, context, ColumnContext(), 0 );
376 : :
377 : 0 : const bool forceBreak = legendNode->columnBreak();
378 : :
379 : 0 : if ( !allowColumnSplit || j == 0 )
380 : : {
381 : 0 : if ( forceBreak )
382 : : {
383 : 0 : if ( groupIsLayerGroup )
384 : 0 : layerGroups.prepend( group );
385 : : else
386 : 0 : layerGroups.append( group );
387 : :
388 : 0 : group = LegendComponentGroup();
389 : 0 : group.placeColumnBreakBeforeGroup = true;
390 : 0 : groupIsLayerGroup = false;
391 : 0 : }
392 : :
393 : : // append to layer group
394 : : // the width is not correct at this moment, we must align all symbol labels
395 : 0 : 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 : 0 : if ( !group.components.isEmpty() )
398 : : {
399 : : // TODO: for now we keep Symbol and SymbolLabel Top margin in sync
400 : 0 : group.size.rheight() += mSettings.style( QgsLegendStyle::Symbol ).margin( QgsLegendStyle::Top );
401 : 0 : }
402 : 0 : group.size.rheight() += symbolComponent.size.height();
403 : 0 : group.components.append( symbolComponent );
404 : 0 : }
405 : : else
406 : : {
407 : 0 : if ( group.size.height() > 0 )
408 : : {
409 : 0 : if ( groupIsLayerGroup )
410 : 0 : layerGroups.prepend( group );
411 : : else
412 : 0 : layerGroups.append( group );
413 : 0 : group = LegendComponentGroup();
414 : 0 : groupIsLayerGroup = false;
415 : 0 : }
416 : 0 : LegendComponentGroup symbolGroup;
417 : 0 : symbolGroup.placeColumnBreakBeforeGroup = forceBreak;
418 : 0 : symbolGroup.components.append( symbolComponent );
419 : 0 : symbolGroup.size.rwidth() = symbolComponent.size.width();
420 : 0 : symbolGroup.size.rheight() = symbolComponent.size.height();
421 : 0 : layerGroups.append( symbolGroup );
422 : 0 : }
423 : 0 : }
424 : 0 : if ( group.size.height() > 0 )
425 : : {
426 : 0 : if ( groupIsLayerGroup )
427 : 0 : layerGroups.prepend( group );
428 : : else
429 : 0 : layerGroups.append( group );
430 : 0 : }
431 : 0 : componentGroups.append( layerGroups );
432 : 0 : }
433 : : }
434 : :
435 : 0 : return componentGroups;
436 : 0 : }
437 : :
438 : :
439 : 0 : int QgsLegendRenderer::setColumns( QList<LegendComponentGroup> &componentGroups )
440 : : {
441 : : // Divide groups to columns
442 : 0 : double totalHeight = 0;
443 : 0 : qreal maxGroupHeight = 0;
444 : 0 : int forcedColumnBreaks = 0;
445 : 0 : double totalSpaceAboveGroups = 0;
446 : 0 : for ( const LegendComponentGroup &group : std::as_const( componentGroups ) )
447 : : {
448 : 0 : totalHeight += spaceAboveGroup( group );
449 : 0 : totalSpaceAboveGroups += spaceAboveGroup( group );
450 : 0 : totalHeight += group.size.height();
451 : 0 : maxGroupHeight = std::max( group.size.height(), maxGroupHeight );
452 : :
453 : 0 : if ( group.placeColumnBreakBeforeGroup )
454 : 0 : forcedColumnBreaks++;
455 : : }
456 : 0 : double averageGroupHeight = ( totalHeight - totalSpaceAboveGroups ) / componentGroups.size();
457 : :
458 : 0 : if ( mSettings.columnCount() == 0 && forcedColumnBreaks == 0 )
459 : 0 : 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 : 0 : const int targetNumberColumns = std::max( forcedColumnBreaks + 1, mSettings.columnCount() );
464 : 0 : 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 : 0 : double maxColumnHeight = 0;
472 : 0 : int currentColumn = 0;
473 : 0 : int currentColumnGroupCount = 0; // number of groups in current column
474 : 0 : double currentColumnHeight = 0;
475 : 0 : double closedColumnsHeight = 0;
476 : 0 : int autoPlacedBreaks = 0;
477 : :
478 : : // Calculate the expected average space between items
479 : 0 : double averageSpaceAboveGroups = 0;
480 : 0 : if ( componentGroups.size() > targetNumberColumns )
481 : 0 : 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 : 0 : totalHeight -= targetNumberColumns * averageSpaceAboveGroups;
485 : :
486 : 0 : for ( int i = 0; i < componentGroups.size(); i++ )
487 : : {
488 : 0 : LegendComponentGroup group = componentGroups.at( i );
489 : 0 : double currentHeight = currentColumnHeight;
490 : 0 : if ( currentColumnGroupCount > 0 )
491 : 0 : currentHeight += spaceAboveGroup( group );
492 : 0 : currentHeight += group.size.height();
493 : :
494 : 0 : int numberRemainingGroups = componentGroups.size() - i;
495 : :
496 : : // Recalc average height for remaining columns including current
497 : 0 : int numberRemainingColumns = numberAutoPlacedBreaks + 1 - autoPlacedBreaks;
498 : 0 : 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 : 0 : int averageGroupsPerColumn = std::ceil( avgColumnHeight / ( averageGroupHeight + averageSpaceAboveGroups ) );
502 : 0 : avgColumnHeight = averageGroupsPerColumn * ( averageGroupHeight + averageSpaceAboveGroups ) - averageSpaceAboveGroups;
503 : :
504 : 0 : bool canCreateNewColumn = ( currentColumnGroupCount > 0 ) // do not leave empty column
505 : 0 : && ( currentColumn < targetNumberColumns - 1 ) // must not exceed max number of columns
506 : 0 : && ( autoPlacedBreaks < numberAutoPlacedBreaks );
507 : :
508 : 0 : bool shouldCreateNewColumn = currentHeight > avgColumnHeight // current group height is greater than expected group height
509 : 0 : && currentColumnGroupCount > 0 // do not leave empty column
510 : 0 : && currentHeight > maxGroupHeight // no sense to make smaller columns than max group height
511 : 0 : && currentHeight > maxColumnHeight; // no sense to make smaller columns than max column already created
512 : :
513 : 0 : shouldCreateNewColumn |= group.placeColumnBreakBeforeGroup;
514 : 0 : 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 : 0 : shouldCreateNewColumn |= ( componentGroups.size() - i < targetNumberColumns - currentColumn );
519 : :
520 : 0 : if ( canCreateNewColumn && shouldCreateNewColumn )
521 : : {
522 : : // New column
523 : 0 : currentColumn++;
524 : 0 : if ( !group.placeColumnBreakBeforeGroup )
525 : 0 : autoPlacedBreaks++;
526 : 0 : currentColumnGroupCount = 0;
527 : 0 : closedColumnsHeight += currentColumnHeight;
528 : 0 : currentColumnHeight = group.size.height();
529 : 0 : }
530 : : else
531 : : {
532 : 0 : currentColumnHeight = currentHeight;
533 : : }
534 : 0 : componentGroups[i].column = currentColumn;
535 : 0 : currentColumnGroupCount++;
536 : 0 : maxColumnHeight = std::max( currentColumnHeight, maxColumnHeight );
537 : 0 : }
538 : :
539 : : // Align labels of symbols for each layer/column to the same labelXOffset
540 : 0 : QMap<QString, qreal> maxSymbolWidth;
541 : 0 : for ( int i = 0; i < componentGroups.size(); i++ )
542 : : {
543 : 0 : LegendComponentGroup &group = componentGroups[i];
544 : 0 : for ( int j = 0; j < group.components.size(); j++ )
545 : : {
546 : 0 : if ( QgsLayerTreeModelLegendNode *legendNode = qobject_cast<QgsLayerTreeModelLegendNode *>( group.components.at( j ).item ) )
547 : : {
548 : 0 : QString key = QStringLiteral( "%1-%2" ).arg( reinterpret_cast< qulonglong >( legendNode->layerNode() ) ).arg( group.column );
549 : 0 : maxSymbolWidth[key] = std::max( group.components.at( j ).symbolSize.width(), maxSymbolWidth[key] );
550 : 0 : }
551 : 0 : }
552 : 0 : }
553 : 0 : for ( int i = 0; i < componentGroups.size(); i++ )
554 : : {
555 : 0 : LegendComponentGroup &group = componentGroups[i];
556 : 0 : for ( int j = 0; j < group.components.size(); j++ )
557 : : {
558 : 0 : if ( QgsLayerTreeModelLegendNode *legendNode = qobject_cast<QgsLayerTreeModelLegendNode *>( group.components.at( j ).item ) )
559 : : {
560 : 0 : QString key = QStringLiteral( "%1-%2" ).arg( reinterpret_cast< qulonglong >( legendNode->layerNode() ) ).arg( group.column );
561 : 0 : double space = mSettings.style( QgsLegendStyle::Symbol ).margin( QgsLegendStyle::Right ) +
562 : 0 : mSettings.style( QgsLegendStyle::SymbolLabel ).margin( QgsLegendStyle::Left );
563 : 0 : group.components[j].labelXOffset = maxSymbolWidth[key] + space;
564 : 0 : group.components[j].maxSiblingSymbolWidth = maxSymbolWidth[key];
565 : 0 : group.components[j].size.rwidth() = maxSymbolWidth[key] + space + group.components.at( j ).labelSize.width();
566 : 0 : }
567 : 0 : }
568 : 0 : }
569 : 0 : return targetNumberColumns;
570 : 0 : }
571 : :
572 : 0 : QSizeF QgsLegendRenderer::drawTitle( QgsRenderContext &context, double top, Qt::AlignmentFlag halignment, double legendWidth )
573 : : {
574 : 0 : QSizeF size( 0, 0 );
575 : 0 : if ( mSettings.title().isEmpty() )
576 : : {
577 : 0 : return size;
578 : : }
579 : :
580 : 0 : QStringList lines = mSettings.splitStringForWrapping( mSettings.title() );
581 : 0 : double y = top;
582 : :
583 : 0 : if ( auto *lPainter = context.painter() )
584 : : {
585 : 0 : lPainter->setPen( mSettings.fontColor() );
586 : 0 : }
587 : :
588 : : //calculate width and left pos of rectangle to draw text into
589 : : double textBoxWidth;
590 : : double textBoxLeft;
591 : 0 : widthAndOffsetForTitleText( halignment, legendWidth, textBoxWidth, textBoxLeft );
592 : :
593 : 0 : QFont titleFont = mSettings.style( QgsLegendStyle::Title ).font();
594 : :
595 : 0 : 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 : 0 : qreal width = mSettings.textWidthMillimeters( titleFont, *titlePart ) + 1;
600 : 0 : qreal height = mSettings.fontAscentMillimeters( titleFont ) + mSettings.fontDescentMillimeters( titleFont );
601 : :
602 : 0 : QRectF r( textBoxLeft, y, textBoxWidth, height );
603 : :
604 : 0 : if ( context.painter() )
605 : : {
606 : 0 : mSettings.drawText( context.painter(), r, *titlePart, titleFont, halignment, Qt::AlignVCenter, Qt::TextDontClip );
607 : 0 : }
608 : :
609 : : //update max width of title
610 : 0 : size.rwidth() = std::max( width, size.rwidth() );
611 : :
612 : 0 : y += height;
613 : 0 : if ( titlePart != ( lines.end() - 1 ) )
614 : : {
615 : 0 : y += mSettings.lineSpacing();
616 : 0 : }
617 : 0 : }
618 : 0 : size.rheight() = y - top;
619 : :
620 : : return size;
621 : 0 : }
622 : :
623 : :
624 : 0 : double QgsLegendRenderer::spaceAboveGroup( const LegendComponentGroup &group )
625 : : {
626 : 0 : if ( group.components.isEmpty() ) return 0;
627 : :
628 : 0 : LegendComponent component = group.components.first();
629 : :
630 : 0 : if ( QgsLayerTreeGroup *nodeGroup = qobject_cast<QgsLayerTreeGroup *>( component.item ) )
631 : : {
632 : 0 : return mSettings.style( nodeLegendStyle( nodeGroup ) ).margin( QgsLegendStyle::Top );
633 : : }
634 : 0 : else if ( QgsLayerTreeLayer *nodeLayer = qobject_cast<QgsLayerTreeLayer *>( component.item ) )
635 : : {
636 : 0 : return mSettings.style( nodeLegendStyle( nodeLayer ) ).margin( QgsLegendStyle::Top );
637 : : }
638 : 0 : else if ( qobject_cast<QgsLayerTreeModelLegendNode *>( component.item ) )
639 : : {
640 : : // TODO: use Symbol or SymbolLabel Top margin
641 : 0 : return mSettings.style( QgsLegendStyle::Symbol ).margin( QgsLegendStyle::Top );
642 : : }
643 : :
644 : 0 : return 0;
645 : 0 : }
646 : :
647 : 0 : QSizeF QgsLegendRenderer::drawGroup( const LegendComponentGroup &group, QgsRenderContext &context, ColumnContext columnContext, double top )
648 : : {
649 : 0 : bool first = true;
650 : 0 : QSizeF size = QSizeF( group.size );
651 : 0 : double currentY = top;
652 : 0 : for ( const LegendComponent &component : std::as_const( group.components ) )
653 : : {
654 : 0 : if ( QgsLayerTreeGroup *groupItem = qobject_cast<QgsLayerTreeGroup *>( component.item ) )
655 : : {
656 : 0 : QgsLegendStyle::Style s = nodeLegendStyle( groupItem );
657 : 0 : if ( s != QgsLegendStyle::Hidden )
658 : : {
659 : 0 : if ( !first )
660 : : {
661 : 0 : currentY += mSettings.style( s ).margin( QgsLegendStyle::Top );
662 : 0 : }
663 : 0 : QSizeF groupSize;
664 : 0 : groupSize = drawGroupTitle( groupItem, context, columnContext, currentY );
665 : 0 : size.rwidth() = std::max( groupSize.width(), size.width() );
666 : 0 : }
667 : 0 : }
668 : 0 : else if ( QgsLayerTreeLayer *layerItem = qobject_cast<QgsLayerTreeLayer *>( component.item ) )
669 : : {
670 : 0 : QgsLegendStyle::Style s = nodeLegendStyle( layerItem );
671 : 0 : if ( s != QgsLegendStyle::Hidden )
672 : : {
673 : 0 : if ( !first )
674 : : {
675 : 0 : currentY += mSettings.style( s ).margin( QgsLegendStyle::Top );
676 : 0 : }
677 : 0 : QSizeF subGroupSize;
678 : 0 : subGroupSize = drawLayerTitle( layerItem, context, columnContext, currentY );
679 : 0 : size.rwidth() = std::max( subGroupSize.width(), size.width() );
680 : 0 : }
681 : 0 : }
682 : 0 : else if ( QgsLayerTreeModelLegendNode *legendNode = qobject_cast<QgsLayerTreeModelLegendNode *>( component.item ) )
683 : : {
684 : 0 : if ( !first )
685 : : {
686 : 0 : currentY += mSettings.style( QgsLegendStyle::Symbol ).margin( QgsLegendStyle::Top );
687 : 0 : }
688 : :
689 : 0 : LegendComponent symbolComponent = drawSymbolItem( legendNode, context, columnContext, currentY, component.maxSiblingSymbolWidth );
690 : : // expand width, it may be wider because of label offsets
691 : 0 : size.rwidth() = std::max( symbolComponent.size.width(), size.width() );
692 : 0 : }
693 : 0 : currentY += component.size.height();
694 : 0 : first = false;
695 : : }
696 : 0 : return size;
697 : 0 : }
698 : :
699 : 0 : QgsLegendRenderer::LegendComponent QgsLegendRenderer::drawSymbolItem( QgsLayerTreeModelLegendNode *symbolItem, QgsRenderContext &context, ColumnContext columnContext, double top, double maxSiblingSymbolWidth )
700 : : {
701 : 0 : QgsLayerTreeModelLegendNode::ItemContext ctx;
702 : 0 : ctx.context = &context;
703 : :
704 : : // add a layer expression context scope
705 : 0 : QgsExpressionContextScope *layerScope = nullptr;
706 : 0 : if ( symbolItem->layerNode()->layer() )
707 : : {
708 : 0 : layerScope = QgsExpressionContextUtils::layerScope( symbolItem->layerNode()->layer() );
709 : 0 : context.expressionContext().appendScope( layerScope );
710 : 0 : }
711 : :
712 : 0 : ctx.painter = context.painter();
713 : : Q_NOWARN_DEPRECATED_PUSH
714 : 0 : ctx.point = QPointF( columnContext.left, top );
715 : 0 : ctx.labelXOffset = maxSiblingSymbolWidth;
716 : : Q_NOWARN_DEPRECATED_POP
717 : :
718 : 0 : ctx.top = top;
719 : :
720 : 0 : ctx.columnLeft = columnContext.left;
721 : 0 : ctx.columnRight = columnContext.right;
722 : :
723 : 0 : switch ( mSettings.symbolAlignment() )
724 : 0 : {
725 : : case Qt::AlignLeft:
726 : : default:
727 : 0 : ctx.columnLeft += mSettings.style( QgsLegendStyle::Symbol ).margin( QgsLegendStyle::Left );
728 : 0 : break;
729 : :
730 : : case Qt::AlignRight:
731 : 0 : ctx.columnRight -= mSettings.style( QgsLegendStyle::Symbol ).margin( QgsLegendStyle::Left );
732 : 0 : break;
733 : : }
734 : :
735 : 0 : ctx.maxSiblingSymbolWidth = maxSiblingSymbolWidth;
736 : :
737 : 0 : if ( const QgsSymbolLegendNode *symbolNode = dynamic_cast< const QgsSymbolLegendNode * >( symbolItem ) )
738 : 0 : ctx.patchShape = symbolNode->patchShape();
739 : :
740 : 0 : ctx.patchSize = symbolItem->userPatchSize();
741 : :
742 : 0 : QgsLayerTreeModelLegendNode::ItemMetrics im = symbolItem->draw( mSettings, &ctx );
743 : :
744 : 0 : if ( layerScope )
745 : 0 : delete context.expressionContext().popScope();
746 : :
747 : 0 : LegendComponent component;
748 : 0 : component.item = symbolItem;
749 : 0 : component.symbolSize = im.symbolSize;
750 : 0 : 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 : 0 : double width = std::max( static_cast< double >( im.symbolSize.width() ), maxSiblingSymbolWidth )
756 : 0 : + mSettings.style( QgsLegendStyle::Symbol ).margin( QgsLegendStyle::Left )
757 : 0 : + mSettings.style( QgsLegendStyle::Symbol ).margin( QgsLegendStyle::Right )
758 : 0 : + mSettings.style( QgsLegendStyle::SymbolLabel ).margin( QgsLegendStyle::Left )
759 : 0 : + im.labelSize.width();
760 : :
761 : 0 : double height = std::max( im.symbolSize.height(), im.labelSize.height() );
762 : 0 : component.size = QSizeF( width, height );
763 : : return component;
764 : 0 : }
765 : :
766 : 0 : QSizeF QgsLegendRenderer::drawLayerTitle( QgsLayerTreeLayer *nodeLayer, QgsRenderContext &context, ColumnContext columnContext, double top )
767 : : {
768 : 0 : QSizeF size( 0, 0 );
769 : 0 : QModelIndex idx = mLegendModel->node2index( nodeLayer );
770 : 0 : 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 : 0 : if ( titleString.isEmpty() )
773 : 0 : return size;
774 : :
775 : 0 : double y = top;
776 : :
777 : 0 : if ( auto *lPainter = context.painter() )
778 : 0 : lPainter->setPen( mSettings.layerFontColor() );
779 : :
780 : 0 : QFont layerFont = mSettings.style( nodeLegendStyle( nodeLayer ) ).font();
781 : :
782 : 0 : QgsExpressionContextScope *layerScope = nullptr;
783 : 0 : if ( nodeLayer->layer() )
784 : : {
785 : 0 : layerScope = QgsExpressionContextUtils::layerScope( nodeLayer->layer() );
786 : 0 : context.expressionContext().appendScope( layerScope );
787 : 0 : }
788 : :
789 : 0 : const QStringList lines = mSettings.evaluateItemText( titleString, context.expressionContext() );
790 : 0 : int i = 0;
791 : :
792 : 0 : const double sideMargin = mSettings.style( nodeLegendStyle( nodeLayer ) ).margin( QgsLegendStyle::Left );
793 : 0 : for ( QStringList::ConstIterator layerItemPart = lines.constBegin(); layerItemPart != lines.constEnd(); ++layerItemPart )
794 : : {
795 : 0 : y += mSettings.fontAscentMillimeters( layerFont );
796 : 0 : if ( QPainter *destPainter = context.painter() )
797 : : {
798 : 0 : double x = columnContext.left + sideMargin;
799 : 0 : if ( mSettings.style( nodeLegendStyle( nodeLayer ) ).alignment() != Qt::AlignLeft )
800 : : {
801 : 0 : const double labelWidth = mSettings.textWidthMillimeters( layerFont, *layerItemPart );
802 : 0 : if ( mSettings.style( nodeLegendStyle( nodeLayer ) ).alignment() == Qt::AlignRight )
803 : 0 : x = columnContext.right - labelWidth - sideMargin;
804 : 0 : else if ( mSettings.style( nodeLegendStyle( nodeLayer ) ).alignment() == Qt::AlignHCenter )
805 : 0 : x = columnContext.left + ( columnContext.right - columnContext.left - labelWidth ) / 2;
806 : 0 : }
807 : 0 : mSettings.drawText( destPainter, x, y, *layerItemPart, layerFont );
808 : 0 : }
809 : 0 : qreal width = mSettings.textWidthMillimeters( layerFont, *layerItemPart ) + sideMargin *
810 : 0 : ( mSettings.style( nodeLegendStyle( nodeLayer ) ).alignment() == Qt::AlignHCenter ? 2 : 1 );
811 : 0 : size.rwidth() = std::max( width, size.width() );
812 : 0 : if ( layerItemPart != ( lines.end() - 1 ) )
813 : : {
814 : 0 : y += mSettings.lineSpacing();
815 : 0 : }
816 : 0 : i++;
817 : 0 : }
818 : 0 : size.rheight() = y - top;
819 : 0 : size.rheight() += mSettings.style( nodeLegendStyle( nodeLayer ) ).margin( QgsLegendStyle::Side::Bottom );
820 : :
821 : 0 : if ( layerScope )
822 : 0 : delete context.expressionContext().popScope();
823 : :
824 : : return size;
825 : 0 : }
826 : :
827 : 0 : QSizeF QgsLegendRenderer::drawGroupTitle( QgsLayerTreeGroup *nodeGroup, QgsRenderContext &context, ColumnContext columnContext, double top )
828 : : {
829 : 0 : QSizeF size( 0, 0 );
830 : 0 : QModelIndex idx = mLegendModel->node2index( nodeGroup );
831 : :
832 : 0 : double y = top;
833 : :
834 : 0 : if ( auto *lPainter = context.painter() )
835 : 0 : lPainter->setPen( mSettings.fontColor() );
836 : :
837 : 0 : QFont groupFont = mSettings.style( nodeLegendStyle( nodeGroup ) ).font();
838 : :
839 : 0 : const double sideMargin = mSettings.style( nodeLegendStyle( nodeGroup ) ).margin( QgsLegendStyle::Left );
840 : :
841 : 0 : const QStringList lines = mSettings.evaluateItemText( mLegendModel->data( idx, Qt::DisplayRole ).toString(), context.expressionContext() );
842 : 0 : for ( QStringList::ConstIterator groupPart = lines.constBegin(); groupPart != lines.constEnd(); ++groupPart )
843 : : {
844 : 0 : y += mSettings.fontAscentMillimeters( groupFont );
845 : :
846 : 0 : if ( QPainter *destPainter = context.painter() )
847 : : {
848 : 0 : double x = columnContext.left + sideMargin;
849 : 0 : if ( mSettings.style( nodeLegendStyle( nodeGroup ) ).alignment() != Qt::AlignLeft )
850 : : {
851 : 0 : const double labelWidth = mSettings.textWidthMillimeters( groupFont, *groupPart );
852 : 0 : if ( mSettings.style( nodeLegendStyle( nodeGroup ) ).alignment() == Qt::AlignRight )
853 : 0 : x = columnContext.right - labelWidth - sideMargin;
854 : 0 : else if ( mSettings.style( nodeLegendStyle( nodeGroup ) ).alignment() == Qt::AlignHCenter )
855 : 0 : x = columnContext.left + ( columnContext.right - columnContext.left - labelWidth ) / 2;
856 : 0 : }
857 : 0 : mSettings.drawText( destPainter, x, y, *groupPart, groupFont );
858 : 0 : }
859 : 0 : qreal width = mSettings.textWidthMillimeters( groupFont, *groupPart ) + sideMargin * ( mSettings.style( nodeLegendStyle( nodeGroup ) ).alignment() == Qt::AlignHCenter ? 2 : 1 );
860 : 0 : size.rwidth() = std::max( width, size.width() );
861 : 0 : if ( groupPart != ( lines.end() - 1 ) )
862 : : {
863 : 0 : y += mSettings.lineSpacing();
864 : 0 : }
865 : 0 : }
866 : 0 : size.rheight() = y - top + mSettings.style( nodeLegendStyle( nodeGroup ) ).margin( QgsLegendStyle::Bottom );
867 : : return size;
868 : 0 : }
869 : :
870 : 0 : QgsLegendStyle::Style QgsLegendRenderer::nodeLegendStyle( QgsLayerTreeNode *node, QgsLayerTreeModel *model )
871 : : {
872 : 0 : QString style = node->customProperty( QStringLiteral( "legend/title-style" ) ).toString();
873 : 0 : if ( style == QLatin1String( "hidden" ) )
874 : 0 : return QgsLegendStyle::Hidden;
875 : 0 : else if ( style == QLatin1String( "group" ) )
876 : 0 : return QgsLegendStyle::Group;
877 : 0 : else if ( style == QLatin1String( "subgroup" ) )
878 : 0 : return QgsLegendStyle::Subgroup;
879 : :
880 : : // use a default otherwise
881 : 0 : if ( QgsLayerTree::isGroup( node ) )
882 : 0 : return QgsLegendStyle::Group;
883 : 0 : else if ( QgsLayerTree::isLayer( node ) )
884 : : {
885 : 0 : if ( model->legendNodeEmbeddedInParent( QgsLayerTree::toLayer( node ) ) )
886 : 0 : return QgsLegendStyle::Hidden;
887 : 0 : return QgsLegendStyle::Subgroup;
888 : : }
889 : :
890 : 0 : return QgsLegendStyle::Undefined; // should not happen, only if corrupted project file
891 : 0 : }
892 : :
893 : 0 : QgsLegendStyle::Style QgsLegendRenderer::nodeLegendStyle( QgsLayerTreeNode *node )
894 : : {
895 : 0 : return nodeLegendStyle( node, mLegendModel );
896 : : }
897 : :
898 : 0 : void QgsLegendRenderer::setNodeLegendStyle( QgsLayerTreeNode *node, QgsLegendStyle::Style style )
899 : : {
900 : 0 : QString str;
901 : 0 : switch ( style )
902 : : {
903 : : case QgsLegendStyle::Hidden:
904 : 0 : str = QStringLiteral( "hidden" );
905 : 0 : break;
906 : : case QgsLegendStyle::Group:
907 : 0 : str = QStringLiteral( "group" );
908 : 0 : break;
909 : : case QgsLegendStyle::Subgroup:
910 : 0 : str = QStringLiteral( "subgroup" );
911 : 0 : break;
912 : : default:
913 : 0 : break; // nothing
914 : : }
915 : :
916 : 0 : if ( !str.isEmpty() )
917 : 0 : node->setCustomProperty( QStringLiteral( "legend/title-style" ), str );
918 : : else
919 : 0 : node->removeCustomProperty( QStringLiteral( "legend/title-style" ) );
920 : 0 : }
921 : :
922 : 0 : void QgsLegendRenderer::drawLegend( QgsRenderContext &context )
923 : : {
924 : 0 : paintAndDetermineSize( context );
925 : 0 : }
926 : :
|