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