QGIS API Documentation  3.4.15-Madeira (e83d02e274)
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 }
Class for parsing and evaluation of expressions (formerly called "search strings").
The class is used as a container of context for various read/write operations on other objects...
QString type() const
Definition: qgsrenderer.h:129
void CORE_EXPORT save(QDomElement &elem) const
Serialize to XML.
A rectangle specified with double values.
Definition: qgsrectangle.h:40
bool hasParserError() const
Returns true if an error occurred when parsing the input expression.
QgsFeatureRequest::OrderBy mOrderBy
Definition: qgsrenderer.h:521
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:134
void modifyRequestExtent(QgsRectangle &extent, QgsRenderContext &context) override
Allows for a renderer to modify the extent of a feature request prior to rendering.
double yMaximum() const
Returns the y maximum value (top side of rectangle).
Definition: qgsrectangle.h:171
QgsSymbolList symbols(QgsRenderContext &context) const override
virtual QgsColorRamp * clone() const =0
Creates a clone of the color ramp.
double convertToPainterUnits(double size, QgsUnitTypes::RenderUnit unit, const QgsMapUnitScale &scale=QgsMapUnitScale()) const
Converts a size from the specified units to painter units (pixels).
double y
Definition: qgspointxy.h:48
A class to represent a 2D point.
Definition: qgspointxy.h:43
QSet< QString > referencedColumns() const
Gets list of columns referenced by the expression.
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
double convertToMapUnits(double size, QgsUnitTypes::RenderUnit unit, const QgsMapUnitScale &scale=QgsMapUnitScale()) const
Converts a size from the specified units to map units.
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.
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:106
#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:74
QgsPaintEffect * mPaintEffect
Definition: qgsrenderer.h:505
QgsPointXY asPoint() const
Returns the contents of the geometry as a 2-dimensional point.
The feature class encapsulates a single feature including its id, geometry and a list of field/values...
Definition: qgsfeature.h:55
virtual QColor color(double value) const =0
Returns the color corresponding to a specified value.
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.
QList< QgsSymbol * > QgsSymbolList
Definition: qgsrenderer.h:43
double yMinimum() const
Returns the y minimum value (bottom side of rectangle).
Definition: qgsrectangle.h:176
double xMaximum() const
Returns the x maximum value (right side of rectangle).
Definition: qgsrectangle.h:161
A renderer which draws points as a live heatmap.
bool renderingStopped() const
Returns TRUE if the rendering operation has been stopped and any ongoing rendering should be canceled...
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:139
static QgsColorRamp * loadColorRamp(QDomElement &element)
Creates a color ramp from the settings encoded in an XML element.
int lookupField(const QString &fieldName) const
Looks up field&#39;s index from the field name.
Definition: qgsfields.cpp:320
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
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.
QgsHeatmapRenderer * clone() const override
Create a deep copy of this renderer.
virtual bool saveProperties(QDomDocument &doc, QDomElement &element) const
Saves the current state of the effect to a DOM element.
Contains information about the context of a rendering operation.
QPainter * painter()
Returns the destination QPainter for the render operation.
virtual void startRender(QgsRenderContext &context, const QgsFields &fields)
Must be called when a new render cycle is started.
Definition: qgsrenderer.cpp:92
void setYMaximum(double y)
Set the maximum y value.
Definition: qgsrectangle.h:144
bool isMultipart() const
Returns true if WKB of the geometry is of WKBMulti* type.
void copyRendererData(QgsFeatureRenderer *destRenderer) const
Clones generic renderer data to another renderer.
Definition: qgsrenderer.cpp:48
bool hasGeometry() const
Returns true if the feature has an associated geometry.
Definition: qgsfeature.cpp:197
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.
const QgsMapToPixel & mapToPixel() const
Returns the context&#39;s map to pixel transform, which transforms between map coordinates and device coo...
QgsMultiPointXY asMultiPoint() const
Returns contents of the geometry as a multi point if wkbType is WKBMultiPoint, otherwise an empty lis...
QgsFeatureRequest::OrderBy orderBy() const
Gets the order in which features shall be processed by this renderer.
static QgsMapUnitScale decodeMapUnitScale(const QString &str)
QgsGeometry geometry
Definition: qgsfeature.h:67
QgsCoordinateTransform coordinateTransform() const
Returns the current coordinate transform for the context.
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
QgsPointXY transform(const QgsPointXY &p) const
Transform the point from map (world) coordinates to device coordinates.
QgsWkbTypes::GeometryType type() const
Returns type of the geometry as a QgsWkbTypes::GeometryType.
double xMinimum() const
Returns the x minimum value (left side of rectangle).
Definition: qgsrectangle.h:166
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:129
bool isValid() const
Returns true if the coordinate transform is valid, ie both the source and destination CRS have been s...