QGIS API Documentation 4.3.0-Master (0978c174f8e)
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#include <QString>
32
33using namespace Qt::StringLiterals;
34
36 : QgsFeatureRenderer( u"heatmapRenderer"_s )
37{
38 mGradientRamp = std::make_unique<QgsGradientColorRamp>( QColor( 255, 255, 255 ), QColor( 0, 0, 0 ) );
39 mLegendSettings.setMinimumLabel( QObject::tr( "Minimum" ) );
40 mLegendSettings.setMaximumLabel( QObject::tr( "Maximum" ) );
41}
42
45
46void 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 = std::make_unique<QgsExpression>( mWeightExpressionString );
70 mWeightExpression->prepare( &context.expressionContext() );
71 }
72
73 bool ok = false;
74 const double dataDefinedExplicitMax = dataDefinedProperties().valueAsDouble( Property::HeatmapMaximum, context.expressionContext(), mExplicitMax, &ok );
75 if ( ok )
76 mExplicitMax = dataDefinedExplicitMax;
77
78 const double dataDefinedRadius = dataDefinedProperties().valueAsDouble( Property::HeatmapRadius, context.expressionContext(), mRadius, &ok );
79 if ( ok )
80 mRadius = dataDefinedRadius;
81
82 initializeValues( context );
83}
84
85QgsMultiPointXY QgsHeatmapRenderer::convertToMultipoint( const QgsGeometry *geom )
86{
87 QgsMultiPointXY multiPoint;
88 if ( !geom->isMultipart() )
89 {
90 multiPoint << geom->asPoint();
91 }
92 else
93 {
94 multiPoint = geom->asMultiPoint();
95 }
96
97 return multiPoint;
98}
99
100bool QgsHeatmapRenderer::renderFeature( const QgsFeature &feature, QgsRenderContext &context, int layer, bool selected, bool drawVertexMarker )
101{
102 Q_UNUSED( layer )
103 Q_UNUSED( selected )
104 Q_UNUSED( drawVertexMarker )
105
106 if ( !context.painter() )
107 {
108 return false;
109 }
110
111 if ( !feature.hasGeometry() || feature.geometry().type() != Qgis::GeometryType::Point )
112 {
113 //can only render point type
114 return false;
115 }
116
117 double weight = 1.0;
118 if ( !mWeightExpressionString.isEmpty() )
119 {
120 QVariant value;
121 if ( mWeightAttrNum == -1 )
122 {
123 Q_ASSERT( mWeightExpression.get() );
124 value = mWeightExpression->evaluate( &context.expressionContext() );
125 }
126 else
127 {
128 const QgsAttributes attrs = feature.attributes();
129 value = attrs.value( mWeightAttrNum );
130 }
131 bool ok = false;
132 const double evalWeight = value.toDouble( &ok );
133 if ( ok )
134 {
135 weight = evalWeight;
136 }
137 }
138
139 const int width = context.painter()->device()->width() / mRenderQuality;
140 const int height = context.painter()->device()->height() / mRenderQuality;
141
142 //transform geometry if required
143 QgsGeometry geom = feature.geometry();
144 const QgsCoordinateTransform xform = context.coordinateTransform();
145 if ( xform.isValid() )
146 {
147 geom.transform( xform );
148 }
149
150 //convert point to multipoint
151 const QgsMultiPointXY multiPoint = convertToMultipoint( &geom );
152
153 //loop through all points in multipoint
154 for ( QgsMultiPointXY::const_iterator pointIt = multiPoint.constBegin(); pointIt != multiPoint.constEnd(); ++pointIt )
155 {
156 const QgsPointXY pixel = context.mapToPixel().transform( *pointIt );
157 const int pointX = pixel.x() / mRenderQuality;
158 const int pointY = pixel.y() / mRenderQuality;
159 for ( int x = std::max( pointX - mRadiusPixels, 0 ); x < std::min( pointX + mRadiusPixels, width ); ++x )
160 {
161 if ( context.renderingStopped() )
162 break;
163
164 for ( int y = std::max( pointY - mRadiusPixels, 0 ); y < std::min( pointY + mRadiusPixels, height ); ++y )
165 {
166 const int index = y * width + x;
167 if ( index >= mValues.count() )
168 {
169 continue;
170 }
171 const double distanceSquared = std::pow( pointX - x, 2.0 ) + std::pow( pointY - y, 2.0 );
172 if ( distanceSquared > mRadiusSquared )
173 {
174 continue;
175 }
176
177 const double score = weight * quarticKernel( std::sqrt( distanceSquared ), mRadiusPixels );
178 const double value = mValues.at( index ) + score;
179 if ( value > mCalculatedMaxValue )
180 {
181 mCalculatedMaxValue = value;
182 }
183 mValues[index] = value;
184 }
185 }
186 }
187
188 mFeaturesRendered++;
189#if 0
190 //TODO - enable progressive rendering
191 if ( mFeaturesRendered % 200 == 0 )
192 {
193 renderImage( context );
194 }
195#endif
196 return true;
197}
198
199
200double QgsHeatmapRenderer::uniformKernel( const double distance, const int bandwidth ) const
201{
202 Q_UNUSED( distance )
203 Q_UNUSED( bandwidth )
204 return 1.0;
205}
206
207double QgsHeatmapRenderer::quarticKernel( const double distance, const int bandwidth ) const
208{
209 return std::pow( 1. - std::pow( distance / static_cast< double >( bandwidth ), 2 ), 2 );
210}
211
212double QgsHeatmapRenderer::triweightKernel( const double distance, const int bandwidth ) const
213{
214 return std::pow( 1. - std::pow( distance / static_cast< double >( bandwidth ), 2 ), 3 );
215}
216
217double QgsHeatmapRenderer::epanechnikovKernel( const double distance, const int bandwidth ) const
218{
219 return ( 1. - std::pow( distance / static_cast< double >( bandwidth ), 2 ) );
220}
221
222double QgsHeatmapRenderer::triangularKernel( const double distance, const int bandwidth ) const
223{
224 return ( 1. - ( distance / static_cast< double >( bandwidth ) ) );
225}
226
228{
230
231 renderImage( context );
232 mWeightExpression.reset();
233}
234
235void QgsHeatmapRenderer::renderImage( QgsRenderContext &context )
236{
237 if ( !context.painter() || !mGradientRamp || context.renderingStopped() )
238 {
239 return;
240 }
241
242 QImage image( context.painter()->device()->width() / mRenderQuality, context.painter()->device()->height() / mRenderQuality, 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(), context.painter()->device()->height() );
272 context.painter()->drawImage( 0, 0, resized );
273 }
274 else
275 {
276 context.painter()->drawImage( 0, 0, image );
277 }
278}
279
281{
282 return u"[HEATMAP]"_s;
283}
284
286{
287 QgsHeatmapRenderer *newRenderer = new QgsHeatmapRenderer();
288 if ( mGradientRamp )
289 {
290 newRenderer->setColorRamp( mGradientRamp->clone() );
291 }
292 newRenderer->setRadius( mRadius );
293 newRenderer->setRadiusUnit( mRadiusUnit );
294 newRenderer->setRadiusMapUnitScale( mRadiusMapUnitScale );
295 newRenderer->setMaximumValue( mExplicitMax );
296 newRenderer->setRenderQuality( mRenderQuality );
297 newRenderer->setWeightExpression( mWeightExpressionString );
298 newRenderer->setLegendSettings( mLegendSettings );
299 copyRendererData( newRenderer );
300
301 return newRenderer;
302}
303
305{
306 //we need to expand out the request extent so that it includes points which are up to the heatmap radius outside of the
307 //actual visible extent
308 const double extension = context.convertToMapUnits( mRadius, mRadiusUnit, mRadiusMapUnitScale );
309 extent.setXMinimum( extent.xMinimum() - extension );
310 extent.setXMaximum( extent.xMaximum() + extension );
311 extent.setYMinimum( extent.yMinimum() - extension );
312 extent.setYMaximum( extent.yMaximum() + extension );
313}
314
316{
317 Q_UNUSED( context )
319 r->setRadius( element.attribute( u"radius"_s, u"50.0"_s ).toFloat() );
320 r->setRadiusUnit( static_cast< Qgis::RenderUnit >( element.attribute( u"radius_unit"_s, u"0"_s ).toInt() ) );
321 r->setRadiusMapUnitScale( QgsSymbolLayerUtils::decodeMapUnitScale( element.attribute( u"radius_map_unit_scale"_s, QString() ) ) );
322 r->setMaximumValue( element.attribute( u"max_value"_s, u"0.0"_s ).toFloat() );
323 r->setRenderQuality( element.attribute( u"quality"_s, u"0"_s ).toInt() );
324 r->setWeightExpression( element.attribute( u"weight_expression"_s ) );
325
326 QDomElement sourceColorRampElem = element.firstChildElement( u"colorramp"_s );
327 if ( !sourceColorRampElem.isNull() && sourceColorRampElem.attribute( u"name"_s ) == "[source]"_L1 )
328 {
329 r->setColorRamp( QgsSymbolLayerUtils::loadColorRamp( sourceColorRampElem ).release() );
330 }
331
333 legendSettings.readXml( element, context );
335
336 return r;
337}
338
339QDomElement QgsHeatmapRenderer::save( QDomDocument &doc, const QgsReadWriteContext &context )
340{
341 QDomElement rendererElem = doc.createElement( RENDERER_TAG_NAME );
342 rendererElem.setAttribute( u"type"_s, u"heatmapRenderer"_s );
343 rendererElem.setAttribute( u"radius"_s, QString::number( mRadius ) );
344 rendererElem.setAttribute( u"radius_unit"_s, QString::number( static_cast< int >( mRadiusUnit ) ) );
345 rendererElem.setAttribute( u"radius_map_unit_scale"_s, QgsSymbolLayerUtils::encodeMapUnitScale( mRadiusMapUnitScale ) );
346 rendererElem.setAttribute( u"max_value"_s, QString::number( mExplicitMax ) );
347 rendererElem.setAttribute( u"quality"_s, QString::number( mRenderQuality ) );
348 rendererElem.setAttribute( u"weight_expression"_s, mWeightExpressionString );
349
350 if ( mGradientRamp )
351 {
352 const QDomElement colorRampElem = QgsSymbolLayerUtils::saveColorRamp( u"[source]"_s, mGradientRamp.get(), doc );
353 rendererElem.appendChild( colorRampElem );
354 }
355 mLegendSettings.writeXml( doc, rendererElem, context );
356
357 saveRendererData( doc, rendererElem, context );
358
359 return rendererElem;
360}
361
363{
364 Q_UNUSED( feature )
365 return nullptr;
366}
367
372
374{
375 QSet<QString> attributes;
376
377 // mAttrName can contain either attribute name or an expression.
378 // Sometimes it is not possible to distinguish between those two,
379 // e.g. "a - b" can be both a valid attribute name or expression.
380 // Since we do not have access to fields here, try both options.
381 attributes << mWeightExpressionString;
382
383 const QgsExpression testExpr( mWeightExpressionString );
384 if ( !testExpr.hasParserError() )
385 attributes.unite( testExpr.referencedColumns() );
386
387 return attributes;
388}
389
391{
392 if ( renderer->type() == "heatmapRenderer"_L1 )
393 {
394 return dynamic_cast<QgsHeatmapRenderer *>( renderer->clone() );
395 }
396 else
397 {
398 auto res = std::make_unique< QgsHeatmapRenderer >();
399 renderer->copyRendererData( res.get() );
400 return res.release();
401 }
402}
403
405{
406 if ( mGradientRamp )
407 {
408 QgsStyleColorRampEntity entity( mGradientRamp.get() );
409 if ( !visitor->visit( QgsStyleEntityVisitorInterface::StyleLeaf( &entity ) ) )
410 return false;
411 }
412 return true;
413}
414
415QList<QgsLayerTreeModelLegendNode *> QgsHeatmapRenderer::createLegendNodes( QgsLayerTreeLayer *nodeLayer ) const
416{
417 return { new QgsColorRampLegendNode( nodeLayer, mGradientRamp->clone(), mLegendSettings, 0, 1 ) };
418}
419
421{
422 mGradientRamp.reset( ramp );
423}
424
426{
427 mLegendSettings = settings;
428}
@ Point
Points.
Definition qgis.h:380
RenderUnit
Rendering size units.
Definition qgis.h:5650
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:60
QgsAttributes attributes
Definition qgsfeature.h:64
QgsGeometry geometry
Definition qgsfeature.h:66
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.
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:62
double y
Definition qgspointxy.h:66
double x
Definition qgspointxy.h:65
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:1491
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:227
QVector< QgsPointXY > QgsMultiPointXY
A collection of QgsPoints that share a common collection of attributes.
Definition qgsgeometry.h:98
#define RENDERER_TAG_NAME
Definition qgsrenderer.h:57
QList< QgsSymbol * > QgsSymbolList
Definition qgsrenderer.h:51
Contains information relating to the style entity currently being visited.