QGIS API Documentation 3.41.0-Master (af5edcb665c)
Loading...
Searching...
No Matches
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 "qgsfeature.h"
22#include "qgscolorramp.h"
23#include "qgscolorrampimpl.h"
24#include "qgsrendercontext.h"
27
28#include <QDomDocument>
29#include <QDomElement>
30
32 : QgsFeatureRenderer( QStringLiteral( "heatmapRenderer" ) )
33{
34 mGradientRamp = new QgsGradientColorRamp( QColor( 255, 255, 255 ), QColor( 0, 0, 0 ) );
35 mLegendSettings.setMinimumLabel( QObject::tr( "Minimum" ) );
36 mLegendSettings.setMaximumLabel( QObject::tr( "Maximum" ) );
37}
38
40{
41 delete mGradientRamp;
42}
43
44void QgsHeatmapRenderer::initializeValues( QgsRenderContext &context )
45{
46 mValues.resize( context.painter()->device()->width() * context.painter()->device()->height() / ( mRenderQuality * mRenderQuality ) );
47 mValues.fill( 0 );
48 mCalculatedMaxValue = 0;
49 mFeaturesRendered = 0;
50 mRadiusPixels = std::round( context.convertToPainterUnits( mRadius, mRadiusUnit, mRadiusMapUnitScale ) / mRenderQuality );
51 mRadiusSquared = mRadiusPixels * mRadiusPixels;
52}
53
55{
56 QgsFeatureRenderer::startRender( context, fields );
57
58 if ( !context.painter() )
59 {
60 return;
61 }
62
63 // find out classification attribute index from name
64 mWeightAttrNum = fields.lookupField( mWeightExpressionString );
65 if ( mWeightAttrNum == -1 )
66 {
67 mWeightExpression.reset( new QgsExpression( mWeightExpressionString ) );
68 mWeightExpression->prepare( &context.expressionContext() );
69 }
70
71 bool ok = false;
72 const double dataDefinedExplicitMax = dataDefinedProperties().valueAsDouble( Property::HeatmapMaximum, context.expressionContext(), mExplicitMax, &ok );
73 if ( ok )
74 mExplicitMax = dataDefinedExplicitMax;
75
76 const double dataDefinedRadius = dataDefinedProperties().valueAsDouble( Property::HeatmapRadius, context.expressionContext(), mRadius, &ok );
77 if ( ok )
78 mRadius = dataDefinedRadius;
79
80 initializeValues( context );
81}
82
83QgsMultiPointXY QgsHeatmapRenderer::convertToMultipoint( const QgsGeometry *geom )
84{
85 QgsMultiPointXY multiPoint;
86 if ( !geom->isMultipart() )
87 {
88 multiPoint << geom->asPoint();
89 }
90 else
91 {
92 multiPoint = geom->asMultiPoint();
93 }
94
95 return multiPoint;
96}
97
98bool QgsHeatmapRenderer::renderFeature( const QgsFeature &feature, QgsRenderContext &context, int layer, bool selected, bool drawVertexMarker )
99{
100 Q_UNUSED( layer )
101 Q_UNUSED( selected )
102 Q_UNUSED( drawVertexMarker )
103
104 if ( !context.painter() )
105 {
106 return false;
107 }
108
109 if ( !feature.hasGeometry() || feature.geometry().type() != Qgis::GeometryType::Point )
110 {
111 //can only render point type
112 return false;
113 }
114
115 double weight = 1.0;
116 if ( !mWeightExpressionString.isEmpty() )
117 {
118 QVariant value;
119 if ( mWeightAttrNum == -1 )
120 {
121 Q_ASSERT( mWeightExpression.get() );
122 value = mWeightExpression->evaluate( &context.expressionContext() );
123 }
124 else
125 {
126 const QgsAttributes attrs = feature.attributes();
127 value = attrs.value( mWeightAttrNum );
128 }
129 bool ok = false;
130 const double evalWeight = value.toDouble( &ok );
131 if ( ok )
132 {
133 weight = evalWeight;
134 }
135 }
136
137 const int width = context.painter()->device()->width() / mRenderQuality;
138 const int height = context.painter()->device()->height() / mRenderQuality;
139
140 //transform geometry if required
141 QgsGeometry geom = feature.geometry();
142 const QgsCoordinateTransform xform = context.coordinateTransform();
143 if ( xform.isValid() )
144 {
145 geom.transform( xform );
146 }
147
148 //convert point to multipoint
149 const QgsMultiPointXY multiPoint = convertToMultipoint( &geom );
150
151 //loop through all points in multipoint
152 for ( QgsMultiPointXY::const_iterator pointIt = multiPoint.constBegin(); pointIt != multiPoint.constEnd(); ++pointIt )
153 {
154 const QgsPointXY pixel = context.mapToPixel().transform( *pointIt );
155 const int pointX = pixel.x() / mRenderQuality;
156 const int pointY = pixel.y() / mRenderQuality;
157 for ( int x = std::max( pointX - mRadiusPixels, 0 ); x < std::min( pointX + mRadiusPixels, width ); ++x )
158 {
159 if ( context.renderingStopped() )
160 break;
161
162 for ( int y = std::max( pointY - mRadiusPixels, 0 ); y < std::min( pointY + mRadiusPixels, height ); ++y )
163 {
164 const int index = y * width + x;
165 if ( index >= mValues.count() )
166 {
167 continue;
168 }
169 const double distanceSquared = std::pow( pointX - x, 2.0 ) + std::pow( pointY - y, 2.0 );
170 if ( distanceSquared > mRadiusSquared )
171 {
172 continue;
173 }
174
175 const double score = weight * quarticKernel( std::sqrt( distanceSquared ), mRadiusPixels );
176 const double value = mValues.at( index ) + score;
177 if ( value > mCalculatedMaxValue )
178 {
179 mCalculatedMaxValue = value;
180 }
181 mValues[ index ] = value;
182 }
183 }
184 }
185
186 mFeaturesRendered++;
187#if 0
188 //TODO - enable progressive rendering
189 if ( mFeaturesRendered % 200 == 0 )
190 {
191 renderImage( context );
192 }
193#endif
194 return true;
195}
196
197
198double QgsHeatmapRenderer::uniformKernel( const double distance, const int bandwidth ) const
199{
200 Q_UNUSED( distance )
201 Q_UNUSED( bandwidth )
202 return 1.0;
203}
204
205double QgsHeatmapRenderer::quarticKernel( const double distance, const int bandwidth ) const
206{
207 return std::pow( 1. - std::pow( distance / static_cast< double >( bandwidth ), 2 ), 2 );
208}
209
210double QgsHeatmapRenderer::triweightKernel( const double distance, const int bandwidth ) const
211{
212 return std::pow( 1. - std::pow( distance / static_cast< double >( bandwidth ), 2 ), 3 );
213}
214
215double QgsHeatmapRenderer::epanechnikovKernel( const double distance, const int bandwidth ) const
216{
217 return ( 1. - std::pow( distance / static_cast< double >( bandwidth ), 2 ) );
218}
219
220double QgsHeatmapRenderer::triangularKernel( const double distance, const int bandwidth ) const
221{
222 return ( 1. - ( distance / static_cast< double >( bandwidth ) ) );
223}
224
226{
228
229 renderImage( context );
230 mWeightExpression.reset();
231}
232
233void QgsHeatmapRenderer::renderImage( QgsRenderContext &context )
234{
235 if ( !context.painter() || !mGradientRamp || context.renderingStopped() )
236 {
237 return;
238 }
239
240 QImage image( context.painter()->device()->width() / mRenderQuality,
241 context.painter()->device()->height() / mRenderQuality,
242 QImage::Format_ARGB32 );
243 image.fill( Qt::transparent );
244
245 const double scaleMax = mExplicitMax > 0 ? mExplicitMax : mCalculatedMaxValue;
246
247 int idx = 0;
248 double pixVal = 0;
249 QColor pixColor;
250 for ( int heightIndex = 0; heightIndex < image.height(); ++heightIndex )
251 {
252 if ( context.renderingStopped() )
253 break;
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 ? std::min( ( mValues.at( idx ) / scaleMax ), 1.0 ) : 0;
260
261 //convert value to color from ramp
262 pixColor = mGradientRamp->color( pixVal );
263
264 scanLine[widthIndex] = pixColor.rgba();
265 idx++;
266 }
267 }
268
269 if ( mRenderQuality > 1 )
270 {
271 const 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 QStringLiteral( "[HEATMAP]" );
284}
285
287{
288 QgsHeatmapRenderer *newRenderer = new QgsHeatmapRenderer();
289 if ( mGradientRamp )
290 {
291 newRenderer->setColorRamp( mGradientRamp->clone() );
292 }
293 newRenderer->setRadius( mRadius );
294 newRenderer->setRadiusUnit( mRadiusUnit );
295 newRenderer->setRadiusMapUnitScale( mRadiusMapUnitScale );
296 newRenderer->setMaximumValue( mExplicitMax );
297 newRenderer->setRenderQuality( mRenderQuality );
298 newRenderer->setWeightExpression( mWeightExpressionString );
299 newRenderer->setLegendSettings( mLegendSettings );
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 const double extension = context.convertToMapUnits( mRadius, mRadiusUnit, mRadiusMapUnitScale );
310 extent.setXMinimum( extent.xMinimum() - extension );
311 extent.setXMaximum( extent.xMaximum() + extension );
312 extent.setYMinimum( extent.yMinimum() - extension );
313 extent.setYMaximum( extent.yMaximum() + extension );
314}
315
317{
318 Q_UNUSED( context )
320 r->setRadius( element.attribute( QStringLiteral( "radius" ), QStringLiteral( "50.0" ) ).toFloat() );
321 r->setRadiusUnit( static_cast< Qgis::RenderUnit >( element.attribute( QStringLiteral( "radius_unit" ), QStringLiteral( "0" ) ).toInt() ) );
322 r->setRadiusMapUnitScale( QgsSymbolLayerUtils::decodeMapUnitScale( element.attribute( QStringLiteral( "radius_map_unit_scale" ), QString() ) ) );
323 r->setMaximumValue( element.attribute( QStringLiteral( "max_value" ), QStringLiteral( "0.0" ) ).toFloat() );
324 r->setRenderQuality( element.attribute( QStringLiteral( "quality" ), QStringLiteral( "0" ) ).toInt() );
325 r->setWeightExpression( element.attribute( QStringLiteral( "weight_expression" ) ) );
326
327 QDomElement sourceColorRampElem = element.firstChildElement( QStringLiteral( "colorramp" ) );
328 if ( !sourceColorRampElem.isNull() && sourceColorRampElem.attribute( QStringLiteral( "name" ) ) == QLatin1String( "[source]" ) )
329 {
330 r->setColorRamp( QgsSymbolLayerUtils::loadColorRamp( sourceColorRampElem ) );
331 }
332
334 legendSettings.readXml( element, context );
336
337 return r;
338}
339
340QDomElement QgsHeatmapRenderer::save( QDomDocument &doc, const QgsReadWriteContext &context )
341{
342 QDomElement rendererElem = doc.createElement( RENDERER_TAG_NAME );
343 rendererElem.setAttribute( QStringLiteral( "type" ), QStringLiteral( "heatmapRenderer" ) );
344 rendererElem.setAttribute( QStringLiteral( "radius" ), QString::number( mRadius ) );
345 rendererElem.setAttribute( QStringLiteral( "radius_unit" ), QString::number( static_cast< int >( mRadiusUnit ) ) );
346 rendererElem.setAttribute( QStringLiteral( "radius_map_unit_scale" ), QgsSymbolLayerUtils::encodeMapUnitScale( mRadiusMapUnitScale ) );
347 rendererElem.setAttribute( QStringLiteral( "max_value" ), QString::number( mExplicitMax ) );
348 rendererElem.setAttribute( QStringLiteral( "quality" ), QString::number( mRenderQuality ) );
349 rendererElem.setAttribute( QStringLiteral( "weight_expression" ), mWeightExpressionString );
350
351 if ( mGradientRamp )
352 {
353 const QDomElement colorRampElem = QgsSymbolLayerUtils::saveColorRamp( QStringLiteral( "[source]" ), mGradientRamp, doc );
354 rendererElem.appendChild( colorRampElem );
355 }
356 mLegendSettings.writeXml( doc, rendererElem, context );
357
358 saveRendererData( doc, rendererElem, context );
359
360 return rendererElem;
361}
362
364{
365 Q_UNUSED( feature )
366 return nullptr;
367}
368
373
375{
376 QSet<QString> attributes;
377
378 // mAttrName can contain either attribute name or an expression.
379 // Sometimes it is not possible to distinguish between those two,
380 // e.g. "a - b" can be both a valid attribute name or expression.
381 // Since we do not have access to fields here, try both options.
382 attributes << mWeightExpressionString;
383
384 const QgsExpression testExpr( mWeightExpressionString );
385 if ( !testExpr.hasParserError() )
386 attributes.unite( testExpr.referencedColumns() );
387
388 return attributes;
389}
390
392{
393 if ( renderer->type() == QLatin1String( "heatmapRenderer" ) )
394 {
395 return dynamic_cast<QgsHeatmapRenderer *>( renderer->clone() );
396 }
397 else
398 {
399 std::unique_ptr< QgsHeatmapRenderer > res = std::make_unique< QgsHeatmapRenderer >();
400 renderer->copyRendererData( res.get() );
401 return res.release();
402 }
403}
404
406{
407 if ( mGradientRamp )
408 {
409 QgsStyleColorRampEntity entity( mGradientRamp );
410 if ( !visitor->visit( QgsStyleEntityVisitorInterface::StyleLeaf( &entity ) ) )
411 return false;
412 }
413 return true;
414}
415
416QList<QgsLayerTreeModelLegendNode *> QgsHeatmapRenderer::createLegendNodes( QgsLayerTreeLayer *nodeLayer ) const
417{
418 return
419 {
420 new QgsColorRampLegendNode( nodeLayer,
421 mGradientRamp->clone(),
422 mLegendSettings,
423 0,
424 1 )
425 };
426}
427
429{
430 delete mGradientRamp;
431 mGradientRamp = ramp;
432}
433
435{
436 mLegendSettings = settings;
437}
RenderUnit
Rendering size units.
Definition qgis.h:4910
double valueAsDouble(int key, const QgsExpressionContext &context, double defaultValue=0.0, bool *ok=nullptr) const
Calculates the current value of the property with the specified key and interprets it as a double.
A vector of attributes.
Settings for a color ramp legend node.
void setMaximumLabel(const QString &label)
Sets the label for the maximum value on the ramp.
void readXml(const QDomElement &element, const QgsReadWriteContext &context)
Reads settings from an XML element.
void writeXml(QDomDocument &doc, QDomElement &element, const QgsReadWriteContext &context) const
Writes settings to an XML element.
void setMinimumLabel(const QString &label)
Sets the label for the minimum value on the ramp.
A legend node which renders a color ramp.
Abstract base class for color ramps.
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.
Abstract base class for all 2D vector feature renderers.
QgsPropertyCollection & dataDefinedProperties()
Returns a reference to the renderer's property collection, used for data defined overrides.
virtual void stopRender(QgsRenderContext &context)
Must be called when a render cycle has finished, to allow the renderer to clean up.
@ HeatmapRadius
Heatmap renderer radius.
@ HeatmapMaximum
Heatmap maximum value.
QString type() const
void copyRendererData(QgsFeatureRenderer *destRenderer) const
Clones generic renderer data to another renderer.
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.
virtual QgsFeatureRenderer * clone() const =0
Create a deep copy of this renderer.
The feature class encapsulates a single feature including its unique ID, geometry and a list of field...
Definition qgsfeature.h:58
QgsAttributes attributes
Definition qgsfeature.h:67
QgsGeometry geometry
Definition qgsfeature.h:69
bool hasGeometry() const
Returns true if the feature has an associated geometry.
Container of fields for a vector layer.
Definition qgsfields.h:46
Q_INVOKABLE int lookupField(const QString &fieldName) const
Looks up field's index from the field name.
A geometry is the spatial representation of a feature.
Qgis::GeometryOperationResult transform(const QgsCoordinateTransform &ct, Qgis::TransformDirection direction=Qgis::TransformDirection::Forward, bool transformZ=false)
Transforms this geometry as described by the coordinate transform ct.
QgsMultiPointXY asMultiPoint() const
Returns the contents of the geometry as a multi-point.
QgsPointXY asPoint() const
Returns the contents of the geometry as a 2-dimensional point.
Qgis::GeometryType type
bool isMultipart() const
Returns true if WKB of the geometry is of WKBMulti* type.
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.
void setLegendSettings(const QgsColorRampLegendNodeSettings &settings)
Sets the color ramp legend settings.
void setRadiusUnit(const Qgis::RenderUnit unit)
Sets the units used for the heatmap's radius.
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
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.
QList< QgsLayerTreeModelLegendNode * > createLegendNodes(QgsLayerTreeLayer *nodeLayer) const override
Returns a list of legend nodes to be used for the legend for the renderer.
QgsHeatmapRenderer * clone() const override
Create a deep copy of this renderer.
const QgsColorRampLegendNodeSettings & legendSettings() const
Returns the color ramp legend settings.
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
Layer tree node points to a map layer.
QgsPointXY transform(const QgsPointXY &p) const
Transforms a point p from map (world) coordinates to device coordinates.
A class to represent a 2D point.
Definition qgspointxy.h:60
double y
Definition qgspointxy.h:64
double x
Definition qgspointxy.h:63
The class is used as a container of context for various read/write operations on other objects.
A rectangle specified with double values.
double xMinimum
double yMinimum
double xMaximum
void setYMinimum(double y)
Set the minimum y value.
void setXMinimum(double x)
Set the minimum x value.
void setYMaximum(double y)
Set the maximum y value.
void setXMaximum(double x)
Set the maximum x value.
double yMaximum
Contains information about the context of a rendering operation.
double convertToMapUnits(double size, Qgis::RenderUnit unit, const QgsMapUnitScale &scale=QgsMapUnitScale()) const
Converts a size from the specified units to map units.
double convertToPainterUnits(double size, Qgis::RenderUnit unit, const QgsMapUnitScale &scale=QgsMapUnitScale(), Qgis::RenderSubcomponentProperty property=Qgis::RenderSubcomponentProperty::Generic) const
Converts a size from the specified units to painter units (pixels).
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...
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:1428
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:231
QVector< QgsPointXY > QgsMultiPointXY
A collection of QgsPoints that share a common collection of attributes.
Definition qgsgeometry.h:80
#define RENDERER_TAG_NAME
Definition qgsrenderer.h:53
QList< QgsSymbol * > QgsSymbolList
Definition qgsrenderer.h:47
Contains information relating to the style entity currently being visited.