QGIS API Documentation  3.10.0-A Coruña (6c816b4204)
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  : mType( other.mType )
26  , mTitleLabel( other.mTitleLabel )
27  , mSizeClasses( other.mSizeClasses )
28  , mSymbol( other.mSymbol.get() ? other.mSymbol->clone() : nullptr )
29  , mSizeScaleTransformer( other.mSizeScaleTransformer.get() ? new QgsSizeScaleTransformer( *other.mSizeScaleTransformer ) : nullptr )
30  , mVAlign( other.mVAlign )
31  , mFont( other.mFont )
32  , mTextColor( other.mTextColor )
33  , mTextAlignment( other.mTextAlignment )
34 {
35 }
36 
38 {
39  if ( this != &other )
40  {
41  mType = other.mType;
42  mTitleLabel = other.mTitleLabel;
43  mSizeClasses = other.mSizeClasses;
44  mSymbol.reset( other.mSymbol.get() ? other.mSymbol->clone() : nullptr );
45  mSizeScaleTransformer.reset( other.mSizeScaleTransformer.get() ? new QgsSizeScaleTransformer( *other.mSizeScaleTransformer ) : nullptr );
46  mVAlign = other.mVAlign;
47  mFont = other.mFont;
48  mTextColor = other.mTextColor;
49  mTextAlignment = other.mTextAlignment;
50  }
51  return *this;
52 }
53 
55 {
56  mSymbol.reset( symbol );
57 }
58 
60 {
61  return mSymbol.get();
62 }
63 
65 {
66  mSizeScaleTransformer.reset( transformer );
67 }
68 
70 {
71  return mSizeScaleTransformer.get();
72 }
73 
74 
76 {
77  mSymbol.reset( symbol->clone() );
78  mSymbol->setDataDefinedSize( QgsProperty() ); // original symbol may have had data-defined size associated
79 
80  const QgsSizeScaleTransformer *sizeTransformer = dynamic_cast< const QgsSizeScaleTransformer * >( ddSize.transformer() );
81  mSizeScaleTransformer.reset( sizeTransformer ? sizeTransformer->clone() : nullptr );
82 
83  if ( mTitleLabel.isEmpty() )
84  mTitleLabel = ddSize.propertyType() == QgsProperty::ExpressionBasedProperty ? ddSize.expressionString() : ddSize.field();
85 
86  // automatically generate classes if no classes are defined
87  if ( sizeTransformer && mSizeClasses.isEmpty() )
88  {
89  mSizeClasses.clear();
90  const auto prettyBreaks { QgsSymbolLayerUtils::prettyBreaks( sizeTransformer->minValue(), sizeTransformer->maxValue(), 4 ) };
91  for ( double v : prettyBreaks )
92  {
93  mSizeClasses << SizeClass( v, QString::number( v ) );
94  }
95  }
96 }
97 
99 {
101  if ( !mTitleLabel.isEmpty() )
102  {
103  QgsLegendSymbolItem title( nullptr, mTitleLabel, QString() );
104  lst << title;
105  }
106 
107  switch ( mType )
108  {
109  case LegendCollapsed:
110  {
113  lst << i;
114  break;
115  }
116 
117  case LegendSeparated:
118  {
119  lst.reserve( mSizeClasses.size() );
120  for ( const SizeClass &cl : mSizeClasses )
121  {
122  QgsLegendSymbolItem si( mSymbol.get(), cl.label, QString() );
123  QgsMarkerSymbol *s = static_cast<QgsMarkerSymbol *>( si.symbol() );
124  double size = cl.size;
125  if ( mSizeScaleTransformer )
126  {
127  size = mSizeScaleTransformer->size( size );
128  }
129 
130  s->setSize( size );
131  lst << si;
132  }
133  break;
134  }
135  }
136  return lst;
137 }
138 
139 
140 void QgsDataDefinedSizeLegend::drawCollapsedLegend( QgsRenderContext &context, QSize *outputSize, int *labelXOffset ) const
141 {
142  if ( mType != LegendCollapsed || mSizeClasses.isEmpty() || !mSymbol )
143  {
144  if ( outputSize )
145  *outputSize = QSize();
146  if ( labelXOffset )
147  *labelXOffset = 0;
148  return;
149  }
150 
151  // parameters that could be configurable
152  double hLengthLineMM = 2; // extra horizontal space to be occupied by callout line
153  double hSpaceLineTextMM = 1; // horizontal space between end of the line and start of the text
154 
155  std::unique_ptr<QgsMarkerSymbol> s( mSymbol->clone() );
156 
157  QList<SizeClass> classes = mSizeClasses;
158 
159  // optionally scale size values if transformer is defined
160  if ( mSizeScaleTransformer )
161  {
162  for ( SizeClass &cls : classes )
163  cls.size = mSizeScaleTransformer->size( cls.size );
164  }
165 
166  // make sure we draw bigger symbols first
167  std::sort( classes.begin(), classes.end(), []( const SizeClass & a, const SizeClass & b ) { return a.size > b.size; } );
168 
169  int hLengthLine = std::round( context.convertToPainterUnits( hLengthLineMM, QgsUnitTypes::RenderMillimeters ) );
170  int hSpaceLineText = std::round( context.convertToPainterUnits( hSpaceLineTextMM, QgsUnitTypes::RenderMillimeters ) );
171  int dpm = std::round( context.scaleFactor() * 1000 ); // scale factor = dots per millimeter
172 
173  // get font metrics - we need a temporary image just to get the metrics right for the given DPI
174  QImage tmpImg( QSize( 1, 1 ), QImage::Format_ARGB32_Premultiplied );
175  tmpImg.setDotsPerMeterX( dpm );
176  tmpImg.setDotsPerMeterY( dpm );
177  QFontMetrics fm( mFont, &tmpImg );
178  int textHeight = fm.height();
179  int leading = fm.leading();
180  int minTextDistY = textHeight + leading;
181 
182  //
183  // determine layout of the rendered elements
184  //
185 
186  // find out how wide the text will be
187  int maxTextWidth = 0;
188  for ( const SizeClass &c : qgis::as_const( classes ) )
189  {
190  int w = fm.width( c.label );
191  if ( w > maxTextWidth )
192  maxTextWidth = w;
193  }
194  // add extra width needed to handle varying rendering of font weight
195  maxTextWidth += 1;
196 
197  // find out size of the largest symbol
198  double largestSize = classes.at( 0 ).size;
199  int outputLargestSize = std::round( context.convertToPainterUnits( largestSize, s->sizeUnit(), s->sizeMapUnitScale() ) );
200 
201  // find out top Y coordinate for individual symbol sizes
202  QList<int> symbolTopY;
203  for ( const SizeClass &c : qgis::as_const( classes ) )
204  {
205  int outputSymbolSize = std::round( context.convertToPainterUnits( c.size, s->sizeUnit(), s->sizeMapUnitScale() ) );
206  switch ( mVAlign )
207  {
208  case AlignCenter:
209  symbolTopY << std::round( outputLargestSize / 2 - outputSymbolSize / 2 );
210  break;
211  case AlignBottom:
212  symbolTopY << std::round( outputLargestSize - outputSymbolSize );
213  break;
214  }
215  }
216 
217  // determine Y coordinate of texts: ideally they should be at the same level as symbolTopY
218  // but we need to avoid overlapping texts, so adjust the vertical positions
219  int middleIndex = 0; // classes.count() / 2; // will get the ideal position
220  QList<int> textCenterY;
221  int lastY = symbolTopY[middleIndex];
222  textCenterY << lastY;
223  for ( int i = middleIndex + 1; i < classes.count(); ++i )
224  {
225  int symbolY = symbolTopY[i];
226  if ( symbolY - lastY < minTextDistY )
227  symbolY = lastY + minTextDistY;
228  textCenterY << symbolY;
229  lastY = symbolY;
230  }
231 
232  int textTopY = textCenterY.first() - textHeight / 2;
233  int textBottomY = textCenterY.last() + textHeight / 2;
234  int totalTextHeight = textBottomY - textTopY;
235 
236  int fullWidth = outputLargestSize + hLengthLine + hSpaceLineText + maxTextWidth;
237  int fullHeight = std::max( static_cast< int >( std::round( outputLargestSize ) ) - textTopY, totalTextHeight );
238 
239  if ( outputSize )
240  *outputSize = QSize( fullWidth, fullHeight );
241  if ( labelXOffset )
242  *labelXOffset = outputLargestSize + hLengthLine + hSpaceLineText;
243 
244  if ( !context.painter() )
245  return; // only layout
246 
247  //
248  // drawing
249  //
250 
251  QPainter *p = context.painter();
252 
253  p->save();
254  p->translate( 0, -textTopY );
255 
256  // draw symbols first so that they do not cover
257  for ( const SizeClass &c : qgis::as_const( classes ) )
258  {
259  s->setSize( c.size );
260 
261  int outputSymbolSize = std::round( context.convertToPainterUnits( c.size, s->sizeUnit(), s->sizeMapUnitScale() ) );
262  double tx = ( outputLargestSize - outputSymbolSize ) / 2;
263 
264  p->save();
265  switch ( mVAlign )
266  {
267  case AlignCenter:
268  p->translate( tx, ( outputLargestSize - outputSymbolSize ) / 2 );
269  break;
270  case AlignBottom:
271  p->translate( tx, outputLargestSize - outputSymbolSize );
272  break;
273  }
274  s->drawPreviewIcon( p, QSize( outputSymbolSize, outputSymbolSize ) );
275  p->restore();
276  }
277 
278  p->setPen( mTextColor );
279  p->setFont( mFont );
280 
281  int i = 0;
282  for ( const SizeClass &c : qgis::as_const( classes ) )
283  {
284  // line from symbol to the text
285  p->drawLine( outputLargestSize / 2, symbolTopY[i], outputLargestSize + hLengthLine, textCenterY[i] );
286 
287  // draw label
288  QRect rect( outputLargestSize + hLengthLine + hSpaceLineText, textCenterY[i] - textHeight / 2,
289  maxTextWidth, textHeight );
290  p->drawText( rect, mTextAlignment, c.label );
291  i++;
292  }
293 
294  p->restore();
295 }
296 
297 
298 QImage QgsDataDefinedSizeLegend::collapsedLegendImage( QgsRenderContext &context, const QColor &backgroundColor, double paddingMM ) const
299 {
300  if ( mType != LegendCollapsed || mSizeClasses.isEmpty() || !mSymbol )
301  return QImage();
302 
303  // find out the size first
304  QSize contentSize;
305  drawCollapsedLegend( context, &contentSize );
306 
307  int padding = std::round( context.convertToPainterUnits( paddingMM, QgsUnitTypes::RenderMillimeters ) );
308  int dpm = std::round( context.scaleFactor() * 1000 ); // scale factor = dots per millimeter
309 
310  QImage img( contentSize.width() + padding * 2, contentSize.height() + padding * 2, QImage::Format_ARGB32_Premultiplied );
311  img.setDotsPerMeterX( dpm );
312  img.setDotsPerMeterY( dpm );
313  img.fill( backgroundColor );
314 
315  QPainter painter( &img );
316  painter.setRenderHint( QPainter::Antialiasing, true );
317 
318  painter.translate( padding, padding ); // so we do not need to care about padding at all
319 
320  // now do the rendering
321  QPainter *oldPainter = context.painter();
322  context.setPainter( &painter );
323  drawCollapsedLegend( context );
324  context.setPainter( oldPainter );
325 
326  painter.end();
327  return img;
328 }
329 
331 {
332  if ( elem.isNull() )
333  return nullptr;
335  ddsLegend->setLegendType( elem.attribute( QStringLiteral( "type" ) ) == QLatin1String( "collapsed" ) ? LegendCollapsed : LegendSeparated );
336  ddsLegend->setVerticalAlignment( elem.attribute( QStringLiteral( "valign" ) ) == QLatin1String( "center" ) ? AlignCenter : AlignBottom );
337  ddsLegend->setTitle( elem.attribute( QStringLiteral( "title" ) ) );
338 
339  QDomElement elemSymbol = elem.firstChildElement( QStringLiteral( "symbol" ) );
340  if ( !elemSymbol.isNull() )
341  {
342  ddsLegend->setSymbol( QgsSymbolLayerUtils::loadSymbol<QgsMarkerSymbol>( elemSymbol, context ) );
343  }
344 
345  QgsSizeScaleTransformer *transformer = nullptr;
346  QDomElement elemTransformer = elem.firstChildElement( QStringLiteral( "transformer" ) );
347  if ( !elemTransformer.isNull() )
348  {
349  transformer = new QgsSizeScaleTransformer;
350  transformer->loadVariant( QgsXmlUtils::readVariant( elemTransformer ) );
351  }
352  ddsLegend->setSizeScaleTransformer( transformer );
353 
354  QDomElement elemTextStyle = elem.firstChildElement( QStringLiteral( "text-style" ) );
355  if ( !elemTextStyle.isNull() )
356  {
357  QDomElement elemFont = elemTextStyle.firstChildElement( QStringLiteral( "font" ) );
358  if ( !elemFont.isNull() )
359  {
360  ddsLegend->setFont( QFont( elemFont.attribute( QStringLiteral( "family" ) ), elemFont.attribute( QStringLiteral( "size" ) ).toInt(),
361  elemFont.attribute( QStringLiteral( "weight" ) ).toInt(), elemFont.attribute( QStringLiteral( "italic" ) ).toInt() ) );
362  }
363  ddsLegend->setTextColor( QgsSymbolLayerUtils::decodeColor( elemTextStyle.attribute( QStringLiteral( "color" ) ) ) );
364  ddsLegend->setTextAlignment( static_cast<Qt::AlignmentFlag>( elemTextStyle.attribute( QStringLiteral( "align" ) ).toInt() ) );
365  }
366 
367  QDomElement elemClasses = elem.firstChildElement( QStringLiteral( "classes" ) );
368  if ( !elemClasses.isNull() )
369  {
370  QList<SizeClass> classes;
371  QDomElement elemClass = elemClasses.firstChildElement( QStringLiteral( "class" ) );
372  while ( !elemClass.isNull() )
373  {
374  classes << SizeClass( elemClass.attribute( QStringLiteral( "size" ) ).toDouble(), elemClass.attribute( QStringLiteral( "label" ) ) );
375  elemClass = elemClass.nextSiblingElement();
376  }
377  ddsLegend->setClasses( classes );
378  }
379 
380  return ddsLegend;
381 }
382 
383 void QgsDataDefinedSizeLegend::writeXml( QDomElement &elem, const QgsReadWriteContext &context ) const
384 {
385  QDomDocument doc = elem.ownerDocument();
386 
387  elem.setAttribute( QStringLiteral( "type" ), mType == LegendCollapsed ? "collapsed" : "separated" );
388  elem.setAttribute( QStringLiteral( "valign" ), mVAlign == AlignCenter ? "center" : "bottom" );
389  elem.setAttribute( QStringLiteral( "title" ), mTitleLabel );
390 
391  if ( mSymbol )
392  {
393  QDomElement elemSymbol = QgsSymbolLayerUtils::saveSymbol( QStringLiteral( "source" ), mSymbol.get(), doc, context );
394  elem.appendChild( elemSymbol );
395  }
396 
397  if ( mSizeScaleTransformer )
398  {
399  QDomElement elemTransformer = QgsXmlUtils::writeVariant( mSizeScaleTransformer->toVariant(), doc );
400  elemTransformer.setTagName( QStringLiteral( "transformer" ) );
401  elem.appendChild( elemTransformer );
402  }
403 
404  QDomElement elemFont = doc.createElement( QStringLiteral( "font" ) );
405  elemFont.setAttribute( QStringLiteral( "family" ), mFont.family() );
406  elemFont.setAttribute( QStringLiteral( "size" ), mFont.pointSize() );
407  elemFont.setAttribute( QStringLiteral( "weight" ), mFont.weight() );
408  elemFont.setAttribute( QStringLiteral( "italic" ), mFont.italic() );
409 
410  QDomElement elemTextStyle = doc.createElement( QStringLiteral( "text-style" ) );
411  elemTextStyle.setAttribute( QStringLiteral( "color" ), QgsSymbolLayerUtils::encodeColor( mTextColor ) );
412  elemTextStyle.setAttribute( QStringLiteral( "align" ), static_cast<int>( mTextAlignment ) );
413  elemTextStyle.appendChild( elemFont );
414  elem.appendChild( elemTextStyle );
415 
416  if ( !mSizeClasses.isEmpty() )
417  {
418  QDomElement elemClasses = doc.createElement( QStringLiteral( "classes" ) );
419  for ( const SizeClass &sc : qgis::as_const( mSizeClasses ) )
420  {
421  QDomElement elemClass = doc.createElement( QStringLiteral( "class" ) );
422  elemClass.setAttribute( QStringLiteral( "size" ), sc.size );
423  elemClass.setAttribute( QStringLiteral( "label" ), sc.label );
424  elemClasses.appendChild( elemClass );
425  }
426  elem.appendChild( elemClasses );
427  }
428 }
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.
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:860
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...
QgsDataDefinedSizeLegend()=default
Constructor for QgsDataDefinedSizeLegend.
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...
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:1409
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.
void drawCollapsedLegend(QgsRenderContext &context, QSize *outputSize SIP_OUT=nullptr, int *labelXOffset SIP_OUT=nullptr) const
Draw the legend if using LegendOneNodeForAll and optionally output size of the legend and x offset of...
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:1708
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.