1 /***************************************************************************
2     qgshistogramdiagram.cpp
3     ---------------------
4     begin                : August 2012
5     copyright            : (C) 2012 by Matthias Kuhn
6     email                : matthias at opengis dot ch
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 #include "qgshistogramdiagram.h"
16 #include "qgsdiagramrenderer.h"
17 #include "qgsrendercontext.h"
18 #include "qgsexpression.h"
19 #include "qgssymbollayerutils.h"
20 #include "qgslinesymbol.h"
21 
22 #include <QPainter>
23 
QgsHistogramDiagram()24 QgsHistogramDiagram::QgsHistogramDiagram()
25 {
26   mCategoryBrush.setStyle( Qt::SolidPattern );
27   mPen.setStyle( Qt::SolidLine );
28   mScaleFactor = 0;
29 }
30 
clone() const31 QgsHistogramDiagram *QgsHistogramDiagram::clone() const
32 {
33   return new QgsHistogramDiagram( *this );
34 }
35 
diagramSize(const QgsFeature & feature,const QgsRenderContext & c,const QgsDiagramSettings & s,const QgsDiagramInterpolationSettings & is)36 QSizeF QgsHistogramDiagram::diagramSize( const QgsFeature &feature, const QgsRenderContext &c, const QgsDiagramSettings &s, const QgsDiagramInterpolationSettings &is )
37 {
38   QSizeF size;
39   if ( feature.attributes().isEmpty() )
40   {
41     return size; //zero size if no attributes
42   }
43 
44   if ( qgsDoubleNear( is.upperValue, is.lowerValue ) )
45     return size; // invalid value range => zero size
46 
47   double maxValue = 0;
48 
49   QgsExpressionContext expressionContext = c.expressionContext();
50   expressionContext.setFeature( feature );
51   if ( !feature.fields().isEmpty() )
52     expressionContext.setFields( feature.fields() );
53 
54   for ( const QString &cat : std::as_const( s.categoryAttributes ) )
55   {
56     QgsExpression *expression = getExpression( cat, expressionContext );
57     maxValue = std::max( expression->evaluate( &expressionContext ).toDouble(), maxValue );
58   }
59 
60   // Scale, if extension is smaller than the specified minimum
61   if ( maxValue < s.minimumSize )
62   {
63     maxValue = s.minimumSize;
64   }
65 
66   // eh - this method returns size in unknown units ...! We'll have to fake it and use a rough estimation of
67   // a conversion factor to painter units...
68   // TODO QGIS 4.0 -- these methods should all use painter units, dependent on the render context scaling...
69   double painterUnitConversionScale = c.convertToPainterUnits( 1, s.sizeType );
70 
71   const double spacing = c.convertToPainterUnits( s.spacing(), s.spacingUnit(), s.spacingMapUnitScale() ) / painterUnitConversionScale;
72 
73   switch ( s.diagramOrientation )
74   {
75     case QgsDiagramSettings::Up:
76     case QgsDiagramSettings::Down:
77       mScaleFactor = ( ( is.upperSize.width() - is.lowerSize.height() ) / ( is.upperValue - is.lowerValue ) );
78       size.scale( s.barWidth * s.categoryAttributes.size() + spacing * std::max( 0, static_cast<int>( s.categoryAttributes.size() ) - 1 ), maxValue * mScaleFactor, Qt::IgnoreAspectRatio );
79       break;
80 
81     case QgsDiagramSettings::Right:
82     case QgsDiagramSettings::Left:
83       mScaleFactor = ( ( is.upperSize.width() - is.lowerSize.width() ) / ( is.upperValue - is.lowerValue ) );
84       size.scale( maxValue * mScaleFactor, s.barWidth * s.categoryAttributes.size() + spacing * std::max( 0, static_cast<int>( s.categoryAttributes.size() ) - 1 ), Qt::IgnoreAspectRatio );
85       break;
86   }
87 
88   if ( s.showAxis() && s.axisLineSymbol() )
89   {
90     const double maxBleed = QgsSymbolLayerUtils::estimateMaxSymbolBleed( s.axisLineSymbol(), c ) / painterUnitConversionScale;
91     size.setWidth( size.width() + 2 * maxBleed );
92     size.setHeight( size.height() + 2 * maxBleed );
93   }
94 
95   return size;
96 }
97 
legendSize(double value,const QgsDiagramSettings & s,const QgsDiagramInterpolationSettings & is) const98 double QgsHistogramDiagram::legendSize( double value, const QgsDiagramSettings &s, const QgsDiagramInterpolationSettings &is ) const
99 {
100   if ( qgsDoubleNear( is.upperValue, is.lowerValue ) )
101     return s.minimumSize; // invalid value range => zero size
102 
103   // Scale, if extension is smaller than the specified minimum
104   if ( value < s.minimumSize )
105   {
106     value = s.minimumSize;
107   }
108 
109   double scaleFactor = ( ( is.upperSize.width() - is.lowerSize.width() ) / ( is.upperValue - is.lowerValue ) );
110   return value * scaleFactor;
111 }
112 
diagramName() const113 QString QgsHistogramDiagram::diagramName() const
114 {
115   return DIAGRAM_NAME_HISTOGRAM;
116 }
117 
diagramSize(const QgsAttributes & attributes,const QgsRenderContext & c,const QgsDiagramSettings & s)118 QSizeF QgsHistogramDiagram::diagramSize( const QgsAttributes &attributes, const QgsRenderContext &c, const QgsDiagramSettings &s )
119 {
120   Q_UNUSED( c )
121   QSizeF size;
122 
123   if ( attributes.isEmpty() )
124   {
125     return QSizeF(); //zero size if no attributes
126   }
127 
128   double maxValue = attributes.at( 0 ).toDouble();
129 
130   for ( int i = 0; i < attributes.count(); ++i )
131   {
132     maxValue = std::max( attributes.at( i ).toDouble(), maxValue );
133   }
134 
135   // eh - this method returns size in unknown units ...! We'll have to fake it and use a rough estimation of
136   // a conversion factor to painter units...
137   // TODO QGIS 4.0 -- these methods should all use painter units, dependent on the render context scaling...
138   double painterUnitConversionScale = c.convertToPainterUnits( 1, s.sizeType );
139 
140   const double spacing = c.convertToPainterUnits( s.spacing(), s.spacingUnit(), s.spacingMapUnitScale() ) / painterUnitConversionScale;
141 
142   switch ( s.diagramOrientation )
143   {
144     case QgsDiagramSettings::Up:
145     case QgsDiagramSettings::Down:
146       mScaleFactor = maxValue / s.size.height();
147       size.scale( s.barWidth * s.categoryColors.size() + spacing * std::max( 0, static_cast<int>( s.categoryAttributes.size() ) - 1 ), s.size.height(), Qt::IgnoreAspectRatio );
148       break;
149 
150     case QgsDiagramSettings::Right:
151     case QgsDiagramSettings::Left:
152       mScaleFactor = maxValue / s.size.width();
153       size.scale( s.size.width(), s.barWidth * s.categoryColors.size() + spacing * std::max( 0, static_cast<int>( s.categoryAttributes.size() ) - 1 ), Qt::IgnoreAspectRatio );
154       break;
155   }
156 
157   if ( s.showAxis() && s.axisLineSymbol() )
158   {
159     const double maxBleed = QgsSymbolLayerUtils::estimateMaxSymbolBleed( s.axisLineSymbol(), c ) / painterUnitConversionScale;
160     size.setWidth( size.width() + 2 * maxBleed );
161     size.setHeight( size.height() + 2 * maxBleed );
162   }
163 
164   return size;
165 }
166 
renderDiagram(const QgsFeature & feature,QgsRenderContext & c,const QgsDiagramSettings & s,QPointF position)167 void QgsHistogramDiagram::renderDiagram( const QgsFeature &feature, QgsRenderContext &c, const QgsDiagramSettings &s, QPointF position )
168 {
169   QPainter *p = c.painter();
170   if ( !p )
171   {
172     return;
173   }
174 
175   QList<double> values;
176   double maxValue = 0;
177 
178   QgsExpressionContext expressionContext = c.expressionContext();
179   expressionContext.setFeature( feature );
180   if ( !feature.fields().isEmpty() )
181     expressionContext.setFields( feature.fields() );
182 
183   values.reserve( s.categoryAttributes.size() );
184   for ( const QString &cat : std::as_const( s.categoryAttributes ) )
185   {
186     QgsExpression *expression = getExpression( cat, expressionContext );
187     double currentVal = expression->evaluate( &expressionContext ).toDouble();
188     values.push_back( currentVal );
189     maxValue = std::max( currentVal, maxValue );
190   }
191 
192   double scaledMaxVal = sizePainterUnits( maxValue * mScaleFactor, s, c );
193 
194   double currentOffset = 0;
195   double scaledWidth = sizePainterUnits( s.barWidth, s, c );
196 
197   const double spacing = c.convertToPainterUnits( s.spacing(), s.spacingUnit(), s.spacingMapUnitScale() );
198 
199   double baseX = position.x();
200   double baseY = position.y();
201 
202   if ( s.showAxis() && s.axisLineSymbol() )
203   {
204     // if showing axis, the diagram position needs shifting from the default base x so that the axis
205     // line stroke sits within the desired label engine rect (otherwise we risk overlaps of the axis line stroke)
206     const double maxBleed = QgsSymbolLayerUtils::estimateMaxSymbolBleed( s.axisLineSymbol(), c );
207     baseX += maxBleed;
208     baseY -= maxBleed;
209   }
210 
211 
212   mPen.setColor( s.penColor );
213   setPenWidth( mPen, s, c );
214   p->setPen( mPen );
215 
216   QList<double>::const_iterator valIt = values.constBegin();
217   QList< QColor >::const_iterator colIt = s.categoryColors.constBegin();
218   for ( ; valIt != values.constEnd(); ++valIt, ++colIt )
219   {
220     double length = sizePainterUnits( *valIt * mScaleFactor, s, c );
221 
222     mCategoryBrush.setColor( *colIt );
223     p->setBrush( mCategoryBrush );
224 
225     switch ( s.diagramOrientation )
226     {
227       case QgsDiagramSettings::Up:
228         p->drawRect( QRectF( baseX + currentOffset, baseY, scaledWidth, length * -1 ) );
229         break;
230 
231       case QgsDiagramSettings::Down:
232         p->drawRect( QRectF( baseX + currentOffset, baseY - scaledMaxVal, scaledWidth, length ) );
233         break;
234 
235       case QgsDiagramSettings::Right:
236         p->drawRect( QRectF( baseX, baseY - scaledWidth * values.size() - spacing * std::max( 0, static_cast<int>( values.size() ) - 1 ) + currentOffset, length, scaledWidth ) );
237         break;
238 
239       case QgsDiagramSettings::Left:
240         p->drawRect( QRectF( baseX + scaledMaxVal, baseY - scaledWidth * values.size() - spacing * std::max( 0, static_cast<int>( values.size() ) - 1 ) + currentOffset, 0 - length, scaledWidth ) );
241         break;
242     }
243 
244     currentOffset += scaledWidth + spacing;
245   }
246 
247   if ( s.showAxis() && s.axisLineSymbol() )
248   {
249     s.axisLineSymbol()->startRender( c );
250     QPolygonF axisPoints;
251     switch ( s.diagramOrientation )
252     {
253       case QgsDiagramSettings::Up:
254         axisPoints << QPointF( baseX, baseY - scaledMaxVal ) << QPointF( baseX, baseY ) << QPointF( baseX + scaledWidth * values.size() + spacing * std::max( 0, static_cast<int>( values.size() ) - 1 ), baseY );
255         break;
256 
257       case QgsDiagramSettings::Down:
258         axisPoints << QPointF( baseX, baseY ) << QPointF( baseX, baseY - scaledMaxVal ) << QPointF( baseX + scaledWidth * values.size() + spacing * std::max( 0, static_cast<int>( values.size() ) - 1 ), baseY - scaledMaxVal );
259         break;
260 
261       case QgsDiagramSettings::Right:
262         axisPoints << QPointF( baseX + scaledMaxVal, baseY - scaledWidth * values.size() - spacing * std::max( 0, static_cast<int>( values.size() ) - 1 ) )
263                    << QPointF( baseX, baseY - scaledWidth * values.size() - spacing * std::max( 0, static_cast<int>( values.size() ) - 1 ) )
264                    << QPointF( baseX, baseY );
265         break;
266 
267       case QgsDiagramSettings::Left:
268         axisPoints << QPointF( baseX, baseY - scaledWidth * values.size() - spacing * std::max( 0, static_cast<int>( values.size() ) - 1 ) )
269                    << QPointF( baseX + scaledMaxVal, baseY - scaledWidth * values.size() - spacing * std::max( 0, static_cast<int>( values.size() ) - 1 ) )
270                    << QPointF( baseX + scaledMaxVal, baseY );
271         break;
272     }
273 
274     s.axisLineSymbol()->renderPolyline( axisPoints, nullptr, c );
275     s.axisLineSymbol()->stopRender( c );
276   }
277 }
278