QGIS API Documentation  3.8.0-Zanzibar (11aff65)
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  if ( mType == LegendCollapsed )
108  {
111  lst << i;
112  return lst;
113  }
114  else if ( mType == LegendSeparated )
115  {
116  const auto constMSizeClasses = mSizeClasses;
117  for ( const SizeClass &cl : constMSizeClasses )
118  {
119  QgsLegendSymbolItem si( mSymbol.get(), cl.label, QString() );
120  QgsMarkerSymbol *s = static_cast<QgsMarkerSymbol *>( si.symbol() );
121  s->setSize( cl.size );
122  lst << si;
123  }
124  }
125  return lst;
126 }
127 
128 
129 void QgsDataDefinedSizeLegend::drawCollapsedLegend( QgsRenderContext &context, QSize *outputSize, int *labelXOffset ) const
130 {
131  if ( mType != LegendCollapsed || mSizeClasses.isEmpty() || !mSymbol )
132  {
133  if ( outputSize )
134  *outputSize = QSize();
135  if ( labelXOffset )
136  *labelXOffset = 0;
137  return;
138  }
139 
140  // parameters that could be configurable
141  double hLengthLineMM = 2; // extra horizontal space to be occupied by callout line
142  double hSpaceLineTextMM = 1; // horizontal space between end of the line and start of the text
143 
144  std::unique_ptr<QgsMarkerSymbol> s( mSymbol->clone() );
145 
146  QList<SizeClass> classes = mSizeClasses;
147 
148  // optionally scale size values if transformer is defined
149  if ( mSizeScaleTransformer )
150  {
151  for ( SizeClass &cls : classes )
152  cls.size = mSizeScaleTransformer->size( cls.size );
153  }
154 
155  // make sure we draw bigger symbols first
156  std::sort( classes.begin(), classes.end(), []( const SizeClass & a, const SizeClass & b ) { return a.size > b.size; } );
157 
158  int hLengthLine = std::round( context.convertToPainterUnits( hLengthLineMM, QgsUnitTypes::RenderMillimeters ) );
159  int hSpaceLineText = std::round( context.convertToPainterUnits( hSpaceLineTextMM, QgsUnitTypes::RenderMillimeters ) );
160  int dpm = std::round( context.scaleFactor() * 1000 ); // scale factor = dots per millimeter
161 
162  // get font metrics - we need a temporary image just to get the metrics right for the given DPI
163  QImage tmpImg( QSize( 1, 1 ), QImage::Format_ARGB32_Premultiplied );
164  tmpImg.setDotsPerMeterX( dpm );
165  tmpImg.setDotsPerMeterY( dpm );
166  QFontMetrics fm( mFont, &tmpImg );
167  int textHeight = fm.height();
168  int leading = fm.leading();
169  int minTextDistY = textHeight + leading;
170 
171  //
172  // determine layout of the rendered elements
173  //
174 
175  // find out how wide the text will be
176  int maxTextWidth = 0;
177  for ( const SizeClass &c : qgis::as_const( classes ) )
178  {
179  int w = fm.width( c.label );
180  if ( w > maxTextWidth )
181  maxTextWidth = w;
182  }
183  // add extra width needed to handle varying rendering of font weight
184  maxTextWidth += 1;
185 
186  // find out size of the largest symbol
187  double largestSize = classes.at( 0 ).size;
188  int outputLargestSize = std::round( context.convertToPainterUnits( largestSize, s->sizeUnit(), s->sizeMapUnitScale() ) );
189 
190  // find out top Y coordinate for individual symbol sizes
191  QList<int> symbolTopY;
192  for ( const SizeClass &c : qgis::as_const( classes ) )
193  {
194  int outputSymbolSize = std::round( context.convertToPainterUnits( c.size, s->sizeUnit(), s->sizeMapUnitScale() ) );
195  switch ( mVAlign )
196  {
197  case AlignCenter:
198  symbolTopY << std::round( outputLargestSize / 2 - outputSymbolSize / 2 );
199  break;
200  case AlignBottom:
201  symbolTopY << std::round( outputLargestSize - outputSymbolSize );
202  break;
203  }
204  }
205 
206  // determine Y coordinate of texts: ideally they should be at the same level as symbolTopY
207  // but we need to avoid overlapping texts, so adjust the vertical positions
208  int middleIndex = 0; // classes.count() / 2; // will get the ideal position
209  QList<int> textCenterY;
210  int lastY = symbolTopY[middleIndex];
211  textCenterY << lastY;
212  for ( int i = middleIndex + 1; i < classes.count(); ++i )
213  {
214  int symbolY = symbolTopY[i];
215  if ( symbolY - lastY < minTextDistY )
216  symbolY = lastY + minTextDistY;
217  textCenterY << symbolY;
218  lastY = symbolY;
219  }
220 
221  int textTopY = textCenterY.first() - textHeight / 2;
222  int textBottomY = textCenterY.last() + textHeight / 2;
223  int totalTextHeight = textBottomY - textTopY;
224 
225  int fullWidth = outputLargestSize + hLengthLine + hSpaceLineText + maxTextWidth;
226  int fullHeight = std::max( static_cast< int >( std::round( outputLargestSize ) ) - textTopY, totalTextHeight );
227 
228  if ( outputSize )
229  *outputSize = QSize( fullWidth, fullHeight );
230  if ( labelXOffset )
231  *labelXOffset = outputLargestSize + hLengthLine + hSpaceLineText;
232 
233  if ( !context.painter() )
234  return; // only layout
235 
236  //
237  // drawing
238  //
239 
240  QPainter *p = context.painter();
241 
242  p->save();
243  p->translate( 0, -textTopY );
244 
245  // draw symbols first so that they do not cover
246  for ( const SizeClass &c : qgis::as_const( classes ) )
247  {
248  s->setSize( c.size );
249 
250  int outputSymbolSize = std::round( context.convertToPainterUnits( c.size, s->sizeUnit(), s->sizeMapUnitScale() ) );
251  double tx = ( outputLargestSize - outputSymbolSize ) / 2;
252 
253  p->save();
254  switch ( mVAlign )
255  {
256  case AlignCenter:
257  p->translate( tx, ( outputLargestSize - outputSymbolSize ) / 2 );
258  break;
259  case AlignBottom:
260  p->translate( tx, outputLargestSize - outputSymbolSize );
261  break;
262  }
263  s->drawPreviewIcon( p, QSize( outputSymbolSize, outputSymbolSize ) );
264  p->restore();
265  }
266 
267  p->setPen( mTextColor );
268  p->setFont( mFont );
269 
270  int i = 0;
271  for ( const SizeClass &c : qgis::as_const( classes ) )
272  {
273  // line from symbol to the text
274  p->drawLine( outputLargestSize / 2, symbolTopY[i], outputLargestSize + hLengthLine, textCenterY[i] );
275 
276  // draw label
277  QRect rect( outputLargestSize + hLengthLine + hSpaceLineText, textCenterY[i] - textHeight / 2,
278  maxTextWidth, textHeight );
279  p->drawText( rect, mTextAlignment, c.label );
280  i++;
281  }
282 
283  p->restore();
284 }
285 
286 
287 QImage QgsDataDefinedSizeLegend::collapsedLegendImage( QgsRenderContext &context, const QColor &backgroundColor, double paddingMM ) const
288 {
289  if ( mType != LegendCollapsed || mSizeClasses.isEmpty() || !mSymbol )
290  return QImage();
291 
292  // find out the size first
293  QSize contentSize;
294  drawCollapsedLegend( context, &contentSize );
295 
296  int padding = std::round( context.convertToPainterUnits( paddingMM, QgsUnitTypes::RenderMillimeters ) );
297  int dpm = std::round( context.scaleFactor() * 1000 ); // scale factor = dots per millimeter
298 
299  QImage img( contentSize.width() + padding * 2, contentSize.height() + padding * 2, QImage::Format_ARGB32_Premultiplied );
300  img.setDotsPerMeterX( dpm );
301  img.setDotsPerMeterY( dpm );
302  img.fill( backgroundColor );
303 
304  QPainter painter( &img );
305  painter.setRenderHint( QPainter::Antialiasing, true );
306 
307  painter.translate( padding, padding ); // so we do not need to care about padding at all
308 
309  // now do the rendering
310  QPainter *oldPainter = context.painter();
311  context.setPainter( &painter );
312  drawCollapsedLegend( context );
313  context.setPainter( oldPainter );
314 
315  painter.end();
316  return img;
317 }
318 
320 {
321  if ( elem.isNull() )
322  return nullptr;
324  ddsLegend->setLegendType( elem.attribute( QStringLiteral( "type" ) ) == QLatin1String( "collapsed" ) ? LegendCollapsed : LegendSeparated );
325  ddsLegend->setVerticalAlignment( elem.attribute( QStringLiteral( "valign" ) ) == QLatin1String( "center" ) ? AlignCenter : AlignBottom );
326  ddsLegend->setTitle( elem.attribute( QStringLiteral( "title" ) ) );
327 
328  QDomElement elemSymbol = elem.firstChildElement( QStringLiteral( "symbol" ) );
329  if ( !elemSymbol.isNull() )
330  {
331  ddsLegend->setSymbol( QgsSymbolLayerUtils::loadSymbol<QgsMarkerSymbol>( elemSymbol, context ) );
332  }
333 
334  QgsSizeScaleTransformer *transformer = nullptr;
335  QDomElement elemTransformer = elem.firstChildElement( QStringLiteral( "transformer" ) );
336  if ( !elemTransformer.isNull() )
337  {
338  transformer = new QgsSizeScaleTransformer;
339  transformer->loadVariant( QgsXmlUtils::readVariant( elemTransformer ) );
340  }
341  ddsLegend->setSizeScaleTransformer( transformer );
342 
343  QDomElement elemTextStyle = elem.firstChildElement( QStringLiteral( "text-style" ) );
344  if ( !elemTextStyle.isNull() )
345  {
346  QDomElement elemFont = elemTextStyle.firstChildElement( QStringLiteral( "font" ) );
347  if ( !elemFont.isNull() )
348  {
349  ddsLegend->setFont( QFont( elemFont.attribute( QStringLiteral( "family" ) ), elemFont.attribute( QStringLiteral( "size" ) ).toInt(),
350  elemFont.attribute( QStringLiteral( "weight" ) ).toInt(), elemFont.attribute( QStringLiteral( "italic" ) ).toInt() ) );
351  }
352  ddsLegend->setTextColor( QgsSymbolLayerUtils::decodeColor( elemTextStyle.attribute( QStringLiteral( "color" ) ) ) );
353  ddsLegend->setTextAlignment( static_cast<Qt::AlignmentFlag>( elemTextStyle.attribute( QStringLiteral( "align" ) ).toInt() ) );
354  }
355 
356  QDomElement elemClasses = elem.firstChildElement( QStringLiteral( "classes" ) );
357  if ( !elemClasses.isNull() )
358  {
359  QList<SizeClass> classes;
360  QDomElement elemClass = elemClasses.firstChildElement( QStringLiteral( "class" ) );
361  while ( !elemClass.isNull() )
362  {
363  classes << SizeClass( elemClass.attribute( QStringLiteral( "size" ) ).toDouble(), elemClass.attribute( QStringLiteral( "label" ) ) );
364  elemClass = elemClass.nextSiblingElement();
365  }
366  ddsLegend->setClasses( classes );
367  }
368 
369  return ddsLegend;
370 }
371 
372 void QgsDataDefinedSizeLegend::writeXml( QDomElement &elem, const QgsReadWriteContext &context ) const
373 {
374  QDomDocument doc = elem.ownerDocument();
375 
376  elem.setAttribute( QStringLiteral( "type" ), mType == LegendCollapsed ? "collapsed" : "separated" );
377  elem.setAttribute( QStringLiteral( "valign" ), mVAlign == AlignCenter ? "center" : "bottom" );
378  elem.setAttribute( QStringLiteral( "title" ), mTitleLabel );
379 
380  if ( mSymbol )
381  {
382  QDomElement elemSymbol = QgsSymbolLayerUtils::saveSymbol( QStringLiteral( "source" ), mSymbol.get(), doc, context );
383  elem.appendChild( elemSymbol );
384  }
385 
386  if ( mSizeScaleTransformer )
387  {
388  QDomElement elemTransformer = QgsXmlUtils::writeVariant( mSizeScaleTransformer->toVariant(), doc );
389  elemTransformer.setTagName( QStringLiteral( "transformer" ) );
390  elem.appendChild( elemTransformer );
391  }
392 
393  QDomElement elemFont = doc.createElement( QStringLiteral( "font" ) );
394  elemFont.setAttribute( QStringLiteral( "family" ), mFont.family() );
395  elemFont.setAttribute( QStringLiteral( "size" ), mFont.pointSize() );
396  elemFont.setAttribute( QStringLiteral( "weight" ), mFont.weight() );
397  elemFont.setAttribute( QStringLiteral( "italic" ), mFont.italic() );
398 
399  QDomElement elemTextStyle = doc.createElement( QStringLiteral( "text-style" ) );
400  elemTextStyle.setAttribute( QStringLiteral( "color" ), QgsSymbolLayerUtils::encodeColor( mTextColor ) );
401  elemTextStyle.setAttribute( QStringLiteral( "align" ), static_cast<int>( mTextAlignment ) );
402  elemTextStyle.appendChild( elemFont );
403  elem.appendChild( elemTextStyle );
404 
405  if ( !mSizeClasses.isEmpty() )
406  {
407  QDomElement elemClasses = doc.createElement( QStringLiteral( "classes" ) );
408  for ( const SizeClass &sc : qgis::as_const( mSizeClasses ) )
409  {
410  QDomElement elemClass = doc.createElement( QStringLiteral( "class" ) );
411  elemClass.setAttribute( QStringLiteral( "size" ), sc.size );
412  elemClass.setAttribute( QStringLiteral( "label" ), sc.label );
413  elemClasses.appendChild( elemClass );
414  }
415  elem.appendChild( elemClasses );
416  }
417 }
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.
void setSize(double size)
Sets the size for the whole symbol.
Definition: qgssymbol.cpp:1318
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:766
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.
QString title() const
Returns title label for data-defined size legend.
static QDomElement saveSymbol(const QString &symbolName, QgsSymbol *symbol, QDomDocument &doc, const QgsReadWriteContext &context)
Writes a symbol definition to XML.
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:1641
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.