Branch data Line data Source code
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 : : #include "qgsgeometry.h"
18 : : #include "qgsproject.h"
19 : : #include "qgsvectorlayer.h"
20 : : #include "qgslogger.h"
21 : : #include "qgsrenderer.h"
22 : :
23 : 0 : QgsSnappingUtils::QgsSnappingUtils( QObject *parent, bool enableSnappingForInvisibleFeature )
24 : 0 : : QObject( parent )
25 : 0 : , mSnappingConfig( QgsProject::instance() )
26 : 0 : , mEnableSnappingForInvisibleFeature( enableSnappingForInvisibleFeature )
27 : 0 : {
28 : 0 : }
29 : :
30 : 0 : QgsSnappingUtils::~QgsSnappingUtils()
31 : 0 : {
32 : 0 : clearAllLocators();
33 : 0 : }
34 : :
35 : :
36 : 0 : QgsPointLocator *QgsSnappingUtils::locatorForLayer( QgsVectorLayer *vl )
37 : : {
38 : 0 : if ( !vl )
39 : 0 : return nullptr;
40 : :
41 : 0 : if ( !mLocators.contains( vl ) )
42 : : {
43 : 0 : QgsPointLocator *vlpl = new QgsPointLocator( vl, destinationCrs(), mMapSettings.transformContext(), nullptr );
44 : 0 : connect( vlpl, &QgsPointLocator::initFinished, this, &QgsSnappingUtils::onInitFinished );
45 : 0 : mLocators.insert( vl, vlpl );
46 : 0 : }
47 : 0 : return mLocators.value( vl );
48 : 0 : }
49 : :
50 : 0 : void QgsSnappingUtils::clearAllLocators()
51 : : {
52 : 0 : qDeleteAll( mLocators );
53 : 0 : mLocators.clear();
54 : :
55 : 0 : qDeleteAll( mTemporaryLocators );
56 : 0 : mTemporaryLocators.clear();
57 : 0 : }
58 : :
59 : :
60 : 0 : QgsPointLocator *QgsSnappingUtils::locatorForLayerUsingStrategy( QgsVectorLayer *vl, const QgsPointXY &pointMap, double tolerance )
61 : : {
62 : 0 : if ( vl->geometryType() == QgsWkbTypes::NullGeometry || mStrategy == IndexNeverFull )
63 : 0 : return nullptr;
64 : :
65 : 0 : QgsRectangle aoi( pointMap.x() - tolerance, pointMap.y() - tolerance,
66 : 0 : pointMap.x() + tolerance, pointMap.y() + tolerance );
67 : :
68 : 0 : QgsPointLocator *loc = locatorForLayer( vl );
69 : :
70 : 0 : if ( loc->isIndexing() || isIndexPrepared( loc, aoi ) )
71 : 0 : return loc;
72 : : else
73 : 0 : return temporaryLocatorForLayer( vl, pointMap, tolerance );
74 : 0 : }
75 : :
76 : 0 : QgsPointLocator *QgsSnappingUtils::temporaryLocatorForLayer( QgsVectorLayer *vl, const QgsPointXY &pointMap, double tolerance )
77 : : {
78 : 0 : if ( mTemporaryLocators.contains( vl ) )
79 : 0 : delete mTemporaryLocators.take( vl );
80 : :
81 : 0 : QgsRectangle rect( pointMap.x() - tolerance, pointMap.y() - tolerance,
82 : 0 : pointMap.x() + tolerance, pointMap.y() + tolerance );
83 : :
84 : 0 : QgsPointLocator *vlpl = new QgsPointLocator( vl, destinationCrs(), mMapSettings.transformContext(), &rect );
85 : 0 : connect( vlpl, &QgsPointLocator::initFinished, this, &QgsSnappingUtils::onInitFinished );
86 : :
87 : 0 : mTemporaryLocators.insert( vl, vlpl );
88 : 0 : return mTemporaryLocators.value( vl );
89 : 0 : }
90 : :
91 : 0 : bool QgsSnappingUtils::isIndexPrepared( QgsPointLocator *loc, const QgsRectangle &areaOfInterest )
92 : : {
93 : 0 : if ( mStrategy == IndexAlwaysFull && loc->hasIndex() )
94 : 0 : return true;
95 : :
96 : 0 : if ( mStrategy == IndexExtent && loc->hasIndex() && ( !loc->extent() || loc->extent()->intersects( areaOfInterest ) ) )
97 : 0 : return true;
98 : :
99 : 0 : QgsRectangle aoi( areaOfInterest );
100 : 0 : aoi.scale( 0.999 );
101 : 0 : return mStrategy == IndexHybrid && loc->hasIndex() && ( !loc->extent() || loc->extent()->contains( aoi ) ); // the index - even if it exists - is not suitable
102 : 0 : }
103 : :
104 : 0 : static QgsPointLocator::Match _findClosestSegmentIntersection( const QgsPointXY &pt, const QgsPointLocator::MatchList &segments )
105 : : {
106 : 0 : if ( segments.isEmpty() )
107 : 0 : return QgsPointLocator::Match();
108 : :
109 : 0 : QSet<QgsPointXY> endpoints;
110 : :
111 : : // make a geometry
112 : 0 : QVector<QgsGeometry> geoms;
113 : 0 : const auto constSegments = segments;
114 : 0 : for ( const QgsPointLocator::Match &m : constSegments )
115 : : {
116 : 0 : if ( m.hasEdge() )
117 : : {
118 : 0 : QgsPolylineXY pl( 2 );
119 : 0 : m.edgePoints( pl[0], pl[1] );
120 : 0 : geoms << QgsGeometry::fromPolylineXY( pl );
121 : 0 : endpoints << pl[0] << pl[1];
122 : 0 : }
123 : : }
124 : :
125 : 0 : QgsGeometry g = QgsGeometry::unaryUnion( geoms );
126 : :
127 : : // get intersection points
128 : 0 : QList<QgsPointXY> newPoints;
129 : 0 : if ( g.wkbType() == QgsWkbTypes::LineString )
130 : : {
131 : 0 : const auto constAsPolyline = g.asPolyline();
132 : 0 : for ( const QgsPointXY &p : constAsPolyline )
133 : : {
134 : 0 : if ( !endpoints.contains( p ) )
135 : 0 : newPoints << p;
136 : : }
137 : 0 : }
138 : 0 : if ( g.wkbType() == QgsWkbTypes::MultiLineString )
139 : : {
140 : 0 : const auto constAsMultiPolyline = g.asMultiPolyline();
141 : 0 : for ( const QgsPolylineXY &pl : constAsMultiPolyline )
142 : : {
143 : 0 : const auto constPl = pl;
144 : 0 : for ( const QgsPointXY &p : constPl )
145 : : {
146 : 0 : if ( !endpoints.contains( p ) )
147 : 0 : newPoints << p;
148 : : }
149 : 0 : }
150 : 0 : }
151 : :
152 : 0 : if ( newPoints.isEmpty() )
153 : 0 : return QgsPointLocator::Match();
154 : :
155 : : // find the closest points
156 : 0 : QgsPointXY minP;
157 : 0 : double minSqrDist = 1e20; // "infinity"
158 : 0 : const auto constNewPoints = newPoints;
159 : 0 : for ( const QgsPointXY &p : constNewPoints )
160 : : {
161 : 0 : double sqrDist = pt.sqrDist( p.x(), p.y() );
162 : 0 : if ( sqrDist < minSqrDist )
163 : : {
164 : 0 : minSqrDist = sqrDist;
165 : 0 : minP = p;
166 : 0 : }
167 : : }
168 : :
169 : 0 : return QgsPointLocator::Match( QgsPointLocator::Vertex, nullptr, 0, std::sqrt( minSqrDist ), minP );
170 : 0 : }
171 : :
172 : 0 : static void _replaceIfBetter( QgsPointLocator::Match &bestMatch, const QgsPointLocator::Match &candidateMatch, double maxDistance )
173 : : {
174 : : // is candidate match relevant?
175 : 0 : if ( !candidateMatch.isValid() || candidateMatch.distance() > maxDistance )
176 : 0 : return;
177 : :
178 : : // is candidate match actually better?
179 : 0 : if ( bestMatch.isValid() && bestMatch.type() == candidateMatch.type() && bestMatch.distance() - 10e-6 < candidateMatch.distance() )
180 : 0 : return;
181 : :
182 : : // ORDER
183 : : // LineEndpoint
184 : : // Vertex, Intersection
185 : : // Middle
186 : : // Centroid
187 : : // Edge
188 : : // Area
189 : :
190 : : // first line endpoint -- these are like vertex matches, but even more strict
191 : 0 : if ( ( bestMatch.type() & QgsPointLocator::LineEndpoint ) && !( candidateMatch.type() & QgsPointLocator::LineEndpoint ) )
192 : 0 : return;
193 : 0 : if ( candidateMatch.type() & QgsPointLocator::LineEndpoint )
194 : : {
195 : 0 : bestMatch = candidateMatch;
196 : 0 : return;
197 : : }
198 : :
199 : : // Second Vertex, or intersection
200 : 0 : if ( ( bestMatch.type() & QgsPointLocator::Vertex ) && !( candidateMatch.type() & QgsPointLocator::Vertex ) )
201 : 0 : return;
202 : 0 : if ( candidateMatch.type() & QgsPointLocator::Vertex )
203 : : {
204 : 0 : bestMatch = candidateMatch;
205 : 0 : return;
206 : : }
207 : :
208 : : // prefer vertex, centroid, middle matches over edge matches (even if they are closer)
209 : 0 : if ( ( bestMatch.type() & QgsPointLocator::Centroid || bestMatch.type() & QgsPointLocator::MiddleOfSegment ) && ( candidateMatch.type() & QgsPointLocator::Edge || candidateMatch.type() & QgsPointLocator::Area ) )
210 : 0 : return;
211 : :
212 : : // prefer middle matches over centroid matches (even if they are closer)
213 : 0 : if ( ( bestMatch.type() & QgsPointLocator::MiddleOfSegment ) && ( candidateMatch.type() & QgsPointLocator::Centroid ) )
214 : 0 : return;
215 : :
216 : 0 : bestMatch = candidateMatch; // the other match is better!
217 : 0 : }
218 : :
219 : 0 : static void _updateBestMatch( QgsPointLocator::Match &bestMatch, const QgsPointXY &pointMap, QgsPointLocator *loc, QgsPointLocator::Types type, double tolerance, QgsPointLocator::MatchFilter *filter, bool relaxed )
220 : : {
221 : 0 : if ( type & QgsPointLocator::Vertex )
222 : : {
223 : 0 : _replaceIfBetter( bestMatch, loc->nearestVertex( pointMap, tolerance, filter, relaxed ), tolerance );
224 : 0 : }
225 : 0 : if ( bestMatch.type() != QgsPointLocator::Vertex && ( type & QgsPointLocator::Edge ) )
226 : : {
227 : 0 : _replaceIfBetter( bestMatch, loc->nearestEdge( pointMap, tolerance, filter, relaxed ), tolerance );
228 : 0 : }
229 : 0 : if ( bestMatch.type() != QgsPointLocator::Vertex && bestMatch.type() != QgsPointLocator::Edge && ( type & QgsPointLocator::Area ) )
230 : : {
231 : : // if edges were detected, set tolerance to 0 to only do pointInPolygon (and avoid redo nearestEdge)
232 : 0 : if ( type & QgsPointLocator::Edge )
233 : 0 : tolerance = 0;
234 : 0 : _replaceIfBetter( bestMatch, loc->nearestArea( pointMap, tolerance, filter, relaxed ), tolerance );
235 : 0 : }
236 : 0 : if ( type & QgsPointLocator::Centroid )
237 : : {
238 : 0 : _replaceIfBetter( bestMatch, loc->nearestCentroid( pointMap, tolerance, filter ), tolerance );
239 : 0 : }
240 : 0 : if ( type & QgsPointLocator::MiddleOfSegment )
241 : : {
242 : 0 : _replaceIfBetter( bestMatch, loc->nearestMiddleOfSegment( pointMap, tolerance, filter ), tolerance );
243 : 0 : }
244 : 0 : if ( type & QgsPointLocator::LineEndpoint )
245 : : {
246 : 0 : _replaceIfBetter( bestMatch, loc->nearestLineEndpoints( pointMap, tolerance, filter ), tolerance );
247 : 0 : }
248 : 0 : }
249 : :
250 : :
251 : 0 : static QgsPointLocator::Types _snappingTypeToPointLocatorType( QgsSnappingConfig::SnappingTypeFlag type )
252 : : {
253 : 0 : return QgsPointLocator::Types( static_cast<int>( type ) );
254 : : }
255 : :
256 : 0 : QgsPointLocator::Match QgsSnappingUtils::snapToMap( QPoint point, QgsPointLocator::MatchFilter *filter, bool relaxed )
257 : : {
258 : 0 : return snapToMap( mMapSettings.mapToPixel().toMapCoordinates( point ), filter, relaxed );
259 : : }
260 : :
261 : 0 : inline QgsRectangle _areaOfInterest( const QgsPointXY &point, double tolerance )
262 : : {
263 : 0 : return QgsRectangle( point.x() - tolerance, point.y() - tolerance,
264 : 0 : point.x() + tolerance, point.y() + tolerance );
265 : : }
266 : :
267 : 0 : QgsPointLocator::Match QgsSnappingUtils::snapToMap( const QgsPointXY &pointMap, QgsPointLocator::MatchFilter *filter, bool relaxed )
268 : : {
269 : 0 : if ( !mMapSettings.hasValidSettings() || !mSnappingConfig.enabled() )
270 : : {
271 : 0 : return QgsPointLocator::Match();
272 : : }
273 : :
274 : 0 : if ( mSnappingConfig.mode() == QgsSnappingConfig::ActiveLayer )
275 : : {
276 : 0 : if ( !mCurrentLayer || mSnappingConfig.typeFlag() == QgsSnappingConfig::NoSnapFlag )
277 : 0 : return QgsPointLocator::Match();
278 : :
279 : : // data from project
280 : 0 : double tolerance = QgsTolerance::toleranceInProjectUnits( mSnappingConfig.tolerance(), mCurrentLayer, mMapSettings, mSnappingConfig.units() );
281 : 0 : QgsPointLocator::Types type = _snappingTypeToPointLocatorType( mSnappingConfig.typeFlag() );
282 : :
283 : 0 : prepareIndex( QList<LayerAndAreaOfInterest>() << qMakePair( mCurrentLayer, _areaOfInterest( pointMap, tolerance ) ), relaxed );
284 : :
285 : : // use ad-hoc locator
286 : 0 : QgsPointLocator *loc = locatorForLayerUsingStrategy( mCurrentLayer, pointMap, tolerance );
287 : 0 : if ( !loc )
288 : 0 : return QgsPointLocator::Match();
289 : 0 :
290 : 0 : QgsPointLocator::Match bestMatch;
291 : 0 : QgsPointLocator::MatchList edges; // for snap on intersection
292 : 0 : _updateBestMatch( bestMatch, pointMap, loc, type, tolerance, filter, relaxed );
293 : :
294 : 0 : if ( mSnappingConfig.intersectionSnapping() )
295 : : {
296 : 0 : QgsPointLocator *locEdges = locatorForLayerUsingStrategy( mCurrentLayer, pointMap, tolerance );
297 : 0 : if ( !locEdges )
298 : 0 : return QgsPointLocator::Match();
299 : 0 : edges = locEdges->edgesInRect( pointMap, tolerance );
300 : 0 : }
301 : :
302 : 0 : for ( QgsVectorLayer *vl : mExtraSnapLayers )
303 : : {
304 : 0 : QgsPointLocator *loc = locatorForLayerUsingStrategy( vl, pointMap, tolerance );
305 : 0 : _updateBestMatch( bestMatch, pointMap, loc, type, tolerance, filter, false );
306 : 0 : if ( mSnappingConfig.intersectionSnapping() )
307 : 0 : edges << loc->edgesInRect( pointMap, tolerance );
308 : : }
309 : :
310 : 0 : if ( mSnappingConfig.intersectionSnapping() )
311 : : {
312 : 0 : _replaceIfBetter( bestMatch, _findClosestSegmentIntersection( pointMap, edges ), tolerance );
313 : 0 : }
314 : :
315 : 0 : return bestMatch;
316 : 0 : }
317 : 0 : else if ( mSnappingConfig.mode() == QgsSnappingConfig::AdvancedConfiguration )
318 : : {
319 : 0 : QList<LayerAndAreaOfInterest> layers;
320 : 0 : QList<LayerConfig> filteredConfigs;
321 : :
322 : 0 : //maximum scale is the one with smallest denominator
323 : : //minimum scale is the one with highest denominator
324 : : //So : maxscale < range on which snapping is enabled < minscale
325 : 0 : bool inRangeGlobal = ( mSnappingConfig.minimumScale() <= 0.0 || mMapSettings.scale() <= mSnappingConfig.minimumScale() )
326 : 0 : && ( mSnappingConfig.maximumScale() <= 0.0 || mMapSettings.scale() >= mSnappingConfig.maximumScale() );
327 : :
328 : 0 : for ( const LayerConfig &layerConfig : std::as_const( mLayers ) )
329 : : {
330 : 0 : QgsSnappingConfig::IndividualLayerSettings layerSettings = mSnappingConfig.individualLayerSettings( layerConfig.layer );
331 : :
332 : 0 : bool inRangeLayer = ( layerSettings.minimumScale() <= 0.0 || mMapSettings.scale() <= layerSettings.minimumScale() )
333 : 0 : && ( layerSettings.maximumScale() <= 0.0 || mMapSettings.scale() >= layerSettings.maximumScale() );
334 : :
335 : : //If limit to scale is disabled, snapping activated on all layer
336 : : //If no per layer config is set use the global one, otherwise use the layer config
337 : 0 : if ( mSnappingConfig.scaleDependencyMode() == QgsSnappingConfig::Disabled
338 : 0 : || ( mSnappingConfig.scaleDependencyMode() == QgsSnappingConfig::Global && inRangeGlobal )
339 : 0 : || ( mSnappingConfig.scaleDependencyMode() == QgsSnappingConfig::PerLayer && inRangeLayer ) )
340 : : {
341 : 0 : double tolerance = QgsTolerance::toleranceInProjectUnits( layerConfig.tolerance, layerConfig.layer, mMapSettings, layerConfig.unit );
342 : 0 : layers << qMakePair( layerConfig.layer, _areaOfInterest( pointMap, tolerance ) );
343 : 0 : filteredConfigs << layerConfig;
344 : 0 : }
345 : : }
346 : 0 : prepareIndex( layers, relaxed );
347 : :
348 : 0 : QgsPointLocator::Match bestMatch;
349 : 0 : QgsPointLocator::MatchList edges; // for snap on intersection
350 : 0 : double maxTolerance = 0;
351 : 0 : QgsPointLocator::Type maxTypes = QgsPointLocator::Invalid;
352 : :
353 : 0 : for ( const LayerConfig &layerConfig : std::as_const( filteredConfigs ) )
354 : : {
355 : 0 : double tolerance = QgsTolerance::toleranceInProjectUnits( layerConfig.tolerance, layerConfig.layer, mMapSettings, layerConfig.unit );
356 : 0 : if ( QgsPointLocator *loc = locatorForLayerUsingStrategy( layerConfig.layer, pointMap, tolerance ) )
357 : : {
358 : 0 : _updateBestMatch( bestMatch, pointMap, loc, layerConfig.type, tolerance, filter, relaxed );
359 : :
360 : 0 : if ( mSnappingConfig.intersectionSnapping() )
361 : : {
362 : 0 : edges << loc->edgesInRect( pointMap, tolerance );
363 : 0 : }
364 : : // We keep the maximum tolerance for intersection snapping and extra snapping
365 : 0 : maxTolerance = std::max( maxTolerance, tolerance );
366 : : // To avoid yet an additional setting, on extra snappings, we use the combination of all enabled snap types
367 : 0 : maxTypes = static_cast<QgsPointLocator::Type>( maxTypes | layerConfig.type );
368 : 0 : }
369 : : }
370 : :
371 : 0 : for ( QgsVectorLayer *vl : mExtraSnapLayers )
372 : : {
373 : 0 : QgsPointLocator *loc = locatorForLayerUsingStrategy( vl, pointMap, maxTolerance );
374 : 0 : _updateBestMatch( bestMatch, pointMap, loc, maxTypes, maxTolerance, filter, false );
375 : 0 : if ( mSnappingConfig.intersectionSnapping() )
376 : 0 : edges << loc->edgesInRect( pointMap, maxTolerance );
377 : : }
378 : :
379 : 0 : if ( mSnappingConfig.intersectionSnapping() )
380 : 0 : _replaceIfBetter( bestMatch, _findClosestSegmentIntersection( pointMap, edges ), maxTolerance );
381 : :
382 : : return bestMatch;
383 : 0 : }
384 : 0 : else if ( mSnappingConfig.mode() == QgsSnappingConfig::AllLayers )
385 : : {
386 : : // data from project
387 : 0 : double tolerance = QgsTolerance::toleranceInProjectUnits( mSnappingConfig.tolerance(), nullptr, mMapSettings, mSnappingConfig.units() );
388 : 0 : QgsPointLocator::Types type = _snappingTypeToPointLocatorType( mSnappingConfig.typeFlag() );
389 : 0 : QgsRectangle aoi = _areaOfInterest( pointMap, tolerance );
390 : :
391 : 0 : QList<LayerAndAreaOfInterest> layers;
392 : 0 : const auto constLayers = mMapSettings.layers();
393 : 0 : for ( QgsMapLayer *layer : constLayers )
394 : 0 : if ( QgsVectorLayer *vl = qobject_cast<QgsVectorLayer *>( layer ) )
395 : 0 : layers << qMakePair( vl, aoi );
396 : 0 : prepareIndex( layers, relaxed );
397 : :
398 : 0 : QgsPointLocator::MatchList edges; // for snap on intersection
399 : 0 : QgsPointLocator::Match bestMatch;
400 : :
401 : 0 : for ( const LayerAndAreaOfInterest &entry : std::as_const( layers ) )
402 : : {
403 : 0 : QgsVectorLayer *vl = entry.first;
404 : 0 : if ( QgsPointLocator *loc = locatorForLayerUsingStrategy( vl, pointMap, tolerance ) )
405 : : {
406 : 0 : _updateBestMatch( bestMatch, pointMap, loc, type, tolerance, filter, relaxed );
407 : :
408 : 0 : if ( mSnappingConfig.intersectionSnapping() )
409 : 0 : edges << loc->edgesInRect( pointMap, tolerance );
410 : 0 : }
411 : : }
412 : :
413 : 0 : for ( QgsVectorLayer *vl : mExtraSnapLayers )
414 : : {
415 : 0 : QgsPointLocator *loc = locatorForLayerUsingStrategy( vl, pointMap, tolerance );
416 : 0 : _updateBestMatch( bestMatch, pointMap, loc, type, tolerance, filter, false );
417 : 0 : if ( mSnappingConfig.intersectionSnapping() )
418 : 0 : edges << loc->edgesInRect( pointMap, tolerance );
419 : : }
420 : :
421 : 0 : if ( mSnappingConfig.intersectionSnapping() )
422 : 0 : _replaceIfBetter( bestMatch, _findClosestSegmentIntersection( pointMap, edges ), tolerance );
423 : :
424 : : return bestMatch;
425 : 0 : }
426 : :
427 : 0 : return QgsPointLocator::Match();
428 : 0 : }
429 : :
430 : 0 : void QgsSnappingUtils::onInitFinished( bool ok )
431 : : {
432 : 0 : QgsPointLocator *loc = static_cast<QgsPointLocator *>( sender() );
433 : :
434 : : // point locator init didn't work out - too many features!
435 : : // let's make the allowed area smaller for the next time
436 : 0 : if ( !ok )
437 : : {
438 : 0 : mHybridMaxAreaPerLayer[loc->layer()->id()] /= 4;
439 : 0 : }
440 : 0 : }
441 : :
442 : 0 : void QgsSnappingUtils::prepareIndex( const QList<LayerAndAreaOfInterest> &layers, bool relaxed )
443 : : {
444 : : // check if we need to build any index
445 : 0 : QList<LayerAndAreaOfInterest> layersToIndex;
446 : 0 : const auto constLayers = layers;
447 : 0 : for ( const LayerAndAreaOfInterest &entry : constLayers )
448 : : {
449 : 0 : QgsVectorLayer *vl = entry.first;
450 : :
451 : 0 : if ( vl->geometryType() == QgsWkbTypes::NullGeometry || mStrategy == IndexNeverFull )
452 : 0 : continue;
453 : :
454 : 0 : QgsPointLocator *loc = locatorForLayer( vl );
455 : :
456 : 0 : if ( !loc->isIndexing() && !isIndexPrepared( loc, entry.second ) )
457 : 0 : layersToIndex << entry;
458 : : }
459 : 0 : if ( !layersToIndex.isEmpty() )
460 : : {
461 : : // build indexes
462 : 0 : QElapsedTimer t;
463 : 0 : int i = 0;
464 : :
465 : 0 : if ( !relaxed )
466 : : {
467 : 0 : t.start();
468 : 0 : prepareIndexStarting( layersToIndex.count() );
469 : 0 : }
470 : :
471 : 0 : for ( const LayerAndAreaOfInterest &entry : layersToIndex )
472 : : {
473 : 0 : QgsVectorLayer *vl = entry.first;
474 : 0 : QgsPointLocator *loc = locatorForLayer( vl );
475 : :
476 : 0 : if ( loc->isIndexing() && !relaxed )
477 : : {
478 : 0 : loc->waitForIndexingFinished();
479 : 0 : }
480 : :
481 : :
482 : 0 : if ( !mEnableSnappingForInvisibleFeature )
483 : : {
484 : 0 : QgsRenderContext ctx = QgsRenderContext::fromMapSettings( mMapSettings );
485 : 0 : loc->setRenderContext( &ctx );
486 : 0 : }
487 : :
488 : 0 : if ( mStrategy == IndexExtent )
489 : : {
490 : 0 : QgsRectangle rect( mMapSettings.visibleExtent() );
491 : 0 : loc->setExtent( &rect );
492 : 0 : loc->init( -1, relaxed );
493 : 0 : }
494 : 0 : else if ( mStrategy == IndexHybrid )
495 : : {
496 : : // first time the layer is used? - let's set an initial guess about indexing
497 : 0 : if ( !mHybridMaxAreaPerLayer.contains( vl->id() ) )
498 : : {
499 : 0 : int totalFeatureCount = vl->featureCount();
500 : 0 : if ( totalFeatureCount < mHybridPerLayerFeatureLimit )
501 : : {
502 : : // index the whole layer
503 : 0 : mHybridMaxAreaPerLayer[vl->id()] = -1;
504 : 0 : }
505 : : else
506 : : {
507 : : // estimate for how big area it probably makes sense to build partial index to not exceed the limit
508 : : // (we may change the limit later)
509 : 0 : QgsRectangle layerExtent = mMapSettings.layerExtentToOutputExtent( vl, vl->extent() );
510 : 0 : double totalArea = layerExtent.width() * layerExtent.height();
511 : 0 : mHybridMaxAreaPerLayer[vl->id()] = totalArea * mHybridPerLayerFeatureLimit / totalFeatureCount / 4;
512 : : }
513 : 0 : }
514 : :
515 : 0 : double indexReasonableArea = mHybridMaxAreaPerLayer[vl->id()];
516 : 0 : if ( indexReasonableArea == -1 )
517 : : {
518 : : // we can safely index the whole layer
519 : 0 : loc->init( -1, relaxed );
520 : 0 : }
521 : : else
522 : : {
523 : : // use area as big as we think may fit into our limit
524 : 0 : QgsPointXY c = entry.second.center();
525 : 0 : double halfSide = std::sqrt( indexReasonableArea ) / 2;
526 : 0 : QgsRectangle rect( c.x() - halfSide, c.y() - halfSide,
527 : 0 : c.x() + halfSide, c.y() + halfSide );
528 : 0 : loc->setExtent( &rect );
529 : :
530 : : // see if it's possible build index for this area
531 : 0 : loc->init( mHybridPerLayerFeatureLimit, relaxed );
532 : : }
533 : :
534 : 0 : }
535 : : else // full index strategy
536 : 0 : loc->init( relaxed );
537 : :
538 : 0 : if ( !relaxed )
539 : 0 : prepareIndexProgress( ++i );
540 : : }
541 : :
542 : 0 : if ( !relaxed )
543 : : {
544 : 0 : QgsDebugMsg( QStringLiteral( "Prepare index total: %1 ms" ).arg( t.elapsed() ) );
545 : 0 : }
546 : 0 : }
547 : 0 : }
548 : :
549 : 0 : QgsSnappingConfig QgsSnappingUtils::config() const
550 : : {
551 : 0 : return mSnappingConfig;
552 : : }
553 : :
554 : 0 : void QgsSnappingUtils::setEnableSnappingForInvisibleFeature( bool enable )
555 : : {
556 : 0 : mEnableSnappingForInvisibleFeature = enable;
557 : 0 : }
558 : :
559 : 0 : void QgsSnappingUtils::setConfig( const QgsSnappingConfig &config )
560 : : {
561 : 0 : if ( mSnappingConfig == config )
562 : 0 : return;
563 : :
564 : 0 : if ( mSnappingConfig.individualLayerSettings() != config.individualLayerSettings() )
565 : 0 : onIndividualLayerSettingsChanged( config.individualLayerSettings() );
566 : :
567 : 0 : mSnappingConfig = config;
568 : :
569 : 0 : emit configChanged( mSnappingConfig );
570 : 0 : }
571 : :
572 : 0 : void QgsSnappingUtils::toggleEnabled()
573 : : {
574 : 0 : mSnappingConfig.setEnabled( !mSnappingConfig.enabled() );
575 : 0 : emit configChanged( mSnappingConfig );
576 : 0 : }
577 : :
578 : 0 : QgsPointLocator::Match QgsSnappingUtils::snapToCurrentLayer( QPoint point, QgsPointLocator::Types type, QgsPointLocator::MatchFilter *filter )
579 : : {
580 : 0 : if ( !mCurrentLayer )
581 : 0 : return QgsPointLocator::Match();
582 : :
583 : 0 : QgsPointXY pointMap = mMapSettings.mapToPixel().toMapCoordinates( point );
584 : 0 : double tolerance = QgsTolerance::vertexSearchRadius( mMapSettings );
585 : :
586 : 0 : QgsPointLocator *loc = locatorForLayerUsingStrategy( mCurrentLayer, pointMap, tolerance );
587 : 0 : if ( !loc )
588 : 0 : return QgsPointLocator::Match();
589 : :
590 : 0 : QgsPointLocator::Match bestMatch;
591 : 0 : _updateBestMatch( bestMatch, pointMap, loc, type, tolerance, filter, false );
592 : 0 : return bestMatch;
593 : 0 : }
594 : :
595 : 0 : void QgsSnappingUtils::setMapSettings( const QgsMapSettings &settings )
596 : : {
597 : 0 : QString oldDestCRS = mMapSettings.destinationCrs().authid();
598 : 0 : QString newDestCRS = settings.destinationCrs().authid();
599 : 0 : mMapSettings = settings;
600 : :
601 : 0 : if ( newDestCRS != oldDestCRS )
602 : 0 : clearAllLocators();
603 : 0 : }
604 : :
605 : 0 : void QgsSnappingUtils::setCurrentLayer( QgsVectorLayer *layer )
606 : : {
607 : 0 : mCurrentLayer = layer;
608 : 0 : }
609 : :
610 : 0 : QString QgsSnappingUtils::dump()
611 : : {
612 : 0 : QString msg = QStringLiteral( "--- SNAPPING UTILS DUMP ---\n" );
613 : :
614 : 0 : if ( !mMapSettings.hasValidSettings() )
615 : : {
616 : 0 : msg += QLatin1String( "invalid map settings!" );
617 : 0 : return msg;
618 : : }
619 : :
620 : 0 : QList<LayerConfig> layers;
621 : :
622 : 0 : if ( mSnappingConfig.mode() == QgsSnappingConfig::ActiveLayer )
623 : : {
624 : 0 : if ( mSnappingConfig.mode() == QgsSnappingConfig::ActiveLayer && !mCurrentLayer )
625 : : {
626 : 0 : msg += QLatin1String( "no current layer!" );
627 : 0 : return msg;
628 : : }
629 : :
630 : 0 : layers << LayerConfig( mCurrentLayer, _snappingTypeToPointLocatorType( mSnappingConfig.typeFlag() ), mSnappingConfig.tolerance(), mSnappingConfig.units() );
631 : 0 : }
632 : 0 : else if ( mSnappingConfig.mode() == QgsSnappingConfig::AllLayers )
633 : : {
634 : 0 : const auto constLayers = mMapSettings.layers();
635 : 0 : for ( QgsMapLayer *layer : constLayers )
636 : : {
637 : 0 : if ( QgsVectorLayer *vl = qobject_cast<QgsVectorLayer *>( layer ) )
638 : 0 : layers << LayerConfig( vl, _snappingTypeToPointLocatorType( mSnappingConfig.typeFlag() ), mSnappingConfig.tolerance(), mSnappingConfig.units() );
639 : : }
640 : 0 : }
641 : 0 : else if ( mSnappingConfig.mode() == QgsSnappingConfig::AdvancedConfiguration )
642 : : {
643 : 0 : layers = mLayers;
644 : 0 : }
645 : :
646 : 0 : const auto constLayers = layers;
647 : 0 : for ( const LayerConfig &layer : constLayers )
648 : : {
649 : 0 : msg += QString( "layer : %1\n"
650 : : "config: %2 tolerance %3 %4\n" )
651 : 0 : .arg( layer.layer->name() )
652 : 0 : .arg( layer.type ).arg( layer.tolerance ).arg( layer.unit );
653 : :
654 : 0 : if ( mStrategy == IndexAlwaysFull || mStrategy == IndexHybrid || mStrategy == IndexExtent )
655 : : {
656 : 0 : if ( QgsPointLocator *loc = locatorForLayer( layer.layer ) )
657 : : {
658 : 0 : QString extentStr, cachedGeoms, limit( QStringLiteral( "no max area" ) );
659 : 0 : if ( const QgsRectangle *r = loc->extent() )
660 : : {
661 : 0 : extentStr = QStringLiteral( " extent %1" ).arg( r->toString() );
662 : 0 : }
663 : : else
664 : 0 : extentStr = QStringLiteral( "full extent" );
665 : 0 : if ( loc->hasIndex() )
666 : 0 : cachedGeoms = QStringLiteral( "%1 feats" ).arg( loc->cachedGeometryCount() );
667 : : else
668 : 0 : cachedGeoms = QStringLiteral( "not initialized" );
669 : 0 : if ( mStrategy == IndexHybrid )
670 : : {
671 : 0 : if ( mHybridMaxAreaPerLayer.contains( layer.layer->id() ) )
672 : : {
673 : 0 : double maxArea = mStrategy == IndexHybrid ? mHybridMaxAreaPerLayer[layer.layer->id()] : -1;
674 : 0 : if ( maxArea != -1 )
675 : 0 : limit = QStringLiteral( "max area %1" ).arg( maxArea );
676 : 0 : }
677 : : else
678 : 0 : limit = QStringLiteral( "not evaluated" );
679 : 0 : }
680 : 0 : msg += QStringLiteral( "index : YES | %1 | %2 | %3\n" ).arg( cachedGeoms, extentStr, limit );
681 : 0 : }
682 : : else
683 : 0 : msg += QLatin1String( "index : ???\n" ); // should not happen
684 : 0 : }
685 : : else
686 : 0 : msg += QLatin1String( "index : NO\n" );
687 : 0 : msg += QLatin1String( "-\n" );
688 : : }
689 : :
690 : 0 : return msg;
691 : 0 : }
692 : :
693 : 0 : QgsCoordinateReferenceSystem QgsSnappingUtils::destinationCrs() const
694 : : {
695 : 0 : return mMapSettings.destinationCrs();
696 : : }
697 : :
698 : 0 : void QgsSnappingUtils::onIndividualLayerSettingsChanged( const QHash<QgsVectorLayer *, QgsSnappingConfig::IndividualLayerSettings> &layerSettings )
699 : : {
700 : 0 : mLayers.clear();
701 : :
702 : 0 : QHash<QgsVectorLayer *, QgsSnappingConfig::IndividualLayerSettings>::const_iterator i;
703 : :
704 : 0 : for ( i = layerSettings.constBegin(); i != layerSettings.constEnd(); ++i )
705 : : {
706 : 0 : if ( i->enabled() )
707 : : {
708 : 0 : mLayers.append( LayerConfig( i.key(), _snappingTypeToPointLocatorType( static_cast<QgsSnappingConfig::SnappingTypeFlag>( i->typeFlag() ) ), i->tolerance(), i->units() ) );
709 : 0 : }
710 : 0 : }
711 : 0 : }
|