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