QGIS API Documentation  3.18.1-Zürich (202f1bf7e5)
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 #include "qgsstyleentityvisitor.h"
31 
32 #include <QDomDocument>
33 #include <QDomElement>
34 
36  : QgsFeatureRenderer( QStringLiteral( "heatmapRenderer" ) )
37 {
38  mGradientRamp = new QgsGradientColorRamp( QColor( 255, 255, 255 ), QColor( 0, 0, 0 ) );
39 }
40 
42 {
43  delete mGradientRamp;
44 }
45 
46 void QgsHeatmapRenderer::initializeValues( QgsRenderContext &context )
47 {
48  mValues.resize( context.painter()->device()->width() * context.painter()->device()->height() / ( mRenderQuality * mRenderQuality ) );
49  mValues.fill( 0 );
50  mCalculatedMaxValue = 0;
51  mFeaturesRendered = 0;
52  mRadiusPixels = std::round( context.convertToPainterUnits( mRadius, mRadiusUnit, mRadiusMapUnitScale ) / mRenderQuality );
53  mRadiusSquared = mRadiusPixels * mRadiusPixels;
54 }
55 
57 {
58  QgsFeatureRenderer::startRender( context, fields );
59 
60  if ( !context.painter() )
61  {
62  return;
63  }
64 
65  // find out classification attribute index from name
66  mWeightAttrNum = fields.lookupField( mWeightExpressionString );
67  if ( mWeightAttrNum == -1 )
68  {
69  mWeightExpression.reset( new QgsExpression( mWeightExpressionString ) );
70  mWeightExpression->prepare( &context.expressionContext() );
71  }
72 
73  initializeValues( context );
74 }
75 
76 QgsMultiPointXY QgsHeatmapRenderer::convertToMultipoint( const QgsGeometry *geom )
77 {
78  QgsMultiPointXY multiPoint;
79  if ( !geom->isMultipart() )
80  {
81  multiPoint << geom->asPoint();
82  }
83  else
84  {
85  multiPoint = geom->asMultiPoint();
86  }
87 
88  return multiPoint;
89 }
90 
91 bool QgsHeatmapRenderer::renderFeature( const QgsFeature &feature, QgsRenderContext &context, int layer, bool selected, bool drawVertexMarker )
92 {
93  Q_UNUSED( layer )
94  Q_UNUSED( selected )
95  Q_UNUSED( drawVertexMarker )
96 
97  if ( !context.painter() )
98  {
99  return false;
100  }
101 
102  if ( !feature.hasGeometry() || feature.geometry().type() != QgsWkbTypes::PointGeometry )
103  {
104  //can only render point type
105  return false;
106  }
107 
108  double weight = 1.0;
109  if ( !mWeightExpressionString.isEmpty() )
110  {
111  QVariant value;
112  if ( mWeightAttrNum == -1 )
113  {
114  Q_ASSERT( mWeightExpression.get() );
115  value = mWeightExpression->evaluate( &context.expressionContext() );
116  }
117  else
118  {
119  QgsAttributes attrs = feature.attributes();
120  value = attrs.value( mWeightAttrNum );
121  }
122  bool ok = false;
123  double evalWeight = value.toDouble( &ok );
124  if ( ok )
125  {
126  weight = evalWeight;
127  }
128  }
129 
130  int width = context.painter()->device()->width() / mRenderQuality;
131  int height = context.painter()->device()->height() / mRenderQuality;
132 
133  //transform geometry if required
134  QgsGeometry geom = feature.geometry();
136  if ( xform.isValid() )
137  {
138  geom.transform( xform );
139  }
140 
141  //convert point to multipoint
142  QgsMultiPointXY multiPoint = convertToMultipoint( &geom );
143 
144  //loop through all points in multipoint
145  for ( QgsMultiPointXY::const_iterator pointIt = multiPoint.constBegin(); pointIt != multiPoint.constEnd(); ++pointIt )
146  {
147  QgsPointXY pixel = context.mapToPixel().transform( *pointIt );
148  int pointX = pixel.x() / mRenderQuality;
149  int pointY = pixel.y() / mRenderQuality;
150  for ( int x = std::max( pointX - mRadiusPixels, 0 ); x < std::min( pointX + mRadiusPixels, width ); ++x )
151  {
152  if ( context.renderingStopped() )
153  break;
154 
155  for ( int y = std::max( pointY - mRadiusPixels, 0 ); y < std::min( pointY + mRadiusPixels, height ); ++y )
156  {
157  int index = y * width + x;
158  if ( index >= mValues.count() )
159  {
160  continue;
161  }
162  double distanceSquared = std::pow( pointX - x, 2.0 ) + std::pow( pointY - y, 2.0 );
163  if ( distanceSquared > mRadiusSquared )
164  {
165  continue;
166  }
167 
168  double score = weight * quarticKernel( std::sqrt( distanceSquared ), mRadiusPixels );
169  double value = mValues.at( index ) + score;
170  if ( value > mCalculatedMaxValue )
171  {
172  mCalculatedMaxValue = value;
173  }
174  mValues[ index ] = value;
175  }
176  }
177  }
178 
179  mFeaturesRendered++;
180 #if 0
181  //TODO - enable progressive rendering
182  if ( mFeaturesRendered % 200 == 0 )
183  {
184  renderImage( context );
185  }
186 #endif
187  return true;
188 }
189 
190 
191 double QgsHeatmapRenderer::uniformKernel( const double distance, const int bandwidth ) const
192 {
193  Q_UNUSED( distance )
194  Q_UNUSED( bandwidth )
195  return 1.0;
196 }
197 
198 double QgsHeatmapRenderer::quarticKernel( const double distance, const int bandwidth ) const
199 {
200  return std::pow( 1. - std::pow( distance / static_cast< double >( bandwidth ), 2 ), 2 );
201 }
202 
203 double QgsHeatmapRenderer::triweightKernel( const double distance, const int bandwidth ) const
204 {
205  return std::pow( 1. - std::pow( distance / static_cast< double >( bandwidth ), 2 ), 3 );
206 }
207 
208 double QgsHeatmapRenderer::epanechnikovKernel( const double distance, const int bandwidth ) const
209 {
210  return ( 1. - std::pow( distance / static_cast< double >( bandwidth ), 2 ) );
211 }
212 
213 double QgsHeatmapRenderer::triangularKernel( const double distance, const int bandwidth ) const
214 {
215  return ( 1. - ( distance / static_cast< double >( bandwidth ) ) );
216 }
217 
219 {
221 
222  renderImage( context );
223  mWeightExpression.reset();
224 }
225 
226 void QgsHeatmapRenderer::renderImage( QgsRenderContext &context )
227 {
228  if ( !context.painter() || !mGradientRamp || context.renderingStopped() )
229  {
230  return;
231  }
232 
233  QImage image( context.painter()->device()->width() / mRenderQuality,
234  context.painter()->device()->height() / mRenderQuality,
235  QImage::Format_ARGB32 );
236  image.fill( Qt::transparent );
237 
238  double scaleMax = mExplicitMax > 0 ? mExplicitMax : mCalculatedMaxValue;
239 
240  int idx = 0;
241  double pixVal = 0;
242  QColor pixColor;
243  for ( int heightIndex = 0; heightIndex < image.height(); ++heightIndex )
244  {
245  if ( context.renderingStopped() )
246  break;
247 
248  QRgb *scanLine = reinterpret_cast< QRgb * >( image.scanLine( heightIndex ) );
249  for ( int widthIndex = 0; widthIndex < image.width(); ++widthIndex )
250  {
251  //scale result to fit in the range [0, 1]
252  pixVal = mValues.at( idx ) > 0 ? std::min( ( mValues.at( idx ) / scaleMax ), 1.0 ) : 0;
253 
254  //convert value to color from ramp
255  pixColor = mGradientRamp->color( pixVal );
256 
257  scanLine[widthIndex] = pixColor.rgba();
258  idx++;
259  }
260  }
261 
262  if ( mRenderQuality > 1 )
263  {
264  QImage resized = image.scaled( context.painter()->device()->width(),
265  context.painter()->device()->height() );
266  context.painter()->drawImage( 0, 0, resized );
267  }
268  else
269  {
270  context.painter()->drawImage( 0, 0, image );
271  }
272 }
273 
275 {
276  return QStringLiteral( "[HEATMAP]" );
277 }
278 
280 {
281  QgsHeatmapRenderer *newRenderer = new QgsHeatmapRenderer();
282  if ( mGradientRamp )
283  {
284  newRenderer->setColorRamp( mGradientRamp->clone() );
285  }
286  newRenderer->setRadius( mRadius );
287  newRenderer->setRadiusUnit( mRadiusUnit );
288  newRenderer->setRadiusMapUnitScale( mRadiusMapUnitScale );
289  newRenderer->setMaximumValue( mExplicitMax );
290  newRenderer->setRenderQuality( mRenderQuality );
291  newRenderer->setWeightExpression( mWeightExpressionString );
292  copyRendererData( newRenderer );
293 
294  return newRenderer;
295 }
296 
298 {
299  //we need to expand out the request extent so that it includes points which are up to the heatmap radius outside of the
300  //actual visible extent
301  double extension = context.convertToMapUnits( mRadius, mRadiusUnit, mRadiusMapUnitScale );
302  extent.setXMinimum( extent.xMinimum() - extension );
303  extent.setXMaximum( extent.xMaximum() + extension );
304  extent.setYMinimum( extent.yMinimum() - extension );
305  extent.setYMaximum( extent.yMaximum() + extension );
306 }
307 
308 QgsFeatureRenderer *QgsHeatmapRenderer::create( QDomElement &element, const QgsReadWriteContext &context )
309 {
310  Q_UNUSED( context )
312  r->setRadius( element.attribute( QStringLiteral( "radius" ), QStringLiteral( "50.0" ) ).toFloat() );
313  r->setRadiusUnit( static_cast< QgsUnitTypes::RenderUnit >( element.attribute( QStringLiteral( "radius_unit" ), QStringLiteral( "0" ) ).toInt() ) );
314  r->setRadiusMapUnitScale( QgsSymbolLayerUtils::decodeMapUnitScale( element.attribute( QStringLiteral( "radius_map_unit_scale" ), QString() ) ) );
315  r->setMaximumValue( element.attribute( QStringLiteral( "max_value" ), QStringLiteral( "0.0" ) ).toFloat() );
316  r->setRenderQuality( element.attribute( QStringLiteral( "quality" ), QStringLiteral( "0" ) ).toInt() );
317  r->setWeightExpression( element.attribute( QStringLiteral( "weight_expression" ) ) );
318 
319  QDomElement sourceColorRampElem = element.firstChildElement( QStringLiteral( "colorramp" ) );
320  if ( !sourceColorRampElem.isNull() && sourceColorRampElem.attribute( QStringLiteral( "name" ) ) == QLatin1String( "[source]" ) )
321  {
322  r->setColorRamp( QgsSymbolLayerUtils::loadColorRamp( sourceColorRampElem ) );
323  }
324  return r;
325 }
326 
327 QDomElement QgsHeatmapRenderer::save( QDomDocument &doc, const QgsReadWriteContext &context )
328 {
329  Q_UNUSED( context )
330  QDomElement rendererElem = doc.createElement( RENDERER_TAG_NAME );
331  rendererElem.setAttribute( QStringLiteral( "type" ), QStringLiteral( "heatmapRenderer" ) );
332  rendererElem.setAttribute( QStringLiteral( "radius" ), QString::number( mRadius ) );
333  rendererElem.setAttribute( QStringLiteral( "radius_unit" ), QString::number( mRadiusUnit ) );
334  rendererElem.setAttribute( QStringLiteral( "radius_map_unit_scale" ), QgsSymbolLayerUtils::encodeMapUnitScale( mRadiusMapUnitScale ) );
335  rendererElem.setAttribute( QStringLiteral( "max_value" ), QString::number( mExplicitMax ) );
336  rendererElem.setAttribute( QStringLiteral( "quality" ), QString::number( mRenderQuality ) );
337  rendererElem.setAttribute( QStringLiteral( "weight_expression" ), mWeightExpressionString );
338  if ( mGradientRamp )
339  {
340  QDomElement colorRampElem = QgsSymbolLayerUtils::saveColorRamp( QStringLiteral( "[source]" ), mGradientRamp, doc );
341  rendererElem.appendChild( colorRampElem );
342  }
343  rendererElem.setAttribute( QStringLiteral( "forceraster" ), ( mForceRaster ? QStringLiteral( "1" ) : QStringLiteral( "0" ) ) );
344 
346  mPaintEffect->saveProperties( doc, rendererElem );
347 
348  if ( !mOrderBy.isEmpty() )
349  {
350  QDomElement orderBy = doc.createElement( QStringLiteral( "orderby" ) );
351  mOrderBy.save( orderBy );
352  rendererElem.appendChild( orderBy );
353  }
354  rendererElem.setAttribute( QStringLiteral( "enableorderby" ), ( mOrderByEnabled ? QStringLiteral( "1" ) : QStringLiteral( "0" ) ) );
355 
356  return rendererElem;
357 }
358 
360 {
361  Q_UNUSED( feature )
362  return nullptr;
363 }
364 
366 {
367  return QgsSymbolList();
368 }
369 
371 {
372  QSet<QString> attributes;
373 
374  // mAttrName can contain either attribute name or an expression.
375  // Sometimes it is not possible to distinguish between those two,
376  // e.g. "a - b" can be both a valid attribute name or expression.
377  // Since we do not have access to fields here, try both options.
378  attributes << mWeightExpressionString;
379 
380  QgsExpression testExpr( mWeightExpressionString );
381  if ( !testExpr.hasParserError() )
382  attributes.unite( testExpr.referencedColumns() );
383 
384  return attributes;
385 }
386 
388 {
389  if ( renderer->type() == QLatin1String( "heatmapRenderer" ) )
390  {
391  return dynamic_cast<QgsHeatmapRenderer *>( renderer->clone() );
392  }
393  else
394  {
395  return new QgsHeatmapRenderer();
396  }
397 }
398 
400 {
401  if ( mGradientRamp )
402  {
403  QgsStyleColorRampEntity entity( mGradientRamp );
404  if ( !visitor->visit( QgsStyleEntityVisitorInterface::StyleLeaf( &entity ) ) )
405  return false;
406  }
407  return true;
408 }
409 
411 {
412  delete mGradientRamp;
413  mGradientRamp = ramp;
414 }
A vector of attributes.
Definition: qgsattributes.h:58
Abstract base class for color ramps.
Definition: qgscolorramp.h:32
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.
QgsPaintEffect * mPaintEffect
Definition: qgsrenderer.h:529
QString type() const
Definition: qgsrenderer.h:141
void copyRendererData(QgsFeatureRenderer *destRenderer) const
Clones generic renderer data to another renderer.
Definition: qgsrenderer.cpp:50
QgsFeatureRequest::OrderBy mOrderBy
Definition: qgsrenderer.h:545
virtual void startRender(QgsRenderContext &context, const QgsFields &fields)
Must be called when a new render cycle is started.
Definition: qgsrenderer.cpp:94
QgsFeatureRequest::OrderBy orderBy() const
Gets the order in which features shall be processed by this renderer.
void CORE_EXPORT save(QDomElement &elem) const
Serialize to XML.
The feature class encapsulates a single feature including its id, geometry and a list of field/values...
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:204
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:344
A geometry is the spatial representation of a feature.
Definition: qgsgeometry.h:124
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:127
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.
Gradient color ramp, which smoothly interpolates between two colors and also supports optional extra ...
Definition: qgscolorramp.h:151
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
store renderer info to 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
Transform the point p from map (world) coordinates to device coordinates.
Definition: qgsmaptopixel.h:82
static bool isDefaultStack(QgsPaintEffect *effect)
Tests whether a paint effect matches the default effects stack.
virtual bool saveProperties(QDomDocument &doc, QDomElement &element) const
Saves the current state of the effect to a DOM element.
A class to represent a 2D point.
Definition: qgspointxy.h:44
double y
Definition: qgspointxy.h:48
Q_GADGET double x
Definition: qgspointxy.h:47
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:172
double xMaximum() const SIP_HOLDGIL
Returns the x maximum value (right side of rectangle).
Definition: qgsrectangle.h:162
double xMinimum() const SIP_HOLDGIL
Returns the x minimum value (left side of rectangle).
Definition: qgsrectangle.h:167
double yMinimum() const SIP_HOLDGIL
Returns the y minimum value (bottom side of rectangle).
Definition: qgsrectangle.h:177
void setYMinimum(double y) SIP_HOLDGIL
Set the minimum y value.
Definition: qgsrectangle.h:140
void setXMaximum(double x) SIP_HOLDGIL
Set the maximum x value.
Definition: qgsrectangle.h:135
void setXMinimum(double x) SIP_HOLDGIL
Set the minimum x value.
Definition: qgsrectangle.h:130
void setYMaximum(double y) SIP_HOLDGIL
Set the maximum y value.
Definition: qgsrectangle.h:145
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 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...
double convertToPainterUnits(double size, QgsUnitTypes::RenderUnit unit, const QgsMapUnitScale &scale=QgsMapUnitScale()) const
Converts a size from the specified units to painter units (pixels).
QgsCoordinateTransform coordinateTransform() const
Returns the current coordinate transform for the context.
A color ramp entity for QgsStyle databases.
Definition: qgsstyle.h:1233
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:65
RenderUnit
Rendering size units.
Definition: qgsunittypes.h:167
QVector< QgsPointXY > QgsMultiPointXY
A collection of QgsPoints that share a common collection of attributes.
Definition: qgsgeometry.h:81
#define RENDERER_TAG_NAME
Definition: qgsrenderer.h:51
QList< QgsSymbol * > QgsSymbolList
Definition: qgsrenderer.h:45
Contains information relating to the style entity currently being visited.