Branch data Line data Source code
1 : : /***************************************************************************
2 : : qgspointdistancerenderer.cpp
3 : : ----------------------------
4 : : begin : January 26, 2010
5 : : copyright : (C) 2010 by Marco Hugentobler
6 : : email : marco at hugis dot net
7 : : ***************************************************************************/
8 : :
9 : : /***************************************************************************
10 : : * *
11 : : * This program is free software; you can redistribute it and/or modify *
12 : : * it under the terms of the GNU General Public License as published by *
13 : : * the Free Software Foundation; either version 2 of the License, or *
14 : : * (at your option) any later version. *
15 : : * *
16 : : ***************************************************************************/
17 : :
18 : : #include "qgspointdistancerenderer.h"
19 : : #include "qgsgeometry.h"
20 : : #include "qgssymbollayerutils.h"
21 : : #include "qgsspatialindex.h"
22 : : #include "qgsmultipoint.h"
23 : : #include "qgslogger.h"
24 : : #include "qgsstyleentityvisitor.h"
25 : : #include "qgsexpressioncontextutils.h"
26 : :
27 : : #include <QDomElement>
28 : : #include <QPainter>
29 : :
30 : : #include <cmath>
31 : :
32 : 0 : QgsPointDistanceRenderer::QgsPointDistanceRenderer( const QString &rendererName, const QString &labelAttributeName )
33 : 0 : : QgsFeatureRenderer( rendererName )
34 : 0 : , mLabelAttributeName( labelAttributeName )
35 : 0 : , mLabelIndex( -1 )
36 : 0 : , mTolerance( 3 )
37 : 0 : , mToleranceUnit( QgsUnitTypes::RenderMillimeters )
38 : 0 : , mDrawLabels( true )
39 : :
40 : 0 : {
41 : 0 : mRenderer.reset( QgsFeatureRenderer::defaultRenderer( QgsWkbTypes::PointGeometry ) );
42 : 0 : }
43 : :
44 : 0 : void QgsPointDistanceRenderer::toSld( QDomDocument &doc, QDomElement &element, const QVariantMap &props ) const
45 : : {
46 : 0 : mRenderer->toSld( doc, element, props );
47 : 0 : }
48 : :
49 : :
50 : 0 : bool QgsPointDistanceRenderer::renderFeature( const QgsFeature &feature, QgsRenderContext &context, int layer, bool selected, bool drawVertexMarker )
51 : : {
52 : : Q_UNUSED( drawVertexMarker )
53 : 0 : Q_UNUSED( context )
54 : : Q_UNUSED( layer )
55 : :
56 : : /*
57 : : * IMPORTANT: This algorithm is ported to Python in the processing "Points Displacement" algorithm.
58 : : * Please port any changes/improvements to that algorithm too!
59 : : */
60 : :
61 : : //check if there is already a point at that position
62 : 0 : if ( !feature.hasGeometry() )
63 : 0 : return false;
64 : :
65 : 0 : QgsMarkerSymbol *symbol = firstSymbolForFeature( feature, context );
66 : :
67 : : //if the feature has no symbol (e.g., no matching rule in a rule-based renderer), skip it
68 : 0 : if ( !symbol )
69 : 0 : return false;
70 : :
71 : : //point position in screen coords
72 : 0 : QgsGeometry geom = feature.geometry();
73 : 0 : QgsWkbTypes::Type geomType = geom.wkbType();
74 : 0 : if ( QgsWkbTypes::flatType( geomType ) != QgsWkbTypes::Point )
75 : : {
76 : : //can only render point type
77 : 0 : return false;
78 : : }
79 : :
80 : 0 : QString label;
81 : 0 : if ( mDrawLabels )
82 : : {
83 : 0 : label = getLabel( feature );
84 : 0 : }
85 : :
86 : 0 : QgsCoordinateTransform xform = context.coordinateTransform();
87 : 0 : QgsFeature transformedFeature = feature;
88 : 0 : if ( xform.isValid() )
89 : : {
90 : 0 : geom.transform( xform );
91 : 0 : transformedFeature.setGeometry( geom );
92 : 0 : }
93 : :
94 : 0 : double searchDistance = context.convertToMapUnits( mTolerance, mToleranceUnit, mToleranceMapUnitScale );
95 : 0 : QgsPointXY point = transformedFeature.geometry().asPoint();
96 : 0 : QList<QgsFeatureId> intersectList = mSpatialIndex->intersects( searchRect( point, searchDistance ) );
97 : 0 : if ( intersectList.empty() )
98 : : {
99 : 0 : mSpatialIndex->addFeature( transformedFeature );
100 : : // create new group
101 : 0 : ClusteredGroup newGroup;
102 : 0 : newGroup << GroupedFeature( transformedFeature, symbol->clone(), selected, label );
103 : 0 : mClusteredGroups.push_back( newGroup );
104 : : // add to group index
105 : 0 : mGroupIndex.insert( transformedFeature.id(), mClusteredGroups.count() - 1 );
106 : 0 : mGroupLocations.insert( transformedFeature.id(), point );
107 : 0 : }
108 : : else
109 : : {
110 : : // find group with closest location to this point (may be more than one within search tolerance)
111 : 0 : QgsFeatureId minDistFeatureId = intersectList.at( 0 );
112 : 0 : double minDist = mGroupLocations.value( minDistFeatureId ).distance( point );
113 : 0 : for ( int i = 1; i < intersectList.count(); ++i )
114 : : {
115 : 0 : QgsFeatureId candidateId = intersectList.at( i );
116 : 0 : double newDist = mGroupLocations.value( candidateId ).distance( point );
117 : 0 : if ( newDist < minDist )
118 : : {
119 : 0 : minDist = newDist;
120 : 0 : minDistFeatureId = candidateId;
121 : 0 : }
122 : 0 : }
123 : :
124 : 0 : int groupIdx = mGroupIndex[ minDistFeatureId ];
125 : 0 : ClusteredGroup &group = mClusteredGroups[groupIdx];
126 : :
127 : : // calculate new centroid of group
128 : 0 : QgsPointXY oldCenter = mGroupLocations.value( minDistFeatureId );
129 : 0 : mGroupLocations[ minDistFeatureId ] = QgsPointXY( ( oldCenter.x() * group.size() + point.x() ) / ( group.size() + 1.0 ),
130 : 0 : ( oldCenter.y() * group.size() + point.y() ) / ( group.size() + 1.0 ) );
131 : :
132 : : // add to a group
133 : 0 : group << GroupedFeature( transformedFeature, symbol->clone(), selected, label );
134 : : // add to group index
135 : 0 : mGroupIndex.insert( transformedFeature.id(), groupIdx );
136 : : }
137 : :
138 : 0 : return true;
139 : 0 : }
140 : :
141 : 0 : void QgsPointDistanceRenderer::drawGroup( const ClusteredGroup &group, QgsRenderContext &context )
142 : : {
143 : : //calculate centroid of all points, this will be center of group
144 : 0 : QgsMultiPoint *groupMultiPoint = new QgsMultiPoint();
145 : 0 : const auto constGroup = group;
146 : 0 : for ( const GroupedFeature &f : constGroup )
147 : : {
148 : 0 : groupMultiPoint->addGeometry( f.feature.geometry().constGet()->clone() );
149 : : }
150 : 0 : QgsGeometry groupGeom( groupMultiPoint );
151 : 0 : QgsGeometry centroid = groupGeom.centroid();
152 : 0 : QPointF pt = centroid.asQPointF();
153 : 0 : context.mapToPixel().transformInPlace( pt.rx(), pt.ry() );
154 : :
155 : 0 : QgsExpressionContextScopePopper scopePopper( context.expressionContext(), createGroupScope( group ) );
156 : 0 : drawGroup( pt, context, group );
157 : 0 : }
158 : :
159 : 0 : void QgsPointDistanceRenderer::setEmbeddedRenderer( QgsFeatureRenderer *r )
160 : : {
161 : 0 : mRenderer.reset( r );
162 : 0 : }
163 : :
164 : 0 : const QgsFeatureRenderer *QgsPointDistanceRenderer::embeddedRenderer() const
165 : : {
166 : 0 : return mRenderer.get();
167 : : }
168 : :
169 : 0 : void QgsPointDistanceRenderer::setLegendSymbolItem( const QString &key, QgsSymbol *symbol )
170 : : {
171 : 0 : if ( !mRenderer )
172 : 0 : return;
173 : :
174 : 0 : mRenderer->setLegendSymbolItem( key, symbol );
175 : 0 : }
176 : :
177 : 0 : bool QgsPointDistanceRenderer::legendSymbolItemsCheckable() const
178 : : {
179 : 0 : if ( !mRenderer )
180 : 0 : return false;
181 : :
182 : 0 : return mRenderer->legendSymbolItemsCheckable();
183 : 0 : }
184 : :
185 : 0 : bool QgsPointDistanceRenderer::legendSymbolItemChecked( const QString &key )
186 : : {
187 : 0 : if ( !mRenderer )
188 : 0 : return false;
189 : :
190 : 0 : return mRenderer->legendSymbolItemChecked( key );
191 : 0 : }
192 : :
193 : 0 : void QgsPointDistanceRenderer::checkLegendSymbolItem( const QString &key, bool state )
194 : : {
195 : 0 : if ( !mRenderer )
196 : 0 : return;
197 : :
198 : 0 : mRenderer->checkLegendSymbolItem( key, state );
199 : 0 : }
200 : :
201 : 0 : QString QgsPointDistanceRenderer::filter( const QgsFields &fields )
202 : : {
203 : 0 : if ( !mRenderer )
204 : 0 : return QgsFeatureRenderer::filter( fields );
205 : : else
206 : 0 : return mRenderer->filter( fields );
207 : 0 : }
208 : :
209 : 0 : bool QgsPointDistanceRenderer::accept( QgsStyleEntityVisitorInterface *visitor ) const
210 : : {
211 : 0 : if ( mRenderer )
212 : 0 : if ( !mRenderer->accept( visitor ) )
213 : 0 : return false;
214 : :
215 : 0 : return true;
216 : 0 : }
217 : :
218 : 0 : QSet<QString> QgsPointDistanceRenderer::usedAttributes( const QgsRenderContext &context ) const
219 : : {
220 : 0 : QSet<QString> attributeList;
221 : 0 : if ( !mLabelAttributeName.isEmpty() )
222 : : {
223 : 0 : attributeList.insert( mLabelAttributeName );
224 : 0 : }
225 : 0 : if ( mRenderer )
226 : : {
227 : 0 : attributeList += mRenderer->usedAttributes( context );
228 : 0 : }
229 : 0 : return attributeList;
230 : 0 : }
231 : :
232 : 0 : bool QgsPointDistanceRenderer::filterNeedsGeometry() const
233 : : {
234 : 0 : return mRenderer ? mRenderer->filterNeedsGeometry() : false;
235 : : }
236 : :
237 : 0 : QgsFeatureRenderer::Capabilities QgsPointDistanceRenderer::capabilities()
238 : : {
239 : 0 : if ( !mRenderer )
240 : : {
241 : 0 : return Capabilities();
242 : : }
243 : 0 : return mRenderer->capabilities();
244 : 0 : }
245 : :
246 : 0 : QgsSymbolList QgsPointDistanceRenderer::symbols( QgsRenderContext &context ) const
247 : : {
248 : 0 : if ( !mRenderer )
249 : : {
250 : 0 : return QgsSymbolList();
251 : : }
252 : 0 : return mRenderer->symbols( context );
253 : 0 : }
254 : :
255 : 0 : QgsSymbol *QgsPointDistanceRenderer::symbolForFeature( const QgsFeature &feature, QgsRenderContext &context ) const
256 : 0 : {
257 : 0 : if ( !mRenderer )
258 : : {
259 : 0 : return nullptr;
260 : : }
261 : 0 : return mRenderer->symbolForFeature( feature, context );
262 : 0 : }
263 : :
264 : 0 : QgsSymbol *QgsPointDistanceRenderer::originalSymbolForFeature( const QgsFeature &feature, QgsRenderContext &context ) const
265 : : {
266 : 0 : if ( !mRenderer )
267 : 0 : return nullptr;
268 : 0 : return mRenderer->originalSymbolForFeature( feature, context );
269 : 0 : }
270 : :
271 : 0 : QgsSymbolList QgsPointDistanceRenderer::symbolsForFeature( const QgsFeature &feature, QgsRenderContext &context ) const
272 : : {
273 : 0 : if ( !mRenderer )
274 : : {
275 : 0 : return QgsSymbolList();
276 : : }
277 : 0 : return mRenderer->symbolsForFeature( feature, context );
278 : 0 : }
279 : :
280 : 0 : QgsSymbolList QgsPointDistanceRenderer::originalSymbolsForFeature( const QgsFeature &feature, QgsRenderContext &context ) const
281 : : {
282 : 0 : if ( !mRenderer )
283 : 0 : return QgsSymbolList();
284 : 0 : return mRenderer->originalSymbolsForFeature( feature, context );
285 : 0 : }
286 : :
287 : 0 : QSet< QString > QgsPointDistanceRenderer::legendKeysForFeature( const QgsFeature &feature, QgsRenderContext &context ) const
288 : : {
289 : 0 : if ( !mRenderer )
290 : 0 : return QSet< QString >() << QString();
291 : 0 : return mRenderer->legendKeysForFeature( feature, context );
292 : 0 : }
293 : :
294 : 0 : bool QgsPointDistanceRenderer::willRenderFeature( const QgsFeature &feature, QgsRenderContext &context ) const
295 : : {
296 : 0 : if ( !mRenderer )
297 : : {
298 : 0 : return false;
299 : : }
300 : 0 : return mRenderer->willRenderFeature( feature, context );
301 : 0 : }
302 : :
303 : :
304 : 0 : void QgsPointDistanceRenderer::startRender( QgsRenderContext &context, const QgsFields &fields )
305 : : {
306 : 0 : QgsFeatureRenderer::startRender( context, fields );
307 : :
308 : 0 : mRenderer->startRender( context, fields );
309 : :
310 : 0 : mClusteredGroups.clear();
311 : 0 : mGroupIndex.clear();
312 : 0 : mGroupLocations.clear();
313 : 0 : mSpatialIndex = new QgsSpatialIndex;
314 : :
315 : 0 : if ( mLabelAttributeName.isEmpty() )
316 : : {
317 : 0 : mLabelIndex = -1;
318 : 0 : }
319 : : else
320 : : {
321 : 0 : mLabelIndex = fields.lookupField( mLabelAttributeName );
322 : : }
323 : :
324 : 0 : if ( mMinLabelScale <= 0 || context.rendererScale() < mMinLabelScale )
325 : : {
326 : 0 : mDrawLabels = true;
327 : 0 : }
328 : : else
329 : : {
330 : 0 : mDrawLabels = false;
331 : : }
332 : 0 : }
333 : :
334 : 0 : void QgsPointDistanceRenderer::stopRender( QgsRenderContext &context )
335 : : {
336 : 0 : QgsFeatureRenderer::stopRender( context );
337 : :
338 : : //printInfoDisplacementGroups(); //just for debugging
339 : :
340 : 0 : if ( !context.renderingStopped() )
341 : : {
342 : 0 : const auto constMClusteredGroups = mClusteredGroups;
343 : 0 : for ( const ClusteredGroup &group : constMClusteredGroups )
344 : : {
345 : 0 : drawGroup( group, context );
346 : : }
347 : 0 : }
348 : :
349 : 0 : mClusteredGroups.clear();
350 : 0 : mGroupIndex.clear();
351 : 0 : mGroupLocations.clear();
352 : 0 : delete mSpatialIndex;
353 : 0 : mSpatialIndex = nullptr;
354 : :
355 : 0 : mRenderer->stopRender( context );
356 : 0 : }
357 : :
358 : 0 : QgsLegendSymbolList QgsPointDistanceRenderer::legendSymbolItems() const
359 : : {
360 : 0 : if ( mRenderer )
361 : : {
362 : 0 : return mRenderer->legendSymbolItems();
363 : : }
364 : 0 : return QgsLegendSymbolList();
365 : 0 : }
366 : :
367 : 0 : QgsRectangle QgsPointDistanceRenderer::searchRect( const QgsPointXY &p, double distance ) const
368 : : {
369 : 0 : return QgsRectangle( p.x() - distance, p.y() - distance, p.x() + distance, p.y() + distance );
370 : : }
371 : :
372 : 0 : void QgsPointDistanceRenderer::printGroupInfo() const
373 : : {
374 : : #ifdef QGISDEBUG
375 : : int nGroups = mClusteredGroups.size();
376 : : QgsDebugMsgLevel( "number of displacement groups:" + QString::number( nGroups ), 3 );
377 : : for ( int i = 0; i < nGroups; ++i )
378 : : {
379 : : QgsDebugMsgLevel( "***************displacement group " + QString::number( i ), 3 );
380 : : const auto constAt = mClusteredGroups.at( i );
381 : : for ( const GroupedFeature &feature : constAt )
382 : : {
383 : : QgsDebugMsgLevel( FID_TO_STRING( feature.feature.id() ), 3 );
384 : : }
385 : : }
386 : : #endif
387 : 0 : }
388 : :
389 : 0 : QString QgsPointDistanceRenderer::getLabel( const QgsFeature &feature ) const
390 : : {
391 : 0 : QString attribute;
392 : 0 : QgsAttributes attrs = feature.attributes();
393 : 0 : if ( mLabelIndex >= 0 && mLabelIndex < attrs.count() )
394 : : {
395 : 0 : attribute = attrs.at( mLabelIndex ).toString();
396 : 0 : }
397 : 0 : return attribute;
398 : 0 : }
399 : :
400 : 0 : void QgsPointDistanceRenderer::drawLabels( QPointF centerPoint, QgsSymbolRenderContext &context, const QList<QPointF> &labelShifts, const ClusteredGroup &group )
401 : : {
402 : 0 : QPainter *p = context.renderContext().painter();
403 : 0 : if ( !p )
404 : : {
405 : 0 : return;
406 : : }
407 : :
408 : 0 : QPen labelPen( mLabelColor );
409 : 0 : p->setPen( labelPen );
410 : :
411 : : //scale font (for printing)
412 : 0 : QFont pixelSizeFont = mLabelFont;
413 : :
414 : 0 : const double fontSizeInPixels = context.renderContext().convertToPainterUnits( mLabelFont.pointSizeF(), QgsUnitTypes::RenderPoints );
415 : 0 : pixelSizeFont.setPixelSize( static_cast< int >( std::round( fontSizeInPixels ) ) );
416 : 0 : QFont scaledFont = pixelSizeFont;
417 : 0 : scaledFont.setPixelSize( pixelSizeFont.pixelSize() );
418 : 0 : p->setFont( scaledFont );
419 : :
420 : 0 : QFontMetricsF fontMetrics( pixelSizeFont );
421 : 0 : QPointF currentLabelShift; //considers the signs to determine the label position
422 : :
423 : 0 : QList<QPointF>::const_iterator labelPosIt = labelShifts.constBegin();
424 : 0 : ClusteredGroup::const_iterator groupIt = group.constBegin();
425 : :
426 : 0 : for ( ; labelPosIt != labelShifts.constEnd() && groupIt != group.constEnd(); ++labelPosIt, ++groupIt )
427 : : {
428 : 0 : currentLabelShift = *labelPosIt;
429 : 0 : if ( currentLabelShift.x() < 0 )
430 : : {
431 : 0 : currentLabelShift.setX( currentLabelShift.x() - fontMetrics.horizontalAdvance( groupIt->label ) );
432 : 0 : }
433 : 0 : if ( currentLabelShift.y() > 0 )
434 : : {
435 : 0 : currentLabelShift.setY( currentLabelShift.y() + fontMetrics.ascent() );
436 : 0 : }
437 : :
438 : 0 : QPointF drawingPoint( centerPoint + currentLabelShift );
439 : 0 : QgsScopedQPainterState painterState( p );
440 : 0 : p->translate( drawingPoint.x(), drawingPoint.y() );
441 : 0 : p->drawText( QPointF( 0, 0 ), groupIt->label );
442 : 0 : }
443 : 0 : }
444 : :
445 : 0 : QgsExpressionContextScope *QgsPointDistanceRenderer::createGroupScope( const ClusteredGroup &group ) const
446 : : {
447 : 0 : QgsExpressionContextScope *clusterScope = new QgsExpressionContextScope();
448 : 0 : if ( group.size() > 1 )
449 : : {
450 : : //scan through symbols to check color, e.g., if all clustered symbols are same color
451 : 0 : QColor groupColor;
452 : 0 : ClusteredGroup::const_iterator groupIt = group.constBegin();
453 : 0 : for ( ; groupIt != group.constEnd(); ++groupIt )
454 : : {
455 : 0 : if ( !groupIt->symbol() )
456 : 0 : continue;
457 : :
458 : 0 : if ( !groupColor.isValid() )
459 : : {
460 : 0 : groupColor = groupIt->symbol()->color();
461 : 0 : }
462 : : else
463 : : {
464 : 0 : if ( groupColor != groupIt->symbol()->color() )
465 : : {
466 : 0 : groupColor = QColor();
467 : 0 : break;
468 : : }
469 : : }
470 : 0 : }
471 : :
472 : 0 : if ( groupColor.isValid() )
473 : : {
474 : 0 : clusterScope->addVariable( QgsExpressionContextScope::StaticVariable( QgsExpressionContext::EXPR_CLUSTER_COLOR, QgsSymbolLayerUtils::encodeColor( groupColor ), true ) );
475 : 0 : }
476 : : else
477 : : {
478 : : //mixed colors
479 : 0 : clusterScope->addVariable( QgsExpressionContextScope::StaticVariable( QgsExpressionContext::EXPR_CLUSTER_COLOR, QVariant(), true ) );
480 : : }
481 : :
482 : 0 : clusterScope->addVariable( QgsExpressionContextScope::StaticVariable( QgsExpressionContext::EXPR_CLUSTER_SIZE, group.size(), true ) );
483 : 0 : }
484 : 0 : if ( !group.empty() )
485 : : {
486 : : // data defined properties may require a feature in the expression context, so just use first feature in group
487 : 0 : clusterScope->setFeature( group.at( 0 ).feature );
488 : 0 : }
489 : 0 : return clusterScope;
490 : 0 : }
491 : :
492 : 0 : QgsMarkerSymbol *QgsPointDistanceRenderer::firstSymbolForFeature( const QgsFeature &feature, QgsRenderContext &context )
493 : : {
494 : 0 : if ( !mRenderer )
495 : : {
496 : 0 : return nullptr;
497 : : }
498 : :
499 : 0 : QgsSymbolList symbolList = mRenderer->symbolsForFeature( feature, context );
500 : 0 : if ( symbolList.isEmpty() )
501 : : {
502 : 0 : return nullptr;
503 : : }
504 : :
505 : 0 : return dynamic_cast< QgsMarkerSymbol * >( symbolList.at( 0 ) );
506 : 0 : }
|