QGIS API Documentation  2.8.2-Wien
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Macros Groups Pages
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 "qgssymbolv2.h"
19 #include "qgssymbollayerv2utils.h"
20 
21 #include "qgslogger.h"
22 #include "qgsfeature.h"
23 #include "qgsvectorlayer.h"
24 #include "qgssymbollayerv2.h"
25 #include "qgsogcutils.h"
26 #include "qgsvectorcolorrampv2.h"
27 #include "qgsrendercontext.h"
28 
29 #include <QDomDocument>
30 #include <QDomElement>
31 
33  : QgsFeatureRendererV2( "heatmapRenderer" )
34  , mCalculatedMaxValue( 0 )
35  , mRadius( 10 )
36  , mRadiusPixels( 0 )
37  , mRadiusSquared( 0 )
38  , mRadiusUnit( QgsSymbolV2::MM )
39  , mWeightAttrNum( -1 )
40  , mGradientRamp( 0 )
41  , mInvertRamp( false )
42  , mExplicitMax( 0.0 )
43  , mRenderQuality( 3 )
44  , mFeaturesRendered( 0 )
45 {
46  mGradientRamp = new QgsVectorGradientColorRampV2( QColor( 255, 255, 255 ), QColor( 0, 0, 0 ) );
47 
48 }
49 
51 {
52  delete mGradientRamp;
53 }
54 
55 void QgsHeatmapRenderer::initializeValues( QgsRenderContext& context )
56 {
57  mValues.resize( context.painter()->device()->width() * context.painter()->device()->height() / ( mRenderQuality * mRenderQuality ) );
58  mValues.fill( 0 );
59  mCalculatedMaxValue = 0;
60  mFeaturesRendered = 0;
61  mRadiusPixels = qRound( mRadius * QgsSymbolLayerV2Utils::pixelSizeScaleFactor( context, mRadiusUnit, mRadiusMapUnitScale ) / mRenderQuality );
62  mRadiusSquared = mRadiusPixels * mRadiusPixels;
63 }
64 
66 {
67  Q_UNUSED( fields );
68  if ( !context.painter() )
69  {
70  return;
71  }
72 
73  // find out classification attribute index from name
74  mWeightAttrNum = fields.fieldNameIndex( mWeightExpressionString );
75  if ( mWeightAttrNum == -1 )
76  {
77  mWeightExpression.reset( new QgsExpression( mWeightExpressionString ) );
78  mWeightExpression->prepare( fields );
79  }
80 
81  initializeValues( context );
82 }
83 
84 QgsMultiPoint QgsHeatmapRenderer::convertToMultipoint( QgsGeometry* geom )
85 {
86  QgsMultiPoint 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 
99 bool QgsHeatmapRenderer::renderFeature( 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.geometry() || feature.geometry()->type() != QGis::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.data() );
123  value = mWeightExpression->evaluate( &feature );
124  }
125  else
126  {
127  const QgsAttributes& attrs = feature.attributes();
128  value = attrs.value( mWeightAttrNum );
129  }
130  bool ok = false;
131  double evalWeight = value.toDouble( &ok );
132  if ( ok )
133  {
134  weight = evalWeight;
135  }
136  }
137 
138  int width = context.painter()->device()->width() / mRenderQuality;
139  int height = context.painter()->device()->height() / mRenderQuality;
140 
141  //transform geometry if required
142  QgsGeometry* geom;
143  bool createdGeom = false;
144  const QgsCoordinateTransform* xform = context.coordinateTransform();
145  if ( xform )
146  {
147  geom = new QgsGeometry( *feature.geometry() );
148  createdGeom = true;
149  geom->transform( *xform );
150  }
151  else
152  {
153  geom = feature.geometry();
154  }
155 
156  //convert point to multipoint
157  QgsMultiPoint multiPoint = convertToMultipoint( geom );
158  if ( createdGeom )
159  {
160  delete geom;
161  }
162  geom = 0;
163 
164  //loop through all points in multipoint
165  for ( QgsMultiPoint::const_iterator pointIt = multiPoint.constBegin(); pointIt != multiPoint.constEnd(); ++pointIt )
166  {
167  QgsPoint pixel = context.mapToPixel().transform( *pointIt );
168  int pointX = pixel.x() / mRenderQuality;
169  int pointY = pixel.y() / mRenderQuality;
170  for ( int x = qMax( pointX - mRadiusPixels, 0 ); x < qMin( pointX + mRadiusPixels, width ); ++x )
171  {
172  for ( int y = qMax( pointY - mRadiusPixels, 0 ); y < qMin( pointY + mRadiusPixels, height ); ++y )
173  {
174  int index = y * width + x;
175  if ( index >= mValues.count( ) )
176  {
177  continue;
178  }
179  double distanceSquared = pow( pointX - x, 2.0 ) + pow( pointY - y, 2.0 );
180  if ( distanceSquared > mRadiusSquared )
181  {
182  continue;
183  }
184 
185  double score = weight * quarticKernel( sqrt( distanceSquared ), mRadiusPixels );
186  double value = mValues[ index ] + score;
187  if ( value > mCalculatedMaxValue )
188  {
189  mCalculatedMaxValue = value;
190  }
191  mValues[ index ] = value;
192  }
193  }
194  }
195 
196  mFeaturesRendered++;
197 #if 0
198  //TODO - enable progressive rendering
199  if ( mFeaturesRendered % 200 == 0 )
200  {
201  renderImage( context );
202  }
203 #endif
204  return true;
205 }
206 
207 
208 double QgsHeatmapRenderer::uniformKernel( const double distance, const int bandwidth ) const
209 {
210  Q_UNUSED( distance );
211  Q_UNUSED( bandwidth );
212  return 1.0;
213 }
214 
215 double QgsHeatmapRenderer::quarticKernel( const double distance, const int bandwidth ) const
216 {
217  return pow( 1. - pow( distance / ( double )bandwidth, 2 ), 2 );
218 }
219 
220 double QgsHeatmapRenderer::triweightKernel( const double distance, const int bandwidth ) const
221 {
222  return pow( 1. - pow( distance / ( double )bandwidth, 2 ), 3 );
223 }
224 
225 double QgsHeatmapRenderer::epanechnikovKernel( const double distance, const int bandwidth ) const
226 {
227  return ( 1. - pow( distance / ( double )bandwidth, 2 ) );
228 }
229 
230 double QgsHeatmapRenderer::triangularKernel( const double distance, const int bandwidth ) const
231 {
232  return ( 1. - ( distance / ( double )bandwidth ) );
233 }
234 
236 {
237  renderImage( context );
238  mWeightExpression.reset();
239 }
240 
241 void QgsHeatmapRenderer::renderImage( QgsRenderContext& context )
242 {
243  if ( !context.painter() || !mGradientRamp )
244  {
245  return;
246  }
247 
248  QImage image( context.painter()->device()->width() / mRenderQuality,
249  context.painter()->device()->height() / mRenderQuality,
250  QImage::Format_ARGB32 );
251  image.fill( Qt::transparent );
252 
253  double scaleMax = mExplicitMax > 0 ? mExplicitMax : mCalculatedMaxValue;
254 
255  int idx = 0;
256  double pixVal = 0;
257  QColor pixColor;
258  for ( int heightIndex = 0; heightIndex < image.height(); ++heightIndex )
259  {
260  QRgb* scanLine = ( QRgb* )image.scanLine( heightIndex );
261  for ( int widthIndex = 0; widthIndex < image.width(); ++widthIndex )
262  {
263  //scale result to fit in the range [0, 1]
264  pixVal = mValues.at( idx ) > 0 ? qMin(( mValues.at( idx ) / scaleMax ), 1.0 ) : 0;
265 
266  //convert value to color from ramp
267  pixColor = mGradientRamp->color( mInvertRamp ? 1 - pixVal : pixVal );
268 
269  scanLine[widthIndex] = pixColor.rgba();
270  idx++;
271  }
272  }
273 
274  if ( mRenderQuality > 1 )
275  {
276  QImage resized = image.scaled( context.painter()->device()->width(),
277  context.painter()->device()->height() );
278  context.painter()->drawImage( 0, 0, resized );
279  }
280  else
281  {
282  context.painter()->drawImage( 0, 0, image );
283  }
284 }
285 
287 {
288  return "[HEATMAP]";
289 }
290 
292 {
293  QgsHeatmapRenderer* newRenderer = new QgsHeatmapRenderer();
294  if ( mGradientRamp )
295  {
296  newRenderer->setColorRamp( mGradientRamp->clone() );
297  }
298  newRenderer->setInvertRamp( mInvertRamp );
299  newRenderer->setRadius( mRadius );
300  newRenderer->setRadiusUnit( mRadiusUnit );
301  newRenderer->setRadiusMapUnitScale( mRadiusMapUnitScale );
302  newRenderer->setMaximumValue( mExplicitMax );
303  newRenderer->setRenderQuality( mRenderQuality );
304  newRenderer->setWeightExpression( mWeightExpressionString );
305 
306  return newRenderer;
307 }
308 
310 {
311  //we need to expand out the request extent so that it includes points which are up to the heatmap radius outside of the
312  //actual visible extent
313  double extension = 0.0;
314  if ( mRadiusUnit == QgsSymbolV2::Pixel )
315  {
317  }
318  else if ( mRadiusUnit == QgsSymbolV2::MM )
319  {
320  double pixelSize = mRadius * QgsSymbolLayerV2Utils::pixelSizeScaleFactor( context, QgsSymbolV2::MM, QgsMapUnitScale() );
321  extension = pixelSize / QgsSymbolLayerV2Utils::pixelSizeScaleFactor( context, QgsSymbolV2::MapUnit, QgsMapUnitScale() );
322  }
323  else
324  {
325  extension = mRadius;
326  }
327  extent.setXMinimum( extent.xMinimum() - extension );
328  extent.setXMaximum( extent.xMaximum() + extension );
329  extent.setYMinimum( extent.yMinimum() - extension );
330  extent.setYMaximum( extent.yMaximum() + extension );
331 }
332 
334 {
336  r->setRadius( element.attribute( "radius", "50.0" ).toFloat() );
337  r->setRadiusUnit(( QgsSymbolV2::OutputUnit )element.attribute( "radius_unit", "0" ).toInt() );
338  r->setRadiusMapUnitScale( QgsSymbolLayerV2Utils::decodeMapUnitScale( element.attribute( "radius_map_unit_scale", QString() ) ) );
339  r->setMaximumValue( element.attribute( "max_value", "0.0" ).toFloat() );
340  r->setRenderQuality( element.attribute( "quality", "0" ).toInt() );
341  r->setWeightExpression( element.attribute( "weight_expression" ) );
342 
343  QDomElement sourceColorRampElem = element.firstChildElement( "colorramp" );
344  if ( !sourceColorRampElem.isNull() && sourceColorRampElem.attribute( "name" ) == "[source]" )
345  {
346  r->setColorRamp( QgsSymbolLayerV2Utils::loadColorRamp( sourceColorRampElem ) );
347  }
348  r->setInvertRamp( element.attribute( "invert_ramp", "0" ).toInt() );
349  return r;
350 }
351 
352 QDomElement QgsHeatmapRenderer::save( QDomDocument& doc )
353 {
354  QDomElement rendererElem = doc.createElement( RENDERER_TAG_NAME );
355  rendererElem.setAttribute( "type", "heatmapRenderer" );
356  rendererElem.setAttribute( "radius", QString::number( mRadius ) );
357  rendererElem.setAttribute( "radius_unit", QString::number( mRadiusUnit ) );
358  rendererElem.setAttribute( "radius_map_unit_scale", QgsSymbolLayerV2Utils::encodeMapUnitScale( mRadiusMapUnitScale ) );
359  rendererElem.setAttribute( "max_value", QString::number( mExplicitMax ) );
360  rendererElem.setAttribute( "quality", QString::number( mRenderQuality ) );
361  rendererElem.setAttribute( "weight_expression", mWeightExpressionString );
362  if ( mGradientRamp )
363  {
364  QDomElement colorRampElem = QgsSymbolLayerV2Utils::saveColorRamp( "[source]", mGradientRamp, doc );
365  rendererElem.appendChild( colorRampElem );
366  }
367  rendererElem.setAttribute( "invert_ramp", QString::number( mInvertRamp ) );
368 
369  return rendererElem;
370 }
371 
373 {
374  Q_UNUSED( feature );
375  return 0;
376 }
377 
379 {
380  return QgsSymbolV2List();
381 }
382 
384 {
385  QSet<QString> attributes;
386 
387  // mAttrName can contain either attribute name or an expression.
388  // Sometimes it is not possible to distinguish between those two,
389  // e.g. "a - b" can be both a valid attribute name or expression.
390  // Since we do not have access to fields here, try both options.
391  attributes << mWeightExpressionString;
392 
393  QgsExpression testExpr( mWeightExpressionString );
394  if ( !testExpr.hasParserError() )
395  attributes.unite( testExpr.referencedColumns().toSet() );
396 
397  return attributes.toList();
398 }
399 
401 {
402  if ( renderer->type() == "heatmapRenderer" )
403  {
404  return dynamic_cast<QgsHeatmapRenderer*>( renderer->clone() );
405  }
406  else
407  {
408  return new QgsHeatmapRenderer();
409  }
410 }
411 
413 {
414  delete mGradientRamp;
415  mGradientRamp = ramp;
416 }