QGIS API Documentation  3.8.0-Zanzibar (11aff65)
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 "qgssymbol.h"
19 #include "qgssymbollayerutils.h"
20 
21 #include "qgslogger.h"
22 #include "qgsfeature.h"
23 #include "qgsvectorlayer.h"
24 #include "qgssymbollayer.h"
25 #include "qgsogcutils.h"
26 #include "qgscolorramp.h"
27 #include "qgsrendercontext.h"
28 #include "qgspainteffect.h"
29 #include "qgspainteffectregistry.h"
30 
31 #include <QDomDocument>
32 #include <QDomElement>
33 
35  : QgsFeatureRenderer( QStringLiteral( "heatmapRenderer" ) )
36 {
37  mGradientRamp = new QgsGradientColorRamp( QColor( 255, 255, 255 ), QColor( 0, 0, 0 ) );
38 }
39 
41 {
42  delete mGradientRamp;
43 }
44 
45 void QgsHeatmapRenderer::initializeValues( QgsRenderContext &context )
46 {
47  mValues.resize( context.painter()->device()->width() * context.painter()->device()->height() / ( mRenderQuality * mRenderQuality ) );
48  mValues.fill( 0 );
49  mCalculatedMaxValue = 0;
50  mFeaturesRendered = 0;
51  mRadiusPixels = std::round( context.convertToPainterUnits( mRadius, mRadiusUnit, mRadiusMapUnitScale ) / mRenderQuality );
52  mRadiusSquared = mRadiusPixels * mRadiusPixels;
53 }
54 
56 {
57  QgsFeatureRenderer::startRender( context, fields );
58 
59  if ( !context.painter() )
60  {
61  return;
62  }
63 
64  // find out classification attribute index from name
65  mWeightAttrNum = fields.lookupField( mWeightExpressionString );
66  if ( mWeightAttrNum == -1 )
67  {
68  mWeightExpression.reset( new QgsExpression( mWeightExpressionString ) );
69  mWeightExpression->prepare( &context.expressionContext() );
70  }
71 
72  initializeValues( context );
73 }
74 
75 QgsMultiPointXY QgsHeatmapRenderer::convertToMultipoint( const QgsGeometry *geom )
76 {
77  QgsMultiPointXY multiPoint;
78  if ( !geom->isMultipart() )
79  {
80  multiPoint << geom->asPoint();
81  }
82  else
83  {
84  multiPoint = geom->asMultiPoint();
85  }
86 
87  return multiPoint;
88 }
89 
90 bool QgsHeatmapRenderer::renderFeature( const QgsFeature &feature, QgsRenderContext &context, int layer, bool selected, bool drawVertexMarker )
91 {
92  Q_UNUSED( layer )
93  Q_UNUSED( selected )
94  Q_UNUSED( drawVertexMarker )
95 
96  if ( !context.painter() )
97  {
98  return false;
99  }
100 
101  if ( !feature.hasGeometry() || feature.geometry().type() != QgsWkbTypes::PointGeometry )
102  {
103  //can only render point type
104  return false;
105  }
106 
107  double weight = 1.0;
108  if ( !mWeightExpressionString.isEmpty() )
109  {
110  QVariant value;
111  if ( mWeightAttrNum == -1 )
112  {
113  Q_ASSERT( mWeightExpression.get() );
114  value = mWeightExpression->evaluate( &context.expressionContext() );
115  }
116  else
117  {
118  QgsAttributes attrs = feature.attributes();
119  value = attrs.value( mWeightAttrNum );
120  }
121  bool ok = false;
122  double evalWeight = value.toDouble( &ok );
123  if ( ok )
124  {
125  weight = evalWeight;
126  }
127  }
128 
129  int width = context.painter()->device()->width() / mRenderQuality;
130  int height = context.painter()->device()->height() / mRenderQuality;
131 
132  //transform geometry if required
133  QgsGeometry geom = feature.geometry();
135  if ( xform.isValid() )
136  {
137  geom.transform( xform );
138  }
139 
140  //convert point to multipoint
141  QgsMultiPointXY multiPoint = convertToMultipoint( &geom );
142 
143  //loop through all points in multipoint
144  for ( QgsMultiPointXY::const_iterator pointIt = multiPoint.constBegin(); pointIt != multiPoint.constEnd(); ++pointIt )
145  {
146  QgsPointXY pixel = context.mapToPixel().transform( *pointIt );
147  int pointX = pixel.x() / mRenderQuality;
148  int pointY = pixel.y() / mRenderQuality;
149  for ( int x = std::max( pointX - mRadiusPixels, 0 ); x < std::min( pointX + mRadiusPixels, width ); ++x )
150  {
151  if ( context.renderingStopped() )
152  break;
153 
154  for ( int y = std::max( pointY - mRadiusPixels, 0 ); y < std::min( pointY + mRadiusPixels, height ); ++y )
155  {
156  int index = y * width + x;
157  if ( index >= mValues.count() )
158  {
159  continue;
160  }
161  double distanceSquared = std::pow( pointX - x, 2.0 ) + std::pow( pointY - y, 2.0 );
162  if ( distanceSquared > mRadiusSquared )
163  {
164  continue;
165  }
166 
167  double score = weight * quarticKernel( std::sqrt( distanceSquared ), mRadiusPixels );
168  double value = mValues.at( index ) + score;
169  if ( value > mCalculatedMaxValue )
170  {
171  mCalculatedMaxValue = value;
172  }
173  mValues[ index ] = value;
174  }
175  }
176  }
177 
178  mFeaturesRendered++;
179 #if 0
180  //TODO - enable progressive rendering
181  if ( mFeaturesRendered % 200 == 0 )
182  {
183  renderImage( context );
184  }
185 #endif
186  return true;
187 }
188 
189 
190 double QgsHeatmapRenderer::uniformKernel( const double distance, const int bandwidth ) const
191 {
192  Q_UNUSED( distance )
193  Q_UNUSED( bandwidth )
194  return 1.0;
195 }
196 
197 double QgsHeatmapRenderer::quarticKernel( const double distance, const int bandwidth ) const
198 {
199  return std::pow( 1. - std::pow( distance / static_cast< double >( bandwidth ), 2 ), 2 );
200 }
201 
202 double QgsHeatmapRenderer::triweightKernel( const double distance, const int bandwidth ) const
203 {
204  return std::pow( 1. - std::pow( distance / static_cast< double >( bandwidth ), 2 ), 3 );
205 }
206 
207 double QgsHeatmapRenderer::epanechnikovKernel( const double distance, const int bandwidth ) const
208 {
209  return ( 1. - std::pow( distance / static_cast< double >( bandwidth ), 2 ) );
210 }
211 
212 double QgsHeatmapRenderer::triangularKernel( const double distance, const int bandwidth ) const
213 {
214  return ( 1. - ( distance / static_cast< double >( bandwidth ) ) );
215 }
216 
218 {
220 
221  renderImage( context );
222  mWeightExpression.reset();
223 }
224 
225 void QgsHeatmapRenderer::renderImage( QgsRenderContext &context )
226 {
227  if ( !context.painter() || !mGradientRamp || context.renderingStopped() )
228  {
229  return;
230  }
231 
232  QImage image( context.painter()->device()->width() / mRenderQuality,
233  context.painter()->device()->height() / mRenderQuality,
234  QImage::Format_ARGB32 );
235  image.fill( Qt::transparent );
236 
237  double scaleMax = mExplicitMax > 0 ? mExplicitMax : mCalculatedMaxValue;
238 
239  int idx = 0;
240  double pixVal = 0;
241  QColor pixColor;
242  for ( int heightIndex = 0; heightIndex < image.height(); ++heightIndex )
243  {
244  if ( context.renderingStopped() )
245  break;
246 
247  QRgb *scanLine = reinterpret_cast< QRgb * >( image.scanLine( heightIndex ) );
248  for ( int widthIndex = 0; widthIndex < image.width(); ++widthIndex )
249  {
250  //scale result to fit in the range [0, 1]
251  pixVal = mValues.at( idx ) > 0 ? std::min( ( mValues.at( idx ) / scaleMax ), 1.0 ) : 0;
252 
253  //convert value to color from ramp
254  pixColor = mGradientRamp->color( pixVal );
255 
256  scanLine[widthIndex] = pixColor.rgba();
257  idx++;
258  }
259  }
260 
261  if ( mRenderQuality > 1 )
262  {
263  QImage resized = image.scaled( context.painter()->device()->width(),
264  context.painter()->device()->height() );
265  context.painter()->drawImage( 0, 0, resized );
266  }
267  else
268  {
269  context.painter()->drawImage( 0, 0, image );
270  }
271 }
272 
274 {
275  return QStringLiteral( "[HEATMAP]" );
276 }
277 
279 {
280  QgsHeatmapRenderer *newRenderer = new QgsHeatmapRenderer();
281  if ( mGradientRamp )
282  {
283  newRenderer->setColorRamp( mGradientRamp->clone() );
284  }
285  newRenderer->setRadius( mRadius );
286  newRenderer->setRadiusUnit( mRadiusUnit );
287  newRenderer->setRadiusMapUnitScale( mRadiusMapUnitScale );
288  newRenderer->setMaximumValue( mExplicitMax );
289  newRenderer->setRenderQuality( mRenderQuality );
290  newRenderer->setWeightExpression( mWeightExpressionString );
291  copyRendererData( newRenderer );
292 
293  return newRenderer;
294 }
295 
297 {
298  //we need to expand out the request extent so that it includes points which are up to the heatmap radius outside of the
299  //actual visible extent
300  double extension = context.convertToMapUnits( mRadius, mRadiusUnit, mRadiusMapUnitScale );
301  extent.setXMinimum( extent.xMinimum() - extension );
302  extent.setXMaximum( extent.xMaximum() + extension );
303  extent.setYMinimum( extent.yMinimum() - extension );
304  extent.setYMaximum( extent.yMaximum() + extension );
305 }
306 
307 QgsFeatureRenderer *QgsHeatmapRenderer::create( QDomElement &element, const QgsReadWriteContext &context )
308 {
309  Q_UNUSED( context )
311  r->setRadius( element.attribute( QStringLiteral( "radius" ), QStringLiteral( "50.0" ) ).toFloat() );
312  r->setRadiusUnit( static_cast< QgsUnitTypes::RenderUnit >( element.attribute( QStringLiteral( "radius_unit" ), QStringLiteral( "0" ) ).toInt() ) );
313  r->setRadiusMapUnitScale( QgsSymbolLayerUtils::decodeMapUnitScale( element.attribute( QStringLiteral( "radius_map_unit_scale" ), QString() ) ) );
314  r->setMaximumValue( element.attribute( QStringLiteral( "max_value" ), QStringLiteral( "0.0" ) ).toFloat() );
315  r->setRenderQuality( element.attribute( QStringLiteral( "quality" ), QStringLiteral( "0" ) ).toInt() );
316  r->setWeightExpression( element.attribute( QStringLiteral( "weight_expression" ) ) );
317 
318  QDomElement sourceColorRampElem = element.firstChildElement( QStringLiteral( "colorramp" ) );
319  if ( !sourceColorRampElem.isNull() && sourceColorRampElem.attribute( QStringLiteral( "name" ) ) == QLatin1String( "[source]" ) )
320  {
321  r->setColorRamp( QgsSymbolLayerUtils::loadColorRamp( sourceColorRampElem ) );
322  }
323  return r;
324 }
325 
326 QDomElement QgsHeatmapRenderer::save( QDomDocument &doc, const QgsReadWriteContext &context )
327 {
328  Q_UNUSED( context )
329  QDomElement rendererElem = doc.createElement( RENDERER_TAG_NAME );
330  rendererElem.setAttribute( QStringLiteral( "type" ), QStringLiteral( "heatmapRenderer" ) );
331  rendererElem.setAttribute( QStringLiteral( "radius" ), QString::number( mRadius ) );
332  rendererElem.setAttribute( QStringLiteral( "radius_unit" ), QString::number( mRadiusUnit ) );
333  rendererElem.setAttribute( QStringLiteral( "radius_map_unit_scale" ), QgsSymbolLayerUtils::encodeMapUnitScale( mRadiusMapUnitScale ) );
334  rendererElem.setAttribute( QStringLiteral( "max_value" ), QString::number( mExplicitMax ) );
335  rendererElem.setAttribute( QStringLiteral( "quality" ), QString::number( mRenderQuality ) );
336  rendererElem.setAttribute( QStringLiteral( "weight_expression" ), mWeightExpressionString );
337  if ( mGradientRamp )
338  {
339  QDomElement colorRampElem = QgsSymbolLayerUtils::saveColorRamp( QStringLiteral( "[source]" ), mGradientRamp, doc );
340  rendererElem.appendChild( colorRampElem );
341  }
342  rendererElem.setAttribute( QStringLiteral( "forceraster" ), ( mForceRaster ? QStringLiteral( "1" ) : QStringLiteral( "0" ) ) );
343 
345  mPaintEffect->saveProperties( doc, rendererElem );
346 
347  if ( !mOrderBy.isEmpty() )
348  {
349  QDomElement orderBy = doc.createElement( QStringLiteral( "orderby" ) );
350  mOrderBy.save( orderBy );
351  rendererElem.appendChild( orderBy );
352  }
353  rendererElem.setAttribute( QStringLiteral( "enableorderby" ), ( mOrderByEnabled ? QStringLiteral( "1" ) : QStringLiteral( "0" ) ) );
354 
355  return rendererElem;
356 }
357 
359 {
360  Q_UNUSED( feature )
361  return nullptr;
362 }
363 
365 {
366  return QgsSymbolList();
367 }
368 
370 {
371  QSet<QString> attributes;
372 
373  // mAttrName can contain either attribute name or an expression.
374  // Sometimes it is not possible to distinguish between those two,
375  // e.g. "a - b" can be both a valid attribute name or expression.
376  // Since we do not have access to fields here, try both options.
377  attributes << mWeightExpressionString;
378 
379  QgsExpression testExpr( mWeightExpressionString );
380  if ( !testExpr.hasParserError() )
381  attributes.unite( testExpr.referencedColumns() );
382 
383  return attributes;
384 }
385 
387 {
388  if ( renderer->type() == QLatin1String( "heatmapRenderer" ) )
389  {
390  return dynamic_cast<QgsHeatmapRenderer *>( renderer->clone() );
391  }
392  else
393  {
394  return new QgsHeatmapRenderer();
395  }
396 }
397 
399 {
400  delete mGradientRamp;
401  mGradientRamp = ramp;
402 }
int lookupField(const QString &fieldName) const
Looks up field&#39;s index from the field name.
Definition: qgsfields.cpp:324
Class for parsing and evaluation of expressions (formerly called "search strings").
bool hasParserError() const
Returns true if an error occurred when parsing the input expression.
The class is used as a container of context for various read/write operations on other objects...
double convertToMapUnits(double size, QgsUnitTypes::RenderUnit unit, const QgsMapUnitScale &scale=QgsMapUnitScale()) const
Converts a size from the specified units to map units.
A rectangle specified with double values.
Definition: qgsrectangle.h:41
QgsFeatureRequest::OrderBy mOrderBy
Definition: qgsrenderer.h:521
bool isMultipart() const
Returns true if WKB of the geometry is of WKBMulti* type.
QString dump() const override
Returns debug information about this renderer.
Abstract base class for all rendered symbols.
Definition: qgssymbol.h:61
OperationResult transform(const QgsCoordinateTransform &ct, QgsCoordinateTransform::TransformDirection direction=QgsCoordinateTransform::ForwardTransform, bool transformZ=false) SIP_THROW(QgsCsException)
Transforms this geometry as described by the coordinate transform ct.
void setColorRamp(QgsColorRamp *ramp)
Sets the color ramp to use for shading the heatmap.
void setXMaximum(double x)
Set the maximum x value.
Definition: qgsrectangle.h:135
void modifyRequestExtent(QgsRectangle &extent, QgsRenderContext &context) override
Allows for a renderer to modify the extent of a feature request prior to rendering.
QgsSymbolList symbols(QgsRenderContext &context) const override
virtual QgsColorRamp * clone() const =0
Creates a clone of the color ramp.
double y
Definition: qgspointxy.h:48
A class to represent a 2D point.
Definition: qgspointxy.h:43
QgsFeatureRequest::OrderBy orderBy() const
Gets the order in which features shall be processed by this renderer.
bool renderFeature(const QgsFeature &feature, QgsRenderContext &context, int layer=-1, bool selected=false, bool drawVertexMarker=false) override SIP_THROW(QgsCsException)
Render a feature using this renderer in the given context.
Abstract base class for color ramps.
Definition: qgscolorramp.h:31
static bool isDefaultStack(QgsPaintEffect *effect)
Tests whether a paint effect matches the default effects stack.
static QDomElement saveColorRamp(const QString &name, QgsColorRamp *ramp, QDomDocument &doc)
Encodes a color ramp&#39;s settings to an XML element.
bool renderingStopped() const
Returns true if the rendering operation has been stopped and any ongoing rendering should be canceled...
static QString encodeMapUnitScale(const QgsMapUnitScale &mapUnitScale)
Container of fields for a vector layer.
Definition: qgsfields.h:42
A geometry is the spatial representation of a feature.
Definition: qgsgeometry.h:111
#define RENDERER_TAG_NAME
Definition: qgsrenderer.h:49
void setWeightExpression(const QString &expression)
Sets the expression used for weighting points when generating the heatmap.
QVector< QgsPointXY > QgsMultiPointXY
A collection of QgsPoints that share a common collection of attributes.
Definition: qgsgeometry.h:79
QgsPaintEffect * mPaintEffect
Definition: qgsrenderer.h:505
QSet< QString > referencedColumns() const
Gets list of columns referenced by the expression.
The feature class encapsulates a single feature including its id, geometry and a list of field/values...
Definition: qgsfeature.h:55
bool hasGeometry() const
Returns true if the feature has an associated geometry.
Definition: qgsfeature.cpp:197
virtual QColor color(double value) const =0
Returns the color corresponding to a specified value.
bool isValid() const
Returns true if the coordinate transform is valid, ie both the source and destination CRS have been s...
QSet< QString > usedAttributes(const QgsRenderContext &context) const override
Returns a list of attributes required by this renderer.
static QgsFeatureRenderer * create(QDomElement &element, const QgsReadWriteContext &context)
Creates a new heatmap renderer instance from XML.
QgsPointXY transform(const QgsPointXY &p) const
Transform the point from map (world) coordinates to device coordinates.
QList< QgsSymbol * > QgsSymbolList
Definition: qgsrenderer.h:43
QString type() const
Definition: qgsrenderer.h:129
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 setYMinimum(double y)
Set the minimum y value.
Definition: qgsrectangle.h:140
static QgsColorRamp * loadColorRamp(QDomElement &element)
Creates a color ramp from the settings encoded in an XML element.
QgsCoordinateTransform coordinateTransform() const
Returns the current coordinate transform for the context.
void startRender(QgsRenderContext &context, const QgsFields &fields) override
Must be called when a new render cycle is started.
void setRenderQuality(const int quality)
Sets the render quality used for drawing the heatmap.
void setRadiusUnit(const QgsUnitTypes::RenderUnit unit)
Sets the units used for the heatmap&#39;s radius.
double x
Definition: qgspointxy.h:47
double yMinimum() const
Returns the y minimum value (bottom side of rectangle).
Definition: qgsrectangle.h:177
QgsSymbol * symbolForFeature(const QgsFeature &feature, QgsRenderContext &context) const override
QDomElement save(QDomDocument &doc, const QgsReadWriteContext &context) override
store renderer info to XML element
QgsExpressionContext & expressionContext()
Gets the expression context.
void setRadius(const double radius)
Sets the radius for the heatmap.
double xMaximum() const
Returns the x maximum value (right side of rectangle).
Definition: qgsrectangle.h:162
QgsHeatmapRenderer * clone() const override
Create a deep copy of this renderer.
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.
const QgsMapToPixel & mapToPixel() const
Returns the context&#39;s map to pixel transform, which transforms between map coordinates and device coo...
QgsPointXY asPoint() const
Returns the contents of the geometry as a 2-dimensional point.
virtual void startRender(QgsRenderContext &context, const QgsFields &fields)
Must be called when a new render cycle is started.
Definition: qgsrenderer.cpp:93
void setYMaximum(double y)
Set the maximum y value.
Definition: qgsrectangle.h:145
void CORE_EXPORT save(QDomElement &elem) const
Serialize to XML.
void stopRender(QgsRenderContext &context) override
Must be called when a render cycle has finished, to allow the renderer to clean up.
virtual void stopRender(QgsRenderContext &context)
Must be called when a render cycle has finished, to allow the renderer to clean up.
static QgsHeatmapRenderer * convertFromRenderer(const QgsFeatureRenderer *renderer)
Class for doing transforms between two map coordinate systems.
void copyRendererData(QgsFeatureRenderer *destRenderer) const
Clones generic renderer data to another renderer.
Definition: qgsrenderer.cpp:49
double xMinimum() const
Returns the x minimum value (left side of rectangle).
Definition: qgsrectangle.h:167
static QgsMapUnitScale decodeMapUnitScale(const QString &str)
double yMaximum() const
Returns the y maximum value (top side of rectangle).
Definition: qgsrectangle.h:172
QgsWkbTypes::GeometryType type
Definition: qgsgeometry.h:115
QgsMultiPointXY asMultiPoint() const
Returns the contents of the geometry as a multi-point.
QgsGeometry geometry
Definition: qgsfeature.h:67
A vector of attributes.
Definition: qgsattributes.h:57
Gradient color ramp, which smoothly interpolates between two colors and also supports optional extra ...
Definition: qgscolorramp.h:139
void setMaximumValue(const double value)
Sets the maximum value used for shading the heatmap.
virtual QgsFeatureRenderer * clone() const =0
Create a deep copy of this renderer.
QgsAttributes attributes
Definition: qgsfeature.h:65
void setXMinimum(double x)
Set the minimum x value.
Definition: qgsrectangle.h:130
virtual bool saveProperties(QDomDocument &doc, QDomElement &element) const
Saves the current state of the effect to a DOM element.