QGIS API Documentation  3.12.1-BucureČ™ti (121cc00ff0)
qgsdatadefinedsizelegend.cpp
Go to the documentation of this file.
1 /***************************************************************************
2  qgsdatadefinedsizelegend.cpp
3  --------------------------------------
4  Date : June 2017
5  Copyright : (C) 2017 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 
17 
18 #include "qgsproperty.h"
19 #include "qgspropertytransformer.h"
20 #include "qgssymbollayerutils.h"
21 #include "qgsxmlutils.h"
22 
23 
25 
27 
29  : mType( other.mType )
30  , mTitleLabel( other.mTitleLabel )
31  , mSizeClasses( other.mSizeClasses )
32  , mSymbol( other.mSymbol.get() ? other.mSymbol->clone() : nullptr )
33  , mSizeScaleTransformer( other.mSizeScaleTransformer.get() ? new QgsSizeScaleTransformer( *other.mSizeScaleTransformer ) : nullptr )
34  , mVAlign( other.mVAlign )
35  , mFont( other.mFont )
36  , mTextColor( other.mTextColor )
37  , mTextAlignment( other.mTextAlignment )
38 {
39 }
40 
42 {
43  if ( this != &other )
44  {
45  mType = other.mType;
46  mTitleLabel = other.mTitleLabel;
47  mSizeClasses = other.mSizeClasses;
48  mSymbol.reset( other.mSymbol.get() ? other.mSymbol->clone() : nullptr );
49  mSizeScaleTransformer.reset( other.mSizeScaleTransformer.get() ? new QgsSizeScaleTransformer( *other.mSizeScaleTransformer ) : nullptr );
50  mVAlign = other.mVAlign;
51  mFont = other.mFont;
52  mTextColor = other.mTextColor;
53  mTextAlignment = other.mTextAlignment;
54  }
55  return *this;
56 }
57 
59 {
60  mSymbol.reset( symbol );
61 }
62 
64 {
65  return mSymbol.get();
66 }
67 
69 {
70  mSizeScaleTransformer.reset( transformer );
71 }
72 
74 {
75  return mSizeScaleTransformer.get();
76 }
77 
78 
80 {
81  mSymbol.reset( symbol->clone() );
82  mSymbol->setDataDefinedSize( QgsProperty() ); // original symbol may have had data-defined size associated
83 
84  const QgsSizeScaleTransformer *sizeTransformer = dynamic_cast< const QgsSizeScaleTransformer * >( ddSize.transformer() );
85  mSizeScaleTransformer.reset( sizeTransformer ? sizeTransformer->clone() : nullptr );
86 
87  if ( mTitleLabel.isEmpty() )
88  mTitleLabel = ddSize.propertyType() == QgsProperty::ExpressionBasedProperty ? ddSize.expressionString() : ddSize.field();
89 
90  // automatically generate classes if no classes are defined
91  if ( sizeTransformer && mSizeClasses.isEmpty() )
92  {
93  mSizeClasses.clear();
94  const auto prettyBreaks { QgsSymbolLayerUtils::prettyBreaks( sizeTransformer->minValue(), sizeTransformer->maxValue(), 4 ) };
95  for ( double v : prettyBreaks )
96  {
97  mSizeClasses << SizeClass( v, QString::number( v ) );
98  }
99  }
100 }
101 
103 {
105  if ( !mTitleLabel.isEmpty() )
106  {
107  QgsLegendSymbolItem title( nullptr, mTitleLabel, QString() );
108  lst << title;
109  }
110 
111  switch ( mType )
112  {
113  case LegendCollapsed:
114  {
117  lst << i;
118  break;
119  }
120 
121  case LegendSeparated:
122  {
123  lst.reserve( mSizeClasses.size() );
124  for ( const SizeClass &cl : mSizeClasses )
125  {
126  QgsLegendSymbolItem si( mSymbol.get(), cl.label, QString() );
127  QgsMarkerSymbol *s = static_cast<QgsMarkerSymbol *>( si.symbol() );
128  double size = cl.size;
129  if ( mSizeScaleTransformer )
130  {
131  size = mSizeScaleTransformer->size( size );
132  }
133 
134  s->setSize( size );
135  lst << si;
136  }
137  break;
138  }
139  }
140  return lst;
141 }
142 
143 
144 void QgsDataDefinedSizeLegend::drawCollapsedLegend( QgsRenderContext &context, QSizeF *outputSize, double *labelXOffset ) const
145 {
146  if ( mType != LegendCollapsed || mSizeClasses.isEmpty() || !mSymbol )
147  {
148  if ( outputSize )
149  *outputSize = QSizeF();
150  if ( labelXOffset )
151  *labelXOffset = 0;
152  return;
153  }
154 
155  // parameters that could be configurable
156  double hLengthLineMM = 2; // extra horizontal space to be occupied by callout line
157  double hSpaceLineTextMM = 1; // horizontal space between end of the line and start of the text
158 
159  std::unique_ptr<QgsMarkerSymbol> s( mSymbol->clone() );
160 
161  QList<SizeClass> classes = mSizeClasses;
162 
163  // optionally scale size values if transformer is defined
164  if ( mSizeScaleTransformer )
165  {
166  for ( SizeClass &cls : classes )
167  cls.size = mSizeScaleTransformer->size( cls.size );
168  }
169 
170  // make sure we draw bigger symbols first
171  std::sort( classes.begin(), classes.end(), []( const SizeClass & a, const SizeClass & b ) { return a.size > b.size; } );
172 
173  double hLengthLine = context.convertToPainterUnits( hLengthLineMM, QgsUnitTypes::RenderMillimeters );
174  double hSpaceLineText = context.convertToPainterUnits( hSpaceLineTextMM, QgsUnitTypes::RenderMillimeters );
175  int dpm = std::round( context.scaleFactor() * 1000 ); // scale factor = dots per millimeter
176 
177  // get font metrics - we need a temporary image just to get the metrics right for the given DPI
178  QImage tmpImg( QSize( 1, 1 ), QImage::Format_ARGB32_Premultiplied );
179  tmpImg.setDotsPerMeterX( dpm );
180  tmpImg.setDotsPerMeterY( dpm );
181  QFontMetricsF fm( mFont, &tmpImg );
182  double textHeight = fm.height();
183  double leading = fm.leading();
184  double minTextDistY = textHeight + leading;
185 
186  //
187  // determine layout of the rendered elements
188  //
189 
190  // find out how wide the text will be
191  double maxTextWidth = 0;
192  for ( const SizeClass &c : qgis::as_const( classes ) )
193  {
194  maxTextWidth = std::max( maxTextWidth, fm.boundingRect( c.label ).width() );
195  }
196  // add extra width needed to handle varying rendering of font weight
197  maxTextWidth += 1;
198 
199  // find out size of the largest symbol
200  double largestSize = classes.at( 0 ).size;
201  double outputLargestSize = context.convertToPainterUnits( largestSize, s->sizeUnit(), s->sizeMapUnitScale() );
202 
203  // find out top Y coordinate for individual symbol sizes
204  QList<double> symbolTopY;
205  for ( const SizeClass &c : qgis::as_const( classes ) )
206  {
207  double outputSymbolSize = context.convertToPainterUnits( c.size, s->sizeUnit(), s->sizeMapUnitScale() );
208  switch ( mVAlign )
209  {
210  case AlignCenter:
211  symbolTopY << outputLargestSize / 2 - outputSymbolSize / 2;
212  break;
213  case AlignBottom:
214  symbolTopY << outputLargestSize - outputSymbolSize;
215  break;
216  }
217  }
218 
219  // determine Y coordinate of texts: ideally they should be at the same level as symbolTopY
220  // but we need to avoid overlapping texts, so adjust the vertical positions
221  double middleIndex = 0; // classes.count() / 2; // will get the ideal position
222  QList<double> textCenterY;
223  double lastY = symbolTopY[middleIndex];
224  textCenterY << lastY;
225  for ( int i = middleIndex + 1; i < classes.count(); ++i )
226  {
227  double symbolY = symbolTopY[i];
228  if ( symbolY - lastY < minTextDistY )
229  symbolY = lastY + minTextDistY;
230  textCenterY << symbolY;
231  lastY = symbolY;
232  }
233 
234  double textTopY = textCenterY.first() - textHeight / 2;
235  double textBottomY = textCenterY.last() + textHeight / 2;
236  double totalTextHeight = textBottomY - textTopY;
237 
238  double fullWidth = outputLargestSize + hLengthLine + hSpaceLineText + maxTextWidth;
239  double fullHeight = std::max( outputLargestSize - textTopY, totalTextHeight );
240 
241  if ( outputSize )
242  *outputSize = QSizeF( fullWidth, fullHeight );
243  if ( labelXOffset )
244  *labelXOffset = outputLargestSize + hLengthLine + hSpaceLineText;
245 
246  if ( !context.painter() )
247  return; // only layout
248 
249  //
250  // drawing
251  //
252 
253  QPainter *p = context.painter();
254 
255  p->save();
256  p->translate( 0, -textTopY );
257 
258  // draw symbols first so that they do not cover
259  for ( const SizeClass &c : qgis::as_const( classes ) )
260  {
261  s->setSize( c.size );
262 
263  double outputSymbolSize = context.convertToPainterUnits( c.size, s->sizeUnit(), s->sizeMapUnitScale() );
264  double tx = ( outputLargestSize - outputSymbolSize ) / 2;
265 
266  p->save();
267  switch ( mVAlign )
268  {
269  case AlignCenter:
270  p->translate( tx, ( outputLargestSize - outputSymbolSize ) / 2 );
271  break;
272  case AlignBottom:
273  p->translate( tx, outputLargestSize - outputSymbolSize );
274  break;
275  }
276  s->drawPreviewIcon( p, QSize( outputSymbolSize, outputSymbolSize ) );
277  p->restore();
278  }
279 
280  p->setPen( mTextColor );
281  p->setFont( mFont );
282 
283  int i = 0;
284  for ( const SizeClass &c : qgis::as_const( classes ) )
285  {
286  // line from symbol to the text
287  p->drawLine( outputLargestSize / 2, symbolTopY[i], outputLargestSize + hLengthLine, textCenterY[i] );
288 
289  // draw label
290  QRect rect( outputLargestSize + hLengthLine + hSpaceLineText, textCenterY[i] - textHeight / 2,
291  maxTextWidth, textHeight );
292  p->drawText( rect, mTextAlignment, c.label );
293  i++;
294  }
295 
296  p->restore();
297 }
298 
299 
300 QImage QgsDataDefinedSizeLegend::collapsedLegendImage( QgsRenderContext &context, const QColor &backgroundColor, double paddingMM ) const
301 {
302  if ( mType != LegendCollapsed || mSizeClasses.isEmpty() || !mSymbol )
303  return QImage();
304 
305  // find out the size first
306  QSizeF contentSize;
307  drawCollapsedLegend( context, &contentSize );
308 
309  double padding = context.convertToPainterUnits( paddingMM, QgsUnitTypes::RenderMillimeters );
310  int dpm = std::round( context.scaleFactor() * 1000 ); // scale factor = dots per millimeter
311 
312  QImage img( contentSize.width() + padding * 2, contentSize.height() + padding * 2, QImage::Format_ARGB32_Premultiplied );
313  img.setDotsPerMeterX( dpm );
314  img.setDotsPerMeterY( dpm );
315  img.fill( backgroundColor );
316 
317  QPainter painter( &img );
318  painter.setRenderHint( QPainter::Antialiasing, true );
319 
320  painter.translate( padding, padding ); // so we do not need to care about padding at all
321 
322  // now do the rendering
323  QPainter *oldPainter = context.painter();
324  context.setPainter( &painter );
325  drawCollapsedLegend( context );
326  context.setPainter( oldPainter );
327 
328  painter.end();
329  return img;
330 }
331 
333 {
334  if ( elem.isNull() )
335  return nullptr;
337  ddsLegend->setLegendType( elem.attribute( QStringLiteral( "type" ) ) == QLatin1String( "collapsed" ) ? LegendCollapsed : LegendSeparated );
338  ddsLegend->setVerticalAlignment( elem.attribute( QStringLiteral( "valign" ) ) == QLatin1String( "center" ) ? AlignCenter : AlignBottom );
339  ddsLegend->setTitle( elem.attribute( QStringLiteral( "title" ) ) );
340 
341  QDomElement elemSymbol = elem.firstChildElement( QStringLiteral( "symbol" ) );
342  if ( !elemSymbol.isNull() )
343  {
344  ddsLegend->setSymbol( QgsSymbolLayerUtils::loadSymbol<QgsMarkerSymbol>( elemSymbol, context ) );
345  }
346 
347  QgsSizeScaleTransformer *transformer = nullptr;
348  QDomElement elemTransformer = elem.firstChildElement( QStringLiteral( "transformer" ) );
349  if ( !elemTransformer.isNull() )
350  {
351  transformer = new QgsSizeScaleTransformer;
352  transformer->loadVariant( QgsXmlUtils::readVariant( elemTransformer ) );
353  }
354  ddsLegend->setSizeScaleTransformer( transformer );
355 
356  QDomElement elemTextStyle = elem.firstChildElement( QStringLiteral( "text-style" ) );
357  if ( !elemTextStyle.isNull() )
358  {
359  QDomElement elemFont = elemTextStyle.firstChildElement( QStringLiteral( "font" ) );
360  if ( !elemFont.isNull() )
361  {
362  ddsLegend->setFont( QFont( elemFont.attribute( QStringLiteral( "family" ) ), elemFont.attribute( QStringLiteral( "size" ) ).toInt(),
363  elemFont.attribute( QStringLiteral( "weight" ) ).toInt(), elemFont.attribute( QStringLiteral( "italic" ) ).toInt() ) );
364  }
365  ddsLegend->setTextColor( QgsSymbolLayerUtils::decodeColor( elemTextStyle.attribute( QStringLiteral( "color" ) ) ) );
366  ddsLegend->setTextAlignment( static_cast<Qt::AlignmentFlag>( elemTextStyle.attribute( QStringLiteral( "align" ) ).toInt() ) );
367  }
368 
369  QDomElement elemClasses = elem.firstChildElement( QStringLiteral( "classes" ) );
370  if ( !elemClasses.isNull() )
371  {
372  QList<SizeClass> classes;
373  QDomElement elemClass = elemClasses.firstChildElement( QStringLiteral( "class" ) );
374  while ( !elemClass.isNull() )
375  {
376  classes << SizeClass( elemClass.attribute( QStringLiteral( "size" ) ).toDouble(), elemClass.attribute( QStringLiteral( "label" ) ) );
377  elemClass = elemClass.nextSiblingElement();
378  }
379  ddsLegend->setClasses( classes );
380  }
381 
382  return ddsLegend;
383 }
384 
385 void QgsDataDefinedSizeLegend::writeXml( QDomElement &elem, const QgsReadWriteContext &context ) const
386 {
387  QDomDocument doc = elem.ownerDocument();
388 
389  elem.setAttribute( QStringLiteral( "type" ), mType == LegendCollapsed ? "collapsed" : "separated" );
390  elem.setAttribute( QStringLiteral( "valign" ), mVAlign == AlignCenter ? "center" : "bottom" );
391  elem.setAttribute( QStringLiteral( "title" ), mTitleLabel );
392 
393  if ( mSymbol )
394  {
395  QDomElement elemSymbol = QgsSymbolLayerUtils::saveSymbol( QStringLiteral( "source" ), mSymbol.get(), doc, context );
396  elem.appendChild( elemSymbol );
397  }
398 
399  if ( mSizeScaleTransformer )
400  {
401  QDomElement elemTransformer = QgsXmlUtils::writeVariant( mSizeScaleTransformer->toVariant(), doc );
402  elemTransformer.setTagName( QStringLiteral( "transformer" ) );
403  elem.appendChild( elemTransformer );
404  }
405 
406  QDomElement elemFont = doc.createElement( QStringLiteral( "font" ) );
407  elemFont.setAttribute( QStringLiteral( "family" ), mFont.family() );
408  elemFont.setAttribute( QStringLiteral( "size" ), mFont.pointSize() );
409  elemFont.setAttribute( QStringLiteral( "weight" ), mFont.weight() );
410  elemFont.setAttribute( QStringLiteral( "italic" ), mFont.italic() );
411 
412  QDomElement elemTextStyle = doc.createElement( QStringLiteral( "text-style" ) );
413  elemTextStyle.setAttribute( QStringLiteral( "color" ), QgsSymbolLayerUtils::encodeColor( mTextColor ) );
414  elemTextStyle.setAttribute( QStringLiteral( "align" ), static_cast<int>( mTextAlignment ) );
415  elemTextStyle.appendChild( elemFont );
416  elem.appendChild( elemTextStyle );
417 
418  if ( !mSizeClasses.isEmpty() )
419  {
420  QDomElement elemClasses = doc.createElement( QStringLiteral( "classes" ) );
421  for ( const SizeClass &sc : qgis::as_const( mSizeClasses ) )
422  {
423  QDomElement elemClass = doc.createElement( QStringLiteral( "class" ) );
424  elemClass.setAttribute( QStringLiteral( "size" ), sc.size );
425  elemClass.setAttribute( QStringLiteral( "label" ), sc.label );
426  elemClasses.appendChild( elemClass );
427  }
428  elem.appendChild( elemClasses );
429  }
430 }
The class is used as a container of context for various read/write operations on other objects...
Symbols are aligned to the center.
Each class (size value) has a separate legend node.
QList< QgsLegendSymbolItem > QgsLegendSymbolList
void setTextAlignment(Qt::AlignmentFlag flag)
Sets horizontal text alignment for rendering of labels - only valid for collapsed legend...
void setFont(const QFont &font)
Sets font used for rendering of labels - only valid for collapsed legend.
Definition of one class for the legend.
QgsDataDefinedSizeLegend()
Constructor for QgsDataDefinedSizeLegend.
Expression based property (QgsExpressionBasedProperty)
Definition: qgsproperty.h:239
QgsDataDefinedSizeLegend & operator=(const QgsDataDefinedSizeLegend &other)
QgsSizeScaleTransformer * clone() const override
Returns a clone of the transformer.
static QDomElement saveSymbol(const QString &symbolName, const QgsSymbol *symbol, QDomDocument &doc, const QgsReadWriteContext &context)
Writes a symbol definition to XML.
QgsLegendSymbolList legendSymbolList() const
Generates legend symbol items according to the configuration.
void setTitle(const QString &title)
Sets title label for data-defined size legend.
void setLegendType(LegendType type)
Sets how the legend should be rendered.
QgsMarkerSymbol * symbol() const
Returns marker symbol that will be used to draw markers in legend.
A marker symbol type, for rendering Point and MultiPoint geometries.
Definition: qgssymbol.h:895
bool loadVariant(const QVariant &definition) override
Loads this transformer from a QVariantMap, wrapped in a QVariant.
const QgsPropertyTransformer * transformer() const
Returns the existing transformer used for manipulating the calculated values for the property...
static QString encodeColor(const QColor &color)
As part of the API refactoring and improvements which landed in the Processing API was substantially reworked from the x version This was done in order to allow much of the underlying Processing framework to be ported into c
QString expressionString() const
Returns the expression used for the property value.
QImage collapsedLegendImage(QgsRenderContext &context, const QColor &backgroundColor=Qt::transparent, double paddingMM=1) const
Returns output image that would be shown in the legend. Returns invalid image if legend is not config...
static QVariant readVariant(const QDomElement &element)
Read a QVariant from a QDomElement.
Type propertyType() const
Returns the property type.
QList< QgsDataDefinedSizeLegend::SizeClass > classes() const
Returns list of classes: each class is a pair of symbol size (in units used by the symbol) and label...
QgsSizeScaleTransformer * sizeScaleTransformer() const
Returns transformer for scaling of symbol sizes. Returns nullptr if no transformer is defined...
double size
Marker size in units used by the symbol (usually millimeters). May be further scaled before rendering...
double minValue() const
Returns the minimum value expected by the transformer.
void setPainter(QPainter *p)
Sets the destination QPainter for the render operation.
void updateFromSymbolAndProperty(const QgsMarkerSymbol *symbol, const QgsProperty &ddSize)
Updates the list of classes, source symbol and title label from given symbol and property.
void setSizeScaleTransformer(QgsSizeScaleTransformer *transformer SIP_TRANSFER)
Sets transformer for scaling of symbol sizes. Takes ownership of the object. Accepts nullptr to set n...
void drawCollapsedLegend(QgsRenderContext &context, QSizeF *outputSize SIP_OUT=nullptr, double *labelXOffset SIP_OUT=nullptr) const
Draw the legend if using LegendOneNodeForAll and optionally output size of the legend and x offset of...
A store for object properties.
Definition: qgsproperty.h:229
void writeXml(QDomElement &elem, const QgsReadWriteContext &context) const
Writes configuration to the given XML element.
QString field() const
Returns the current field name the property references.
void setVerticalAlignment(VerticalAlignment vAlign)
Sets vertical alignment of symbols - only valid for collapsed legend.
Symbols are aligned to the bottom.
static QList< double > prettyBreaks(double minimum, double maximum, int classes)
Computes a sequence of about &#39;classes&#39; equally spaced round values which cover the range of values fr...
The class stores information about one class/rule of a vector layer renderer in a unified way that ca...
Contains information about the context of a rendering operation.
double convertToPainterUnits(double size, QgsUnitTypes::RenderUnit unit, const QgsMapUnitScale &scale=QgsMapUnitScale()) const
Converts a size from the specified units to painter units (pixels).
QPainter * painter()
Returns the destination QPainter for the render operation.
void setSymbol(QgsMarkerSymbol *symbol SIP_TRANSFER)
Sets marker symbol that will be used to draw markers in legend.
double size() const
Returns the estimated size for the whole symbol, which is the maximum size of all marker symbol layer...
Definition: qgssymbol.cpp:1467
QString title() const
Returns title label for data-defined size legend.
QgsPropertyTransformer subclass for scaling a value into a size according to various scaling methods...
void setTextColor(const QColor &color)
Sets text color for rendering of labels - only valid for collapsed legend.
double scaleFactor() const
Returns the scaling factor for the render to convert painter units to physical sizes.
Object that keeps configuration of appearance of marker symbol&#39;s data-defined size in legend...
All classes are rendered within one legend node.
static QgsDataDefinedSizeLegend * readXml(const QDomElement &elem, const QgsReadWriteContext &context) SIP_FACTORY
Creates instance from given element and returns it (caller takes ownership). Returns nullptr on error...
void setDataDefinedSizeLegendSettings(QgsDataDefinedSizeLegend *settings)
Sets extra information about data-defined size.
static QDomElement writeVariant(const QVariant &value, QDomDocument &doc)
Write a QVariant to a QDomElement.
QgsMarkerSymbol * clone() const override
Returns a deep copy of this symbol.
Definition: qgssymbol.cpp:1766
static QColor decodeColor(const QString &str)
void setClasses(const QList< QgsDataDefinedSizeLegend::SizeClass > &classes)
Sets list of classes: each class is a pair of symbol size (in units used by the symbol) and label...
double maxValue() const
Returns the maximum value expected by the transformer.