QGIS API Documentation  2.8.2-Wien
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Macros Groups Pages
qgssnappingutils.cpp
Go to the documentation of this file.
1 /***************************************************************************
2  qgssnappingutils.cpp
3  --------------------------------------
4  Date : November 2014
5  Copyright : (C) 2014 by Martin Dobias
6  Email : wonder dot sk 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 "qgssnappingutils.h"
17 
18 #include "qgsgeometry.h"
19 #include "qgsmaplayerregistry.h"
20 #include "qgsproject.h"
21 #include "qgsvectorlayer.h"
22 
23 
25  : QObject( parent )
26  , mCurrentLayer( 0 )
27  , mSnapToMapMode( SnapCurrentLayer )
28  , mStrategy( IndexHybrid )
29  , mDefaultType( QgsPointLocator::Vertex )
30  , mDefaultTolerance( 10 )
31  , mDefaultUnit( QgsTolerance::Pixels )
32  , mSnapOnIntersection( false )
33 {
34  connect( QgsMapLayerRegistry::instance(), SIGNAL( layersWillBeRemoved( QStringList ) ), this, SLOT( onLayersWillBeRemoved( QStringList ) ) );
35 }
36 
38 {
39  clearAllLocators();
40 }
41 
42 
44 {
45  if ( !vl )
46  return 0;
47 
48  if ( !mLocators.contains( vl ) )
49  {
50  QgsPointLocator* vlpl = new QgsPointLocator( vl, destCRS() );
51  mLocators.insert( vl, vlpl );
52  }
53  return mLocators.value( vl );
54 }
55 
56 void QgsSnappingUtils::clearAllLocators()
57 {
58  foreach ( QgsPointLocator* vlpl, mLocators )
59  delete vlpl;
60  mLocators.clear();
61 
62  foreach ( QgsPointLocator* vlpl, mTemporaryLocators )
63  delete vlpl;
64  mTemporaryLocators.clear();
65 }
66 
67 
68 QgsPointLocator* QgsSnappingUtils::locatorForLayerUsingStrategy( QgsVectorLayer* vl, const QgsPoint& pointMap, double tolerance )
69 {
70  if ( willUseIndex( vl ) )
71  return locatorForLayer( vl );
72  else
73  return temporaryLocatorForLayer( vl, pointMap, tolerance );
74 }
75 
76 QgsPointLocator* QgsSnappingUtils::temporaryLocatorForLayer( QgsVectorLayer* vl, const QgsPoint& pointMap, double tolerance )
77 {
78  if ( mTemporaryLocators.contains( vl ) )
79  delete mTemporaryLocators.take( vl );
80 
81  QgsRectangle rect( pointMap.x() - tolerance, pointMap.y() - tolerance,
82  pointMap.x() + tolerance, pointMap.y() + tolerance );
83  QgsPointLocator* vlpl = new QgsPointLocator( vl, destCRS(), &rect );
84  mTemporaryLocators.insert( vl, vlpl );
85  return mTemporaryLocators.value( vl );
86 }
87 
88 bool QgsSnappingUtils::willUseIndex( QgsVectorLayer* vl ) const
89 {
90  if ( mStrategy == IndexAlwaysFull )
91  return true;
92  else if ( mStrategy == IndexNeverFull )
93  return false;
94  else
95  {
96  if ( mHybridNonindexableLayers.contains( vl->id() ) )
97  return false;
98 
99  // if the layer is too big, the locator will later stop indexing it after reaching a threshold
100  return true;
101  }
102 }
103 
104 
106 {
107  if ( segments.isEmpty() )
108  return QgsPointLocator::Match();
109 
110  QSet<QgsPoint> endpoints;
111 
112  // make a geometry
113  QList<QgsGeometry*> geoms;
114  foreach ( const QgsPointLocator::Match& m, segments )
115  {
116  if ( m.hasEdge() )
117  {
118  QgsPolyline pl( 2 );
119  m.edgePoints( pl[0], pl[1] );
120  geoms << QgsGeometry::fromPolyline( pl );
121  endpoints << pl[0] << pl[1];
122  }
123  }
124 
125  QgsGeometry* g = QgsGeometry::unaryUnion( geoms );
126  qDeleteAll( geoms );
127 
128  // get intersection points
129  QList<QgsPoint> newPoints;
130  if ( g->wkbType() == QGis::WKBLineString )
131  {
132  foreach ( const QgsPoint& p, g->asPolyline() )
133  {
134  if ( !endpoints.contains( p ) )
135  newPoints << p;
136  }
137  }
138  if ( g->wkbType() == QGis::WKBMultiLineString )
139  {
140  foreach ( const QgsPolyline& pl, g->asMultiPolyline() )
141  {
142  foreach ( const QgsPoint& p, pl )
143  {
144  if ( !endpoints.contains( p ) )
145  newPoints << p;
146  }
147  }
148  }
149  delete g;
150 
151  if ( newPoints.isEmpty() )
152  return QgsPointLocator::Match();
153 
154  // find the closest points
155  QgsPoint minP;
156  double minSqrDist = 1e20; // "infinity"
157  foreach ( const QgsPoint& p, newPoints )
158  {
159  double sqrDist = pt.sqrDist( p.x(), p.y() );
160  if ( sqrDist < minSqrDist )
161  {
162  minSqrDist = sqrDist;
163  minP = p;
164  }
165  }
166 
167  return QgsPointLocator::Match( QgsPointLocator::Vertex, 0, 0, sqrt( minSqrDist ), minP );
168 }
169 
170 
171 static void _replaceIfBetter( QgsPointLocator::Match& mBest, const QgsPointLocator::Match& mNew, double maxDistance )
172 {
173  // is other match relevant?
174  if ( !mNew.isValid() || mNew.distance() > maxDistance )
175  return;
176 
177  // is other match actually better?
178  if ( mBest.isValid() && mBest.type() == mNew.type() && mBest.distance() - 10e-6 < mNew.distance() )
179  return;
180 
181  // prefer vertex matches to edge matches (even if they are closer)
182  if ( mBest.type() == QgsPointLocator::Vertex && mNew.type() == QgsPointLocator::Edge )
183  return;
184 
185  mBest = mNew; // the other match is better!
186 }
187 
188 
189 static void _updateBestMatch( QgsPointLocator::Match& bestMatch, const QgsPoint& pointMap, QgsPointLocator* loc, int type, double tolerance, QgsPointLocator::MatchFilter* filter )
190 {
191  if ( type & QgsPointLocator::Vertex )
192  {
193  _replaceIfBetter( bestMatch, loc->nearestVertex( pointMap, tolerance, filter ), tolerance );
194  }
195  if ( bestMatch.type() != QgsPointLocator::Vertex && ( type & QgsPointLocator::Edge ) )
196  {
197  _replaceIfBetter( bestMatch, loc->nearestEdge( pointMap, tolerance, filter ), tolerance );
198  }
199 }
200 
201 
203 {
204  return snapToMap( mMapSettings.mapToPixel().toMapCoordinates( point ), filter );
205 }
206 
208 {
209  if ( !mMapSettings.hasValidSettings() )
210  return QgsPointLocator::Match();
211 
212  if ( mSnapToMapMode == SnapCurrentLayer )
213  {
214  if ( !mCurrentLayer )
215  return QgsPointLocator::Match();
216 
217  prepareIndex( QList<QgsVectorLayer*>() << mCurrentLayer );
218 
219  // data from project
220  double tolerance = QgsTolerance::toleranceInProjectUnits( mDefaultTolerance, mCurrentLayer, mMapSettings, mDefaultUnit );
221  int type = mDefaultType;
222 
223  // use ad-hoc locator
224  QgsPointLocator* loc = locatorForLayerUsingStrategy( mCurrentLayer, pointMap, tolerance );
225  if ( !loc )
226  return QgsPointLocator::Match();
227 
228  QgsPointLocator::Match bestMatch;
229  _updateBestMatch( bestMatch, pointMap, loc, type, tolerance, filter );
230 
231  if ( mSnapOnIntersection )
232  {
233  QgsPointLocator* locEdges = locatorForLayerUsingStrategy( mCurrentLayer, pointMap, tolerance );
234  QgsPointLocator::MatchList edges = locEdges->edgesInRect( pointMap, tolerance );
235  _replaceIfBetter( bestMatch, _findClosestSegmentIntersection( pointMap, edges ), tolerance );
236  }
237 
238  return bestMatch;
239  }
240  else if ( mSnapToMapMode == SnapAdvanced )
241  {
242  QList<QgsVectorLayer*> layers;
243  foreach ( const LayerConfig& layerConfig, mLayers )
244  layers << layerConfig.layer;
245  prepareIndex( layers );
246 
247  QgsPointLocator::Match bestMatch;
248  QgsPointLocator::MatchList edges; // for snap on intersection
249  double maxSnapIntTolerance = 0;
250 
251  foreach ( const LayerConfig& layerConfig, mLayers )
252  {
253  double tolerance = QgsTolerance::toleranceInProjectUnits( layerConfig.tolerance, layerConfig.layer, mMapSettings, layerConfig.unit );
254  if ( QgsPointLocator* loc = locatorForLayerUsingStrategy( layerConfig.layer, pointMap, tolerance ) )
255  {
256  _updateBestMatch( bestMatch, pointMap, loc, layerConfig.type, tolerance, filter );
257 
258  if ( mSnapOnIntersection )
259  {
260  edges << loc->edgesInRect( pointMap, tolerance );
261  maxSnapIntTolerance = qMax( maxSnapIntTolerance, tolerance );
262  }
263  }
264  }
265 
266  if ( mSnapOnIntersection )
267  _replaceIfBetter( bestMatch, _findClosestSegmentIntersection( pointMap, edges ), maxSnapIntTolerance );
268 
269  return bestMatch;
270  }
271  else if ( mSnapToMapMode == SnapAllLayers )
272  {
273  // data from project
274  double tolerance = QgsTolerance::toleranceInProjectUnits( mDefaultTolerance, 0, mMapSettings, mDefaultUnit );
275  int type = mDefaultType;
276 
277  QList<QgsVectorLayer*> layers;
278  foreach ( const QString& layerID, mMapSettings.layers() )
279  if ( QgsVectorLayer* vl = qobject_cast<QgsVectorLayer*>( QgsMapLayerRegistry::instance()->mapLayer( layerID ) ) )
280  layers << vl;
281  prepareIndex( layers );
282 
283  QgsPointLocator::MatchList edges; // for snap on intersection
284  QgsPointLocator::Match bestMatch;
285 
286  foreach ( QgsVectorLayer* vl, layers )
287  {
288  if ( QgsPointLocator* loc = locatorForLayerUsingStrategy( vl, pointMap, tolerance ) )
289  {
290  _updateBestMatch( bestMatch, pointMap, loc, type, tolerance, filter );
291 
292  if ( mSnapOnIntersection )
293  edges << loc->edgesInRect( pointMap, tolerance );
294  }
295  }
296 
297  if ( mSnapOnIntersection )
298  _replaceIfBetter( bestMatch, _findClosestSegmentIntersection( pointMap, edges ), tolerance );
299 
300  return bestMatch;
301  }
302 
303  return QgsPointLocator::Match();
304 }
305 
306 
307 void QgsSnappingUtils::prepareIndex( const QList<QgsVectorLayer*>& layers )
308 {
309  // check if we need to build any index
310  QList<QgsVectorLayer*> layersToIndex;
311  foreach ( QgsVectorLayer* vl, layers )
312  {
313  if ( willUseIndex( vl ) && !locatorForLayer( vl )->hasIndex() )
314  layersToIndex << vl;
315  }
316  if ( layersToIndex.isEmpty() )
317  return;
318 
319  // build indexes
320  QTime t; t.start();
321  int i = 0;
322  prepareIndexStarting( layersToIndex.count() );
323  foreach ( QgsVectorLayer* vl, layersToIndex )
324  {
325  QTime tt; tt.start();
326  if ( !locatorForLayer( vl )->init( mStrategy == IndexHybrid ? 1000000 : -1 ) )
327  mHybridNonindexableLayers.insert( vl->id() );
328  QgsDebugMsg( QString( "Index init: %1 ms (%2)" ).arg( tt.elapsed() ).arg( vl->id() ) );
329  prepareIndexProgress( ++i );
330  }
331  QgsDebugMsg( QString( "Prepare index total: %1 ms" ).arg( t.elapsed() ) );
332 }
333 
334 
336 {
337  if ( !mCurrentLayer )
338  return QgsPointLocator::Match();
339 
340  QgsPoint pointMap = mMapSettings.mapToPixel().toMapCoordinates( point );
341  double tolerance = QgsTolerance::vertexSearchRadius( mMapSettings );
342 
343  QgsPointLocator* loc = locatorForLayerUsingStrategy( mCurrentLayer, pointMap, tolerance );
344  if ( !loc )
345  return QgsPointLocator::Match();
346 
347  QgsPointLocator::Match bestMatch;
348  _updateBestMatch( bestMatch, pointMap, loc, type, tolerance, filter );
349  return bestMatch;
350 }
351 
353 {
354  QString oldDestCRS = mMapSettings.hasCrsTransformEnabled() ? mMapSettings.destinationCrs().authid() : QString();
355  QString newDestCRS = settings.hasCrsTransformEnabled() ? settings.destinationCrs().authid() : QString();
356  mMapSettings = settings;
357 
358  if ( newDestCRS != oldDestCRS )
359  clearAllLocators();
360 }
361 
362 void QgsSnappingUtils::setDefaultSettings( int type, double tolerance, QgsTolerance::UnitType unit )
363 {
364  // force map units - can't use layer units for just any layer
365  if ( unit == QgsTolerance::LayerUnits )
367 
368  mDefaultType = type;
369  mDefaultTolerance = tolerance;
370  mDefaultUnit = unit;
371 }
372 
373 void QgsSnappingUtils::defaultSettings( int& type, double& tolerance, QgsTolerance::UnitType& unit )
374 {
375  type = mDefaultType;
376  tolerance = mDefaultTolerance;
377  unit = mDefaultUnit;
378 }
379 
380 const QgsCoordinateReferenceSystem* QgsSnappingUtils::destCRS()
381 {
382  return mMapSettings.hasCrsTransformEnabled() ? &mMapSettings.destinationCrs() : 0;
383 }
384 
385 
387 {
388  mSnapToMapMode = SnapCurrentLayer;
389  mLayers.clear();
390 
391  QString snapMode = QgsProject::instance()->readEntry( "Digitizing", "/SnappingMode" );
392 
393  int type = 0;
394  QString snapType = QgsProject::instance()->readEntry( "Digitizing", "/DefaultSnapType", QString( "off" ) );
395  if ( snapType == "to segment" )
396  type = QgsPointLocator::Edge;
397  else if ( snapType == "to vertex and segment" )
399  else if ( snapType == "to vertex" )
401  double tolerance = QgsProject::instance()->readDoubleEntry( "Digitizing", "/DefaultSnapTolerance", 0 );
402  QgsTolerance::UnitType unit = ( QgsTolerance::UnitType ) QgsProject::instance()->readNumEntry( "Digitizing", "/DefaultSnapToleranceUnit", QgsTolerance::ProjectUnits );
403  setDefaultSettings( type, tolerance, unit );
404 
405  //snapping on intersection on?
406  setSnapOnIntersections( QgsProject::instance()->readNumEntry( "Digitizing", "/IntersectionSnapping", 0 ) );
407 
408  //read snapping settings from project
409  bool snappingDefinedInProject, ok;
410  QStringList layerIdList = QgsProject::instance()->readListEntry( "Digitizing", "/LayerSnappingList", QStringList(), &snappingDefinedInProject );
411  QStringList enabledList = QgsProject::instance()->readListEntry( "Digitizing", "/LayerSnappingEnabledList", QStringList(), &ok );
412  QStringList toleranceList = QgsProject::instance()->readListEntry( "Digitizing", "/LayerSnappingToleranceList", QStringList(), &ok );
413  QStringList toleranceUnitList = QgsProject::instance()->readListEntry( "Digitizing", "/LayerSnappingToleranceUnitList", QStringList(), &ok );
414  QStringList snapToList = QgsProject::instance()->readListEntry( "Digitizing", "/LayerSnapToList", QStringList(), &ok );
415 
416  // lists must have the same size, otherwise something is wrong
417  if ( layerIdList.size() != enabledList.size() ||
418  layerIdList.size() != toleranceList.size() ||
419  layerIdList.size() != toleranceUnitList.size() ||
420  layerIdList.size() != snapToList.size() )
421  return;
422 
423  if ( !snappingDefinedInProject )
424  return; // nothing defined in project - use current layer
425 
426  // Use snapping information from the project
427  if ( snapMode == "current_layer" )
428  mSnapToMapMode = SnapCurrentLayer;
429  else if ( snapMode == "all_layers" )
430  mSnapToMapMode = SnapAllLayers;
431  else // either "advanced" or empty (for background compatibility)
432  mSnapToMapMode = SnapAdvanced;
433 
434 
435 
436  // load layers, tolerances, snap type
437  QStringList::const_iterator layerIt( layerIdList.constBegin() );
438  QStringList::const_iterator tolIt( toleranceList.constBegin() );
439  QStringList::const_iterator tolUnitIt( toleranceUnitList.constBegin() );
440  QStringList::const_iterator snapIt( snapToList.constBegin() );
441  QStringList::const_iterator enabledIt( enabledList.constBegin() );
442  for ( ; layerIt != layerIdList.constEnd(); ++layerIt, ++tolIt, ++tolUnitIt, ++snapIt, ++enabledIt )
443  {
444  // skip layer if snapping is not enabled
445  if ( *enabledIt != "enabled" )
446  continue;
447 
448  QgsVectorLayer *vlayer = qobject_cast<QgsVectorLayer *>( QgsMapLayerRegistry::instance()->mapLayer( *layerIt ) );
449  if ( !vlayer || !vlayer->hasGeometryType() )
450  continue;
451 
452  int t = ( *snapIt == "to_vertex" ? QgsPointLocator::Vertex :
453  ( *snapIt == "to_segment" ? QgsPointLocator::Edge :
455  mLayers.append( LayerConfig( vlayer, t, tolIt->toDouble(), ( QgsTolerance::UnitType ) tolUnitIt->toInt() ) );
456  }
457 
458 }
459 
460 void QgsSnappingUtils::onLayersWillBeRemoved( QStringList layerIds )
461 {
462  // remove locators for layers that are going to be deleted
463  foreach ( QString layerId, layerIds )
464  {
465  for ( LocatorsMap::iterator it = mLocators.begin(); it != mLocators.end(); )
466  {
467  if ( it.key()->id() == layerId )
468  {
469  delete it.value();
470  it = mLocators.erase( it );
471  }
472  else
473  {
474  ++it;
475  }
476  }
477 
478  for ( LocatorsMap::iterator it = mTemporaryLocators.begin(); it != mTemporaryLocators.end(); )
479  {
480  if ( it.key()->id() == layerId )
481  {
482  delete it.value();
483  it = mTemporaryLocators.erase( it );
484  }
485  else
486  {
487  ++it;
488  }
489  }
490  }
491 }
492