QGIS API Documentation  2.14.0-Essen
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( nullptr )
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 = nullptr;
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 = nullptr;
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.at( 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 / static_cast< double >( bandwidth ), 2 ), 2 );
213 }
214 
215 double QgsHeatmapRenderer::triweightKernel( const double distance, const int bandwidth ) const
216 {
217  return pow( 1. - pow( distance / static_cast< double >( bandwidth ), 2 ), 3 );
218 }
219 
220 double QgsHeatmapRenderer::epanechnikovKernel( const double distance, const int bandwidth ) const
221 {
222  return ( 1. - pow( distance / static_cast< double >( bandwidth ), 2 ) );
223 }
224 
225 double QgsHeatmapRenderer::triangularKernel( const double distance, const int bandwidth ) const
226 {
227  return ( 1. - ( distance / static_cast< 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 = reinterpret_cast< 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  copyRendererData( 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( static_cast< 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  rendererElem.setAttribute( "forceraster", ( mForceRaster ? "1" : "0" ) );
365 
367  mPaintEffect->saveProperties( doc, rendererElem );
368 
369  if ( !mOrderBy.isEmpty() )
370  {
371  QDomElement orderBy = doc.createElement( "orderby" );
372  mOrderBy.save( orderBy );
373  rendererElem.appendChild( orderBy );
374  }
375  rendererElem.setAttribute( "enableorderby", ( mOrderByEnabled ? "1" : "0" ) );
376 
377  return rendererElem;
378 }
379 
381 {
382  Q_UNUSED( feature );
383  return nullptr;
384 }
385 
387 {
388  return QgsSymbolV2List();
389 }
390 
392 {
393  QSet<QString> attributes;
394 
395  // mAttrName can contain either attribute name or an expression.
396  // Sometimes it is not possible to distinguish between those two,
397  // e.g. "a - b" can be both a valid attribute name or expression.
398  // Since we do not have access to fields here, try both options.
399  attributes << mWeightExpressionString;
400 
401  QgsExpression testExpr( mWeightExpressionString );
402  if ( !testExpr.hasParserError() )
403  attributes.unite( testExpr.referencedColumns().toSet() );
404 
405  return attributes.toList();
406 }
407 
409 {
410  if ( renderer->type() == "heatmapRenderer" )
411  {
412  return dynamic_cast<QgsHeatmapRenderer*>( renderer->clone() );
413  }
414  else
415  {
416  return new QgsHeatmapRenderer();
417  }
418 }
419 
421 {
422  delete mGradientRamp;
423  mGradientRamp = ramp;
424 }
Class for parsing and evaluation of expressions (formerly called "search strings").
void setInvertRamp(const bool invert)
Sets whether the ramp is inverted.
#define RENDERER_TAG_NAME
Definition: qgsrendererv2.h:49
void CORE_EXPORT save(QDomElement &elem) const
Serialize to XML.
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.
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
Render a feature using this renderer in the given context.
static QgsVectorColorRampV2 * loadColorRamp(QDomElement &element)
virtual QString dump() const override
for debugging
QgsFeatureRequest::OrderBy orderBy() const
Get the order in which features shall be processed by this renderer.
static QgsFeatureRendererV2 * create(QDomElement &element)
virtual QList< QString > usedAttributes() override
Returns a set of attributes required for this renderer.
QList< QgsSymbolV2 * > QgsSymbolV2List
Definition: qgsrendererv2.h:40
QDomNode appendChild(const QDomNode &newChild)
void setXMaximum(double x)
Set the maximum x value.
Definition: qgsrectangle.h:172
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:197
static QDomElement saveColorRamp(const QString &name, QgsVectorColorRampV2 *ramp, QDomDocument &doc)
int fieldNameIndex(const QString &fieldName) const
Look up field&#39;s index from name also looks up case-insensitive if there is no match otherwise...
Definition: qgsfield.cpp:503
QVector< T > & fill(const T &value, int size)
const_iterator constEnd() const
The output shall be in pixels.
Definition: qgssymbolv2.h:67
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:187
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:187
const QgsCoordinateTransform * coordinateTransform() const
QgsPaintEffect * mPaintEffect
double x() const
Get the x value of the point.
Definition: qgspoint.h:128
void setRadiusUnit(const QgsSymbolV2::OutputUnit unit)
Sets the units used for the heatmap&#39;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:83
virtual QgsFeatureRendererV2 * clone() const =0
The output shall be in millimeters.
Definition: qgssymbolv2.h:64
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:202
void fill(uint pixelValue)
double xMaximum() const
Get the x maximum value (right side of rectangle)
Definition: qgsrectangle.h:187
A renderer which draws points as a live heatmap.
QgsAttributes attributes() const
Returns the feature&#39;s attributes.
Definition: qgsfeature.cpp:110
void setRadiusMapUnitScale(const QgsMapUnitScale &scale)
Sets the map unit scale used for the heatmap&#39;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:177
bool isEmpty() const
bool isEmpty() const
The output shall be in map unitx.
Definition: qgssymbolv2.h:65
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
virtual void startRender(QgsRenderContext &context, const QgsFields &fields) override
Needs to be called when a new render cycle is started.
A class to represent a point.
Definition: qgspoint.h:65
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.
void copyRendererData(QgsFeatureRendererV2 *destRenderer) const
Clones generic renderer data to another renderer.
bool isNull() const
virtual QgsHeatmapRenderer * clone() const override
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()
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:182
QgsFeatureRequest::OrderBy mOrderBy
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
Needs to be called when a render cycle has finished to clean up.
const QgsGeometry * constGeometry() const
Gets a const pointer to the geometry object associated with this feature.
Definition: qgsfeature.cpp:82
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:136
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:115
double xMinimum() const
Get the x minimum value (left side of rectangle)
Definition: qgsrectangle.h:192
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:167
QRgb rgba() const