QGIS API Documentation 3.99.0-Master (26c88405ac0)
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 <memory>
19
20#include "qgscolorramp.h"
21#include "qgscolorrampimpl.h"
23#include "qgsfeature.h"
24#include "qgsrendercontext.h"
26#include "qgssymbol.h"
27#include "qgssymbollayerutils.h"
28
29#include <QDomDocument>
30#include <QDomElement>
31
33 : QgsFeatureRenderer( QStringLiteral( "heatmapRenderer" ) )
34{
35 mGradientRamp = new QgsGradientColorRamp( QColor( 255, 255, 255 ), QColor( 0, 0, 0 ) );
36 mLegendSettings.setMinimumLabel( QObject::tr( "Minimum" ) );
37 mLegendSettings.setMaximumLabel( QObject::tr( "Maximum" ) );
38}
39
41{
42 delete mGradientRamp;
43}
44
45void 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 = std::make_unique<QgsExpression>( mWeightExpressionString );
69 mWeightExpression->prepare( &context.expressionContext() );
70 }
71
72 bool ok = false;
73 const double dataDefinedExplicitMax = dataDefinedProperties().valueAsDouble( Property::HeatmapMaximum, context.expressionContext(), mExplicitMax, &ok );
74 if ( ok )
75 mExplicitMax = dataDefinedExplicitMax;
76
77 const double dataDefinedRadius = dataDefinedProperties().valueAsDouble( Property::HeatmapRadius, context.expressionContext(), mRadius, &ok );
78 if ( ok )
79 mRadius = dataDefinedRadius;
80
81 initializeValues( context );
82}
83
84QgsMultiPointXY QgsHeatmapRenderer::convertToMultipoint( const QgsGeometry *geom )
85{
86 QgsMultiPointXY multiPoint;
87 if ( !geom->isMultipart() )
88 {
89 multiPoint << geom->asPoint();
90 }
91 else
92 {
93 multiPoint = geom->asMultiPoint();
94 }
95
96 return multiPoint;
97}
98
99bool QgsHeatmapRenderer::renderFeature( const QgsFeature &feature, QgsRenderContext &context, int layer, bool selected, bool drawVertexMarker )
100{
101 Q_UNUSED( layer )
102 Q_UNUSED( selected )
103 Q_UNUSED( drawVertexMarker )
104
105 if ( !context.painter() )
106 {
107 return false;
108 }
109
110 if ( !feature.hasGeometry() || feature.geometry().type() != Qgis::GeometryType::Point )
111 {
112 //can only render point type
113 return false;
114 }
115
116 double weight = 1.0;
117 if ( !mWeightExpressionString.isEmpty() )
118 {
119 QVariant value;
120 if ( mWeightAttrNum == -1 )
121 {
122 Q_ASSERT( mWeightExpression.get() );
123 value = mWeightExpression->evaluate( &context.expressionContext() );
124 }
125 else
126 {
127 const QgsAttributes attrs = feature.attributes();
128 value = attrs.value( mWeightAttrNum );
129 }
130 bool ok = false;
131 const double evalWeight = value.toDouble( &ok );
132 if ( ok )
133 {
134 weight = evalWeight;
135 }
136 }
137
138 const int width = context.painter()->device()->width() / mRenderQuality;
139 const int height = context.painter()->device()->height() / mRenderQuality;
140
141 //transform geometry if required
142 QgsGeometry geom = feature.geometry();
143 const QgsCoordinateTransform xform = context.coordinateTransform();
144 if ( xform.isValid() )
145 {
146 geom.transform( xform );
147 }
148
149 //convert point to multipoint
150 const QgsMultiPointXY multiPoint = convertToMultipoint( &geom );
151
152 //loop through all points in multipoint
153 for ( QgsMultiPointXY::const_iterator pointIt = multiPoint.constBegin(); pointIt != multiPoint.constEnd(); ++pointIt )
154 {
155 const QgsPointXY pixel = context.mapToPixel().transform( *pointIt );
156 const int pointX = pixel.x() / mRenderQuality;
157 const int pointY = pixel.y() / mRenderQuality;
158 for ( int x = std::max( pointX - mRadiusPixels, 0 ); x < std::min( pointX + mRadiusPixels, width ); ++x )
159 {
160 if ( context.renderingStopped() )
161 break;
162
163 for ( int y = std::max( pointY - mRadiusPixels, 0 ); y < std::min( pointY + mRadiusPixels, height ); ++y )
164 {
165 const int index = y * width + x;
166 if ( index >= mValues.count() )
167 {
168 continue;
169 }
170 const double distanceSquared = std::pow( pointX - x, 2.0 ) + std::pow( pointY - y, 2.0 );
171 if ( distanceSquared > mRadiusSquared )
172 {
173 continue;
174 }
175
176 const double score = weight * quarticKernel( std::sqrt( distanceSquared ), mRadiusPixels );
177 const double value = mValues.at( index ) + score;
178 if ( value > mCalculatedMaxValue )
179 {
180 mCalculatedMaxValue = value;
181 }
182 mValues[ index ] = value;
183 }
184 }
185 }
186
187 mFeaturesRendered++;
188#if 0
189 //TODO - enable progressive rendering
190 if ( mFeaturesRendered % 200 == 0 )
191 {
192 renderImage( context );
193 }
194#endif
195 return true;
196}
197
198
199double QgsHeatmapRenderer::uniformKernel( const double distance, const int bandwidth ) const
200{
201 Q_UNUSED( distance )
202 Q_UNUSED( bandwidth )
203 return 1.0;
204}
205
206double QgsHeatmapRenderer::quarticKernel( const double distance, const int bandwidth ) const
207{
208 return std::pow( 1. - std::pow( distance / static_cast< double >( bandwidth ), 2 ), 2 );
209}
210
211double QgsHeatmapRenderer::triweightKernel( const double distance, const int bandwidth ) const
212{
213 return std::pow( 1. - std::pow( distance / static_cast< double >( bandwidth ), 2 ), 3 );
214}
215
216double QgsHeatmapRenderer::epanechnikovKernel( const double distance, const int bandwidth ) const
217{
218 return ( 1. - std::pow( distance / static_cast< double >( bandwidth ), 2 ) );
219}
220
221double QgsHeatmapRenderer::triangularKernel( const double distance, const int bandwidth ) const
222{
223 return ( 1. - ( distance / static_cast< double >( bandwidth ) ) );
224}
225
227{
229
230 renderImage( context );
231 mWeightExpression.reset();
232}
233
234void QgsHeatmapRenderer::renderImage( QgsRenderContext &context )
235{
236 if ( !context.painter() || !mGradientRamp || context.renderingStopped() )
237 {
238 return;
239 }
240
241 QImage image( context.painter()->device()->width() / mRenderQuality,
242 context.painter()->device()->height() / mRenderQuality,
243 QImage::Format_ARGB32 );
244 image.fill( Qt::transparent );
245
246 const double scaleMax = mExplicitMax > 0 ? mExplicitMax : mCalculatedMaxValue;
247
248 int idx = 0;
249 double pixVal = 0;
250 QColor pixColor;
251 for ( int heightIndex = 0; heightIndex < image.height(); ++heightIndex )
252 {
253 if ( context.renderingStopped() )
254 break;
255
256 QRgb *scanLine = reinterpret_cast< QRgb * >( image.scanLine( heightIndex ) );
257 for ( int widthIndex = 0; widthIndex < image.width(); ++widthIndex )
258 {
259 //scale result to fit in the range [0, 1]
260 pixVal = mValues.at( idx ) > 0 ? std::min( ( mValues.at( idx ) / scaleMax ), 1.0 ) : 0;
261
262 //convert value to color from ramp
263 pixColor = mGradientRamp->color( pixVal );
264
265 scanLine[widthIndex] = pixColor.rgba();
266 idx++;
267 }
268 }
269
270 if ( mRenderQuality > 1 )
271 {
272 const QImage resized = image.scaled( context.painter()->device()->width(),
273 context.painter()->device()->height() );
274 context.painter()->drawImage( 0, 0, resized );
275 }
276 else
277 {
278 context.painter()->drawImage( 0, 0, image );
279 }
280}
281
283{
284 return QStringLiteral( "[HEATMAP]" );
285}
286
288{
289 QgsHeatmapRenderer *newRenderer = new QgsHeatmapRenderer();
290 if ( mGradientRamp )
291 {
292 newRenderer->setColorRamp( mGradientRamp->clone() );
293 }
294 newRenderer->setRadius( mRadius );
295 newRenderer->setRadiusUnit( mRadiusUnit );
296 newRenderer->setRadiusMapUnitScale( mRadiusMapUnitScale );
297 newRenderer->setMaximumValue( mExplicitMax );
298 newRenderer->setRenderQuality( mRenderQuality );
299 newRenderer->setWeightExpression( mWeightExpressionString );
300 newRenderer->setLegendSettings( mLegendSettings );
301 copyRendererData( newRenderer );
302
303 return newRenderer;
304}
305
307{
308 //we need to expand out the request extent so that it includes points which are up to the heatmap radius outside of the
309 //actual visible extent
310 const double extension = context.convertToMapUnits( mRadius, mRadiusUnit, mRadiusMapUnitScale );
311 extent.setXMinimum( extent.xMinimum() - extension );
312 extent.setXMaximum( extent.xMaximum() + extension );
313 extent.setYMinimum( extent.yMinimum() - extension );
314 extent.setYMaximum( extent.yMaximum() + extension );
315}
316
318{
319 Q_UNUSED( context )
321 r->setRadius( element.attribute( QStringLiteral( "radius" ), QStringLiteral( "50.0" ) ).toFloat() );
322 r->setRadiusUnit( static_cast< Qgis::RenderUnit >( element.attribute( QStringLiteral( "radius_unit" ), QStringLiteral( "0" ) ).toInt() ) );
323 r->setRadiusMapUnitScale( QgsSymbolLayerUtils::decodeMapUnitScale( element.attribute( QStringLiteral( "radius_map_unit_scale" ), QString() ) ) );
324 r->setMaximumValue( element.attribute( QStringLiteral( "max_value" ), QStringLiteral( "0.0" ) ).toFloat() );
325 r->setRenderQuality( element.attribute( QStringLiteral( "quality" ), QStringLiteral( "0" ) ).toInt() );
326 r->setWeightExpression( element.attribute( QStringLiteral( "weight_expression" ) ) );
327
328 QDomElement sourceColorRampElem = element.firstChildElement( QStringLiteral( "colorramp" ) );
329 if ( !sourceColorRampElem.isNull() && sourceColorRampElem.attribute( QStringLiteral( "name" ) ) == QLatin1String( "[source]" ) )
330 {
331 r->setColorRamp( QgsSymbolLayerUtils::loadColorRamp( sourceColorRampElem ).release() );
332 }
333
335 legendSettings.readXml( element, context );
337
338 return r;
339}
340
341QDomElement QgsHeatmapRenderer::save( QDomDocument &doc, const QgsReadWriteContext &context )
342{
343 QDomElement rendererElem = doc.createElement( RENDERER_TAG_NAME );
344 rendererElem.setAttribute( QStringLiteral( "type" ), QStringLiteral( "heatmapRenderer" ) );
345 rendererElem.setAttribute( QStringLiteral( "radius" ), QString::number( mRadius ) );
346 rendererElem.setAttribute( QStringLiteral( "radius_unit" ), QString::number( static_cast< int >( mRadiusUnit ) ) );
347 rendererElem.setAttribute( QStringLiteral( "radius_map_unit_scale" ), QgsSymbolLayerUtils::encodeMapUnitScale( mRadiusMapUnitScale ) );
348 rendererElem.setAttribute( QStringLiteral( "max_value" ), QString::number( mExplicitMax ) );
349 rendererElem.setAttribute( QStringLiteral( "quality" ), QString::number( mRenderQuality ) );
350 rendererElem.setAttribute( QStringLiteral( "weight_expression" ), mWeightExpressionString );
351
352 if ( mGradientRamp )
353 {
354 const QDomElement colorRampElem = QgsSymbolLayerUtils::saveColorRamp( QStringLiteral( "[source]" ), mGradientRamp, doc );
355 rendererElem.appendChild( colorRampElem );
356 }
357 mLegendSettings.writeXml( doc, rendererElem, context );
358
359 saveRendererData( doc, rendererElem, context );
360
361 return rendererElem;
362}
363
365{
366 Q_UNUSED( feature )
367 return nullptr;
368}
369
374
376{
377 QSet<QString> attributes;
378
379 // mAttrName can contain either attribute name or an expression.
380 // Sometimes it is not possible to distinguish between those two,
381 // e.g. "a - b" can be both a valid attribute name or expression.
382 // Since we do not have access to fields here, try both options.
383 attributes << mWeightExpressionString;
384
385 const QgsExpression testExpr( mWeightExpressionString );
386 if ( !testExpr.hasParserError() )
387 attributes.unite( testExpr.referencedColumns() );
388
389 return attributes;
390}
391
393{
394 if ( renderer->type() == QLatin1String( "heatmapRenderer" ) )
395 {
396 return dynamic_cast<QgsHeatmapRenderer *>( renderer->clone() );
397 }
398 else
399 {
400 auto res = std::make_unique< QgsHeatmapRenderer >();
401 renderer->copyRendererData( res.get() );
402 return res.release();
403 }
404}
405
407{
408 if ( mGradientRamp )
409 {
410 QgsStyleColorRampEntity entity( mGradientRamp );
411 if ( !visitor->visit( QgsStyleEntityVisitorInterface::StyleLeaf( &entity ) ) )
412 return false;
413 }
414 return true;
415}
416
417QList<QgsLayerTreeModelLegendNode *> QgsHeatmapRenderer::createLegendNodes( QgsLayerTreeLayer *nodeLayer ) const
418{
419 return
420 {
421 new QgsColorRampLegendNode( nodeLayer,
422 mGradientRamp->clone(),
423 mLegendSettings,
424 0,
425 1 )
426 };
427}
428
430{
431 delete mGradientRamp;
432 mGradientRamp = ramp;
433}
434
436{
437 mLegendSettings = settings;
438}
@ Point
Points.
Definition qgis.h:359
RenderUnit
Rendering size units.
Definition qgis.h:5183
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.
A legend node which renders a color ramp.
Abstract base class for color ramps.
Handles coordinate transforms between two coordinate systems.
bool isValid() const
Returns true if the coordinate transform is valid, ie both the source and destination CRS have been s...
Handles 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.
QgsPropertyCollection & dataDefinedProperties()
Returns a reference to the renderer's property collection, used for data defined overrides.
QgsFeatureRenderer(const QString &type)
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 ...
void setColorRamp(QgsColorRamp *ramp)
Sets the color ramp to use for shading the heatmap.
QgsSymbol * symbolForFeature(const QgsFeature &feature, QgsRenderContext &context) const override
To be overridden.
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
Returns list of symbols used by the renderer.
Layer tree node points to a map layer.
QgsLayerTreeLayer * clone() const override
Create a copy of the node. Returns new instance.
QgsPointXY transform(const QgsPointXY &p) const
Transforms a point p from map (world) coordinates to device coordinates.
Represents a 2D point.
Definition qgspointxy.h:60
double y
Definition qgspointxy.h:64
double x
Definition qgspointxy.h:63
A container for the context for various read/write operations on 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:1429
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 std::unique_ptr< 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, const 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:96
#define RENDERER_TAG_NAME
Definition qgsrenderer.h:55
QList< QgsSymbol * > QgsSymbolList
Definition qgsrenderer.h:49
Contains information relating to the style entity currently being visited.