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