QGIS API Documentation  2.18.21-Las Palmas (9fba24a)
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.
bool hasParserError() const
Returns true if an error occurred when parsing the input expression.
#define RENDERER_TAG_NAME
Definition: qgsrendererv2.h:49
static unsigned index
A rectangle specified with double values.
Definition: qgsrectangle.h:35
QgsAttributes attributes() const
Returns the feature&#39;s attributes.
Definition: qgsfeature.cpp:110
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.
QgsPoint asPoint() const
Return contents of the geometry as a point if wkbType is WKBPoint, otherwise returns [0...
bool isMultipart() const
Returns true if WKB of the geometry is of WKBMulti* type.
static QgsVectorColorRampV2 * loadColorRamp(QDomElement &element)
virtual QString dump() const override
for debugging
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
static QDomElement saveColorRamp(const QString &name, QgsVectorColorRampV2 *ramp, QDomDocument &doc)
QStringList referencedColumns() const
Get list of columns referenced by the expression.
QVector< T > & fill(const T &value, int size)
const_iterator constEnd() const
The output shall be in pixels.
Definition: qgssymbolv2.h:70
static bool isDefaultStack(QgsPaintEffect *effect)
Tests whether a paint effect matches the default effects stack.
Container of fields for a vector layer.
Definition: qgsfield.h:252
A geometry is the spatial representation of a feature.
Definition: qgsgeometry.h:76
const QgsGeometry * constGeometry() const
Gets a const pointer to the geometry object associated with this feature.
Definition: qgsfeature.cpp:82
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
QgsPaintEffect * mPaintEffect
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.
double y() const
Get the y value of the point.
Definition: qgspoint.h:193
void reset(T *other)
T value(int i) const
int width() const
QgsMultiPoint asMultiPoint() const
Return contents of the geometry as a multi point if wkbType is WKBMultiPoint, otherwise an empty list...
QgsPoint transform(const QgsPoint &p) const
Transform the point from map (world) coordinates to device coordinates.
virtual QgsFeatureRendererV2 * clone() const =0
The output shall be in millimeters.
Definition: qgssymbolv2.h:67
QString number(int n, int base)
void resize(int size)
void fill(uint pixelValue)
A renderer which draws points as a live heatmap.
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
Creates a clone of the color ramp.
int toInt(bool *ok, int base) const
void setYMinimum(double y)
Set the minimum y value.
Definition: qgsrectangle.h:177
bool isEmpty() const
QString type() const
Definition: qgsrendererv2.h:92
bool isEmpty() const
The output shall be in map unitx.
Definition: qgssymbolv2.h:68
QPaintDevice * device() const
const QgsCoordinateTransform * coordinateTransform() 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
QGis::GeometryType type() const
Returns type of the geometry as a QGis::GeometryType.
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:117
void setRenderQuality(const int quality)
Sets the render quality used for drawing the heatmap.
virtual QColor color(double value) const =0
Returns the color corresponding to a specified value.
T * data() const
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:571
double yMinimum() const
Get the y minimum value (bottom side of rectangle)
Definition: qgsrectangle.h:202
QgsExpressionContext & expressionContext()
Gets the expression context.
void setRadius(const double radius)
Sets the radius for the heatmap.
double xMaximum() const
Get the x maximum value (right side of rectangle)
Definition: qgsrectangle.h:187
bool isNull() const
virtual QgsHeatmapRenderer * clone() const override
const T & at(int i) const
QgsFeatureRequest::OrderBy orderBy() const
Get the order in which features shall be processed by this renderer.
const_iterator constBegin() const
void copyRendererData(QgsFeatureRendererV2 *destRenderer) const
Clones generic renderer data to another renderer.
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()
const QgsMapToPixel & mapToPixel() const
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
QDomElement firstChildElement(const QString &tagName) const
void CORE_EXPORT save(QDomElement &elem) const
Serialize to XML.
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.
Class for doing transforms between two map coordinate systems.
double xMinimum() const
Get the x minimum value (left side of rectangle)
Definition: qgsrectangle.h:192
int transform(const QgsCoordinateTransform &ct)
Transform this geometry as described by CoordinateTransform ct.
QList< T > toList() const
double yMaximum() const
Get the y maximum value (top side of rectangle)
Definition: qgsrectangle.h:197
double toDouble(bool *ok) const
typedef const_iterator
static QgsMapUnitScale decodeMapUnitScale(const QString &str)
int height() const
QDomElement createElement(const QString &tagName)
A vector of attributes.
Definition: qgsfeature.h:115
Abstract base class for color ramps.
Gradient color ramp, which smoothly interpolates between two colors and also supports optional extra ...
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
double x() const
Get the x value of the point.
Definition: qgspoint.h:185
void setXMinimum(double x)
Set the minimum x value.
Definition: qgsrectangle.h:167
QRgb rgba() const
virtual bool saveProperties(QDomDocument &doc, QDomElement &element) const
Saves the current state of the effect to a DOM element.