QGIS API Documentation  2.12.0-Lyon
qgsheatmaprenderer.cpp
Go to the documentation of this file.
1 /***************************************************************************
2  qgsheatmaprenderer.cpp
3  ----------------------
4  begin : November 2014
5  copyright : (C) 2014 Nyall Dawson
6  email : nyall dot dawson 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 "qgsheatmaprenderer.h"
17 
18 #include "qgssymbolv2.h"
19 #include "qgssymbollayerv2utils.h"
20 
21 #include "qgslogger.h"
22 #include "qgsfeature.h"
23 #include "qgsvectorlayer.h"
24 #include "qgssymbollayerv2.h"
25 #include "qgsogcutils.h"
26 #include "qgsvectorcolorrampv2.h"
27 #include "qgsrendercontext.h"
28 #include "qgspainteffect.h"
29 #include "qgspainteffectregistry.h"
30 
31 #include <QDomDocument>
32 #include <QDomElement>
33 
35  : QgsFeatureRendererV2( "heatmapRenderer" )
36  , mCalculatedMaxValue( 0 )
37  , mRadius( 10 )
38  , mRadiusPixels( 0 )
39  , mRadiusSquared( 0 )
40  , mRadiusUnit( QgsSymbolV2::MM )
41  , mWeightAttrNum( -1 )
42  , mGradientRamp( 0 )
43  , mInvertRamp( false )
44  , mExplicitMax( 0.0 )
45  , mRenderQuality( 3 )
46  , mFeaturesRendered( 0 )
47 {
48  mGradientRamp = new QgsVectorGradientColorRampV2( QColor( 255, 255, 255 ), QColor( 0, 0, 0 ) );
49 
50 }
51 
53 {
54  delete mGradientRamp;
55 }
56 
57 void QgsHeatmapRenderer::initializeValues( QgsRenderContext& context )
58 {
59  mValues.resize( context.painter()->device()->width() * context.painter()->device()->height() / ( mRenderQuality * mRenderQuality ) );
60  mValues.fill( 0 );
61  mCalculatedMaxValue = 0;
62  mFeaturesRendered = 0;
63  mRadiusPixels = qRound( mRadius * QgsSymbolLayerV2Utils::pixelSizeScaleFactor( context, mRadiusUnit, mRadiusMapUnitScale ) / mRenderQuality );
64  mRadiusSquared = mRadiusPixels * mRadiusPixels;
65 }
66 
68 {
69  Q_UNUSED( fields );
70  if ( !context.painter() )
71  {
72  return;
73  }
74 
75  // find out classification attribute index from name
76  mWeightAttrNum = fields.fieldNameIndex( mWeightExpressionString );
77  if ( mWeightAttrNum == -1 )
78  {
79  mWeightExpression.reset( new QgsExpression( mWeightExpressionString ) );
80  mWeightExpression->prepare( &context.expressionContext() );
81  }
82 
83  initializeValues( context );
84  return;
85 }
86 
87 QgsMultiPoint QgsHeatmapRenderer::convertToMultipoint( const QgsGeometry* geom )
88 {
89  QgsMultiPoint multiPoint;
90  if ( !geom->isMultipart() )
91  {
92  multiPoint << geom->asPoint();
93  }
94  else
95  {
96  multiPoint = geom->asMultiPoint();
97  }
98 
99  return multiPoint;
100 }
101 
102 bool QgsHeatmapRenderer::renderFeature( QgsFeature& feature, QgsRenderContext& context, int layer, bool selected, bool drawVertexMarker )
103 {
104  Q_UNUSED( layer );
105  Q_UNUSED( selected );
106  Q_UNUSED( drawVertexMarker );
107 
108  if ( !context.painter() )
109  {
110  return false;
111  }
112 
113  if ( !feature.constGeometry() || feature.constGeometry()->type() != QGis::Point )
114  {
115  //can only render point type
116  return false;
117  }
118 
119  double weight = 1.0;
120  if ( !mWeightExpressionString.isEmpty() )
121  {
122  QVariant value;
123  if ( mWeightAttrNum == -1 )
124  {
125  Q_ASSERT( mWeightExpression.data() );
126  value = mWeightExpression->evaluate( &context.expressionContext() );
127  }
128  else
129  {
130  QgsAttributes attrs = feature.attributes();
131  value = attrs.value( mWeightAttrNum );
132  }
133  bool ok = false;
134  double evalWeight = value.toDouble( &ok );
135  if ( ok )
136  {
137  weight = evalWeight;
138  }
139  }
140 
141  int width = context.painter()->device()->width() / mRenderQuality;
142  int height = context.painter()->device()->height() / mRenderQuality;
143 
144  //transform geometry if required
145  QgsGeometry* transformedGeom = 0;
146  const QgsCoordinateTransform* xform = context.coordinateTransform();
147  if ( xform )
148  {
149  transformedGeom = new QgsGeometry( *feature.constGeometry() );
150  transformedGeom->transform( *xform );
151  }
152 
153  //convert point to multipoint
154  QgsMultiPoint multiPoint = convertToMultipoint( transformedGeom ? transformedGeom : feature.constGeometry() );
155 
156  delete transformedGeom;
157  transformedGeom = 0;
158 
159  //loop through all points in multipoint
160  for ( QgsMultiPoint::const_iterator pointIt = multiPoint.constBegin(); pointIt != multiPoint.constEnd(); ++pointIt )
161  {
162  QgsPoint pixel = context.mapToPixel().transform( *pointIt );
163  int pointX = pixel.x() / mRenderQuality;
164  int pointY = pixel.y() / mRenderQuality;
165  for ( int x = qMax( pointX - mRadiusPixels, 0 ); x < qMin( pointX + mRadiusPixels, width ); ++x )
166  {
167  for ( int y = qMax( pointY - mRadiusPixels, 0 ); y < qMin( pointY + mRadiusPixels, height ); ++y )
168  {
169  int index = y * width + x;
170  if ( index >= mValues.count( ) )
171  {
172  continue;
173  }
174  double distanceSquared = pow( pointX - x, 2.0 ) + pow( pointY - y, 2.0 );
175  if ( distanceSquared > mRadiusSquared )
176  {
177  continue;
178  }
179 
180  double score = weight * quarticKernel( sqrt( distanceSquared ), mRadiusPixels );
181  double value = mValues[ index ] + score;
182  if ( value > mCalculatedMaxValue )
183  {
184  mCalculatedMaxValue = value;
185  }
186  mValues[ index ] = value;
187  }
188  }
189  }
190 
191  mFeaturesRendered++;
192 #if 0
193  //TODO - enable progressive rendering
194  if ( mFeaturesRendered % 200 == 0 )
195  {
196  renderImage( context );
197  }
198 #endif
199  return true;
200 }
201 
202 
203 double QgsHeatmapRenderer::uniformKernel( const double distance, const int bandwidth ) const
204 {
205  Q_UNUSED( distance );
206  Q_UNUSED( bandwidth );
207  return 1.0;
208 }
209 
210 double QgsHeatmapRenderer::quarticKernel( const double distance, const int bandwidth ) const
211 {
212  return pow( 1. - pow( distance / ( double )bandwidth, 2 ), 2 );
213 }
214 
215 double QgsHeatmapRenderer::triweightKernel( const double distance, const int bandwidth ) const
216 {
217  return pow( 1. - pow( distance / ( double )bandwidth, 2 ), 3 );
218 }
219 
220 double QgsHeatmapRenderer::epanechnikovKernel( const double distance, const int bandwidth ) const
221 {
222  return ( 1. - pow( distance / ( double )bandwidth, 2 ) );
223 }
224 
225 double QgsHeatmapRenderer::triangularKernel( const double distance, const int bandwidth ) const
226 {
227  return ( 1. - ( distance / ( double )bandwidth ) );
228 }
229 
231 {
232  renderImage( context );
233  mWeightExpression.reset();
234 }
235 
236 void QgsHeatmapRenderer::renderImage( QgsRenderContext& context )
237 {
238  if ( !context.painter() || !mGradientRamp )
239  {
240  return;
241  }
242 
243  QImage image( context.painter()->device()->width() / mRenderQuality,
244  context.painter()->device()->height() / mRenderQuality,
245  QImage::Format_ARGB32 );
246  image.fill( Qt::transparent );
247 
248  double scaleMax = mExplicitMax > 0 ? mExplicitMax : mCalculatedMaxValue;
249 
250  int idx = 0;
251  double pixVal = 0;
252  QColor pixColor;
253  for ( int heightIndex = 0; heightIndex < image.height(); ++heightIndex )
254  {
255  QRgb* scanLine = ( QRgb* )image.scanLine( heightIndex );
256  for ( int widthIndex = 0; widthIndex < image.width(); ++widthIndex )
257  {
258  //scale result to fit in the range [0, 1]
259  pixVal = mValues.at( idx ) > 0 ? qMin(( mValues.at( idx ) / scaleMax ), 1.0 ) : 0;
260 
261  //convert value to color from ramp
262  pixColor = mGradientRamp->color( mInvertRamp ? 1 - pixVal : pixVal );
263 
264  scanLine[widthIndex] = pixColor.rgba();
265  idx++;
266  }
267  }
268 
269  if ( mRenderQuality > 1 )
270  {
271  QImage resized = image.scaled( context.painter()->device()->width(),
272  context.painter()->device()->height() );
273  context.painter()->drawImage( 0, 0, resized );
274  }
275  else
276  {
277  context.painter()->drawImage( 0, 0, image );
278  }
279 }
280 
282 {
283  return "[HEATMAP]";
284 }
285 
287 {
288  QgsHeatmapRenderer* newRenderer = new QgsHeatmapRenderer();
289  if ( mGradientRamp )
290  {
291  newRenderer->setColorRamp( mGradientRamp->clone() );
292  }
293  newRenderer->setInvertRamp( mInvertRamp );
294  newRenderer->setRadius( mRadius );
295  newRenderer->setRadiusUnit( mRadiusUnit );
296  newRenderer->setRadiusMapUnitScale( mRadiusMapUnitScale );
297  newRenderer->setMaximumValue( mExplicitMax );
298  newRenderer->setRenderQuality( mRenderQuality );
299  newRenderer->setWeightExpression( mWeightExpressionString );
300  copyPaintEffect( newRenderer );
301 
302  return newRenderer;
303 }
304 
306 {
307  //we need to expand out the request extent so that it includes points which are up to the heatmap radius outside of the
308  //actual visible extent
309  double extension = 0.0;
310  if ( mRadiusUnit == QgsSymbolV2::Pixel )
311  {
313  }
314  else if ( mRadiusUnit == QgsSymbolV2::MM )
315  {
316  double pixelSize = mRadius * QgsSymbolLayerV2Utils::pixelSizeScaleFactor( context, QgsSymbolV2::MM, QgsMapUnitScale() );
318  }
319  else
320  {
321  extension = mRadius;
322  }
323  extent.setXMinimum( extent.xMinimum() - extension );
324  extent.setXMaximum( extent.xMaximum() + extension );
325  extent.setYMinimum( extent.yMinimum() - extension );
326  extent.setYMaximum( extent.yMaximum() + extension );
327 }
328 
330 {
332  r->setRadius( element.attribute( "radius", "50.0" ).toFloat() );
333  r->setRadiusUnit(( QgsSymbolV2::OutputUnit )element.attribute( "radius_unit", "0" ).toInt() );
334  r->setRadiusMapUnitScale( QgsSymbolLayerV2Utils::decodeMapUnitScale( element.attribute( "radius_map_unit_scale", QString() ) ) );
335  r->setMaximumValue( element.attribute( "max_value", "0.0" ).toFloat() );
336  r->setRenderQuality( element.attribute( "quality", "0" ).toInt() );
337  r->setWeightExpression( element.attribute( "weight_expression" ) );
338 
339  QDomElement sourceColorRampElem = element.firstChildElement( "colorramp" );
340  if ( !sourceColorRampElem.isNull() && sourceColorRampElem.attribute( "name" ) == "[source]" )
341  {
342  r->setColorRamp( QgsSymbolLayerV2Utils::loadColorRamp( sourceColorRampElem ) );
343  }
344  r->setInvertRamp( element.attribute( "invert_ramp", "0" ).toInt() );
345  return r;
346 }
347 
349 {
350  QDomElement rendererElem = doc.createElement( RENDERER_TAG_NAME );
351  rendererElem.setAttribute( "type", "heatmapRenderer" );
352  rendererElem.setAttribute( "radius", QString::number( mRadius ) );
353  rendererElem.setAttribute( "radius_unit", QString::number( mRadiusUnit ) );
354  rendererElem.setAttribute( "radius_map_unit_scale", QgsSymbolLayerV2Utils::encodeMapUnitScale( mRadiusMapUnitScale ) );
355  rendererElem.setAttribute( "max_value", QString::number( mExplicitMax ) );
356  rendererElem.setAttribute( "quality", QString::number( mRenderQuality ) );
357  rendererElem.setAttribute( "weight_expression", mWeightExpressionString );
358  if ( mGradientRamp )
359  {
360  QDomElement colorRampElem = QgsSymbolLayerV2Utils::saveColorRamp( "[source]", mGradientRamp, doc );
361  rendererElem.appendChild( colorRampElem );
362  }
363  rendererElem.setAttribute( "invert_ramp", QString::number( mInvertRamp ) );
364 
366  mPaintEffect->saveProperties( doc, rendererElem );
367 
368  return rendererElem;
369 }
370 
372 {
373  Q_UNUSED( feature );
374  return 0;
375 }
376 
378 {
379  return QgsSymbolV2List();
380 }
381 
383 {
384  QSet<QString> attributes;
385 
386  // mAttrName can contain either attribute name or an expression.
387  // Sometimes it is not possible to distinguish between those two,
388  // e.g. "a - b" can be both a valid attribute name or expression.
389  // Since we do not have access to fields here, try both options.
390  attributes << mWeightExpressionString;
391 
392  QgsExpression testExpr( mWeightExpressionString );
393  if ( !testExpr.hasParserError() )
394  attributes.unite( testExpr.referencedColumns().toSet() );
395 
396  return attributes.toList();
397 }
398 
400 {
401  if ( renderer->type() == "heatmapRenderer" )
402  {
403  return dynamic_cast<QgsHeatmapRenderer*>( renderer->clone() );
404  }
405  else
406  {
407  return new QgsHeatmapRenderer();
408  }
409 }
410 
412 {
413  delete mGradientRamp;
414  mGradientRamp = ramp;
415 }
Class for parsing and evaluation of expressions (formerly called "search strings").
Definition: qgsexpression.h:92
void setInvertRamp(const bool invert)
Sets whether the ramp is inverted.
#define RENDERER_TAG_NAME
Definition: qgsrendererv2.h:48
static unsigned index
A rectangle specified with double values.
Definition: qgsrectangle.h:35
bool hasParserError() const
Returns true if an error occurred when parsing the input expression.
OutputUnit
The unit of the output.
Definition: qgssymbolv2.h:55
QStringList referencedColumns() const
Get list of columns referenced by the expression.
virtual bool renderFeature(QgsFeature &feature, QgsRenderContext &context, int layer=-1, bool selected=false, bool drawVertexMarker=false) override
static QgsVectorColorRampV2 * loadColorRamp(QDomElement &element)
virtual QString dump() const override
for debugging
static QgsFeatureRendererV2 * create(QDomElement &element)
virtual QList< QString > usedAttributes() override
QList< QgsSymbolV2 * > QgsSymbolV2List
Definition: qgsrendererv2.h:39
QDomNode appendChild(const QDomNode &newChild)
void setXMaximum(double x)
Set the maximum x value.
Definition: qgsrectangle.h:171
virtual void modifyRequestExtent(QgsRectangle &extent, QgsRenderContext &context) override
Allows for a renderer to modify the extent of a feature request prior to rendering.
QString attribute(const QString &name, const QString &defValue) const
double yMaximum() const
Get the y maximum value (top side of rectangle)
Definition: qgsrectangle.h:196
static QDomElement saveColorRamp(const QString &name, QgsVectorColorRampV2 *ramp, QDomDocument &doc)
int fieldNameIndex(const QString &fieldName) const
Look up field's index from name also looks up case-insensitive if there is no match otherwise...
Definition: qgsfield.cpp:382
QVector< T > & fill(const T &value, int size)
const_iterator constEnd() const
The output shall be in pixels.
Definition: qgssymbolv2.h:60
QgsPoint transform(const QgsPoint &p) const
Transform the point from map (world) coordinates to device coordinates.
static bool isDefaultStack(QgsPaintEffect *effect)
Tests whether a paint effect matches the default effects stack.
QGis::GeometryType type() const
Returns type of the geometry as a QGis::GeometryType.
Container of fields for a vector layer.
Definition: qgsfield.h:177
A geometry is the spatial representation of a feature.
Definition: qgsgeometry.h:76
void setWeightExpression(const QString &expression)
Sets the expression used for weighting points when generating the heatmap.
QSet< T > toSet() const
static QString encodeMapUnitScale(const QgsMapUnitScale &mapUnitScale)
The feature class encapsulates a single feature including its id, geometry and a list of field/values...
Definition: qgsfeature.h:176
const QgsCoordinateTransform * coordinateTransform() const
QgsPaintEffect * mPaintEffect
double x() const
Get the x value of the point.
Definition: qgspoint.h:126
void setRadiusUnit(const QgsSymbolV2::OutputUnit unit)
Sets the units used for the heatmap's radius.
static double pixelSizeScaleFactor(const QgsRenderContext &c, QgsSymbolV2::OutputUnit u, const QgsMapUnitScale &scale=QgsMapUnitScale())
Returns scale factor painter units -> pixel dimensions.
void reset(T *other)
T value(int i) const
int width() const
QString type() const
Definition: qgsrendererv2.h:82
virtual QgsFeatureRendererV2 * clone() const =0
The output shall be in millimeters.
Definition: qgssymbolv2.h:57
QString number(int n, int base)
void resize(int size)
double yMinimum() const
Get the y minimum value (bottom side of rectangle)
Definition: qgsrectangle.h:201
void fill(uint pixelValue)
double xMaximum() const
Get the x maximum value (right side of rectangle)
Definition: qgsrectangle.h:186
A renderer which draws points as a live heatmap.
QgsAttributes attributes() const
Returns the feature's attributes.
Definition: qgsfeature.cpp:92
void setRadiusMapUnitScale(const QgsMapUnitScale &scale)
Sets the map unit scale used for the heatmap's radius.
void setAttribute(const QString &name, const QString &value)
virtual QgsVectorColorRampV2 * clone() const =0
int toInt(bool *ok, int base) const
void setYMinimum(double y)
Set the minimum y value.
Definition: qgsrectangle.h:176
bool isEmpty() const
The output shall be in map unitx.
Definition: qgssymbolv2.h:58
QPaintDevice * device() const
virtual Q_DECL_DEPRECATED QgsSymbolV2List symbols()
for symbol levels
virtual QDomElement save(QDomDocument &doc) override
store renderer info to XML element
virtual QgsSymbolV2 * symbolForFeature(QgsFeature &feature, QgsRenderContext &context) override
To be overridden.
virtual void startRender(QgsRenderContext &context, const QgsFields &fields) override
Needs to be called when a new render cycle is started.
virtual QgsFeatureRendererV2 * clone() const override
A class to represent a point.
Definition: qgspoint.h:63
void setRenderQuality(const int quality)
Sets the render quality used for drawing the heatmap.
virtual QColor color(double value) const =0
T * data() const
QgsExpressionContext & expressionContext()
Gets the expression context.
void setRadius(const double radius)
Sets the radius for the heatmap.
bool isNull() const
const T & at(int i) const
virtual bool saveProperties(QDomDocument &doc, QDomElement &element) const
Saves the current state of the effect to a DOM element.
const_iterator constBegin() const
void setColorRamp(QgsVectorColorRampV2 *ramp)
Sets the color ramp to use for shading the heatmap.
Contains information about the context of a rendering operation.
void drawImage(const QRectF &target, const QImage &image, const QRectF &source, QFlags< Qt::ImageConversionFlag > flags)
QPainter * painter()
void copyPaintEffect(QgsFeatureRendererV2 *destRenderer) const
Copies paint effect of this renderer to another renderer.
QSet< T > & unite(const QSet< T > &other)
Struct for storing maximum and minimum scales for measurements in map units.
static QgsHeatmapRenderer * convertFromRenderer(const QgsFeatureRendererV2 *renderer)
void setYMaximum(double y)
Set the maximum y value.
Definition: qgsrectangle.h:181
float toFloat(bool *ok) const
QgsMultiPoint asMultiPoint() const
Return contents of the geometry as a multi point if wkbType is WKBMultiPoint, otherwise an empty list...
bool isMultipart() const
Returns true if WKB of the geometry is of WKBMulti* type.
QDomElement firstChildElement(const QString &tagName) const
int count(const T &value) const
virtual void stopRender(QgsRenderContext &context) override
const QgsGeometry * constGeometry() const
Gets a const pointer to the geometry object associated with this feature.
Definition: qgsfeature.cpp:70
Class for doing transforms between two map coordinate systems.
int transform(const QgsCoordinateTransform &ct)
Transform this geometry as described by CoordinateTransform ct.
const QgsMapToPixel & mapToPixel() const
QList< T > toList() const
double y() const
Get the y value of the point.
Definition: qgspoint.h:134
double toDouble(bool *ok) const
typedef const_iterator
static QgsMapUnitScale decodeMapUnitScale(const QString &str)
int height() const
QDomElement createElement(const QString &tagName)
QgsPoint asPoint() const
Return contents of the geometry as a point if wkbType is WKBPoint, otherwise returns [0...
A vector of attributes.
Definition: qgsfeature.h:109
double xMinimum() const
Get the x minimum value (left side of rectangle)
Definition: qgsrectangle.h:191
void setMaximumValue(const double value)
Sets the maximum value used for shading the heatmap.
QImage scaled(int width, int height, Qt::AspectRatioMode aspectRatioMode, Qt::TransformationMode transformMode) const
void setXMinimum(double x)
Set the minimum x value.
Definition: qgsrectangle.h:166
QRgb rgba() const