Skip to content

Commit 8acc286

Browse files
committedNov 7, 2016
[FEATURE] Snap to layer algorithm accepts a mode parameter
With a new option to prefer to snap to closest point on geometry. The old behaviour was to prefer to snap to nodes, even if a node was further from the input geometry than a segment. The new option allows you to snap geometries to the closest point, regardless of whether it's a node or segment.
1 parent 983fe24 commit 8acc286

File tree

11 files changed

+369
-102
lines changed

11 files changed

+369
-102
lines changed
 

‎python/analysis/vector/qgsgeometrysnapper.sip

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,26 +15,33 @@ class QgsGeometrySnapper : QObject
1515

1616
public:
1717

18+
//! Snapping modes
19+
enum SnapMode
20+
{
21+
PreferNodes, //!< Prefer to snap to nodes, even when a segment may be closer than a node
22+
PreferClosest, //!< Snap to closest point, regardless of it is a node or a segment
23+
};
24+
1825
/**
1926
* Constructor for QgsGeometrySnapper. A reference layer which contains geometries to snap to must be
20-
* set. The snap tolerance is specified in the layer units for the
21-
* reference layer, and it is assumed that all geometries snapped using this object will have the
27+
* set. It is assumed that all geometries snapped using this object will have the
2228
* same CRS as the reference layer (ie, no reprojection is performed).
2329
*/
24-
QgsGeometrySnapper( QgsVectorLayer* referenceLayer, double snapTolerance );
30+
QgsGeometrySnapper( QgsVectorLayer* referenceLayer );
2531

2632
/**
2733
* Snaps a geometry to the reference layer and returns the result. The geometry must be in the same
28-
* CRS as the reference layer.
34+
* CRS as the reference layer, and must have the same type as the reference layer geometry. The snap tolerance
35+
* is specified in the layer units for the reference layer.
2936
*/
30-
QgsGeometry snapGeometry( const QgsGeometry& geometry ) const;
37+
QgsGeometry snapGeometry( const QgsGeometry& geometry, double snapTolerance, SnapMode mode = PreferNodes ) const;
3138

3239
/**
3340
* Snaps a set of features to the reference layer and returns the result. This operation is
3441
* multithreaded for performance. The featureSnapped() signal will be emitted each time a feature
35-
* is processed. This method is not safe to call from multiple threads concurrently.
42+
* is processed. The snap tolerance is specified in the layer units for the reference layer.
3643
*/
37-
QgsFeatureList snapFeatures( const QgsFeatureList& features );
44+
QgsFeatureList snapFeatures( const QgsFeatureList& features, double snapTolerance, SnapMode mode = PreferNodes );
3845

3946
signals:
4047

‎python/plugins/processing/algs/qgis/SnapGeometries.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
from qgis.core import QgsFeature
3131

3232
from processing.core.GeoAlgorithm import GeoAlgorithm
33-
from processing.core.parameters import ParameterVector, ParameterNumber
33+
from processing.core.parameters import ParameterVector, ParameterNumber, ParameterSelection
3434
from processing.core.outputs import OutputVector
3535
from processing.tools import dataobjects, vector
3636

@@ -41,6 +41,7 @@ class SnapGeometriesToLayer(GeoAlgorithm):
4141
REFERENCE_LAYER = 'REFERENCE_LAYER'
4242
TOLERANCE = 'TOLERANCE'
4343
OUTPUT = 'OUTPUT'
44+
BEHAVIOUR = 'BEHAVIOUR'
4445

4546
def defineCharacteristics(self):
4647
self.name, self.i18n_name = self.trAlgorithm('Snap geometries to layer')
@@ -49,12 +50,20 @@ def defineCharacteristics(self):
4950
self.addParameter(ParameterVector(self.INPUT, self.tr('Input layer')))
5051
self.addParameter(ParameterVector(self.REFERENCE_LAYER, self.tr('Reference layer')))
5152
self.addParameter(ParameterNumber(self.TOLERANCE, self.tr('Tolerance (layer units)'), 0.00000001, 9999999999, default=10.0))
53+
54+
self.modes = [self.tr('Prefer aligning nodes'),
55+
self.tr('Prefer closest point')]
56+
self.addParameter(ParameterSelection(
57+
self.BEHAVIOUR,
58+
self.tr('Behaviour'),
59+
self.modes, default=0))
5260
self.addOutput(OutputVector(self.OUTPUT, self.tr('Snapped geometries')))
5361

5462
def processAlgorithm(self, progress):
5563
layer = dataobjects.getObjectFromUri(self.getParameterValue(self.INPUT))
5664
reference_layer = dataobjects.getObjectFromUri(self.getParameterValue(self.REFERENCE_LAYER))
5765
tolerance = self.getParameterValue(self.TOLERANCE)
66+
mode = self.getParameterValue(self.BEHAVIOUR)
5867

5968
writer = self.getOutputFromName(self.OUTPUT).getVectorWriter(
6069
layer.fields(), layer.wkbType(), layer.crs())
@@ -65,9 +74,9 @@ def processAlgorithm(self, progress):
6574
self.progress = progress
6675
self.total = 100.0 / len(features)
6776

68-
snapper = QgsGeometrySnapper(reference_layer, tolerance)
77+
snapper = QgsGeometrySnapper(reference_layer)
6978
snapper.featureSnapped.connect(self.featureSnapped)
70-
snapped_features = snapper.snapFeatures(features)
79+
snapped_features = snapper.snapFeatures(features, tolerance, mode)
7180
for f in snapped_features:
7281
writer.addFeature(QgsFeature(f))
7382

‎python/plugins/processing/tests/testdata/expected/snap_lines_to_lines.gml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@
77
<gml:boundedBy>
88
<gml:Box>
99
<gml:coord><gml:X>-1</gml:X><gml:Y>-5</gml:Y></gml:coord>
10-
<gml:coord><gml:X>11.34678899082569</gml:X><gml:Y>5.288990825688074</gml:Y></gml:coord>
10+
<gml:coord><gml:X>11</gml:X><gml:Y>5</gml:Y></gml:coord>
1111
</gml:Box>
1212
</gml:boundedBy>
13-
13+
1414
<gml:featureMember>
1515
<ogr:snap_lines_to_lines fid="lines.0">
16-
<ogr:geometryProperty><gml:LineString srsName="EPSG:4326"><gml:coordinates>6,2 9,2 9,3 11,5 11.346788990825686,5.288990825688074</gml:coordinates></gml:LineString></ogr:geometryProperty>
16+
<ogr:geometryProperty><gml:LineString srsName="EPSG:4326"><gml:coordinates>6,2 9,2 9,3 11,5</gml:coordinates></gml:LineString></ogr:geometryProperty>
1717
</ogr:snap_lines_to_lines>
1818
</gml:featureMember>
1919
<gml:featureMember>
@@ -23,7 +23,7 @@
2323
</gml:featureMember>
2424
<gml:featureMember>
2525
<ogr:snap_lines_to_lines fid="lines.2">
26-
<ogr:geometryProperty><gml:LineString srsName="EPSG:4326"><gml:coordinates>2,0 2,2 2,2 3,2 3,2</gml:coordinates></gml:LineString></ogr:geometryProperty>
26+
<ogr:geometryProperty><gml:LineString srsName="EPSG:4326"><gml:coordinates>2,0 2,2 2,2 3,2</gml:coordinates></gml:LineString></ogr:geometryProperty>
2727
</ogr:snap_lines_to_lines>
2828
</gml:featureMember>
2929
<gml:featureMember>
@@ -38,7 +38,7 @@
3838
</gml:featureMember>
3939
<gml:featureMember>
4040
<ogr:snap_lines_to_lines fid="lines.5">
41-
<ogr:geometryProperty><gml:LineString srsName="EPSG:4326"><gml:coordinates>6,-3 10,1 10.208073394495411,0.849724770642202</gml:coordinates></gml:LineString></ogr:geometryProperty>
41+
<ogr:geometryProperty><gml:LineString srsName="EPSG:4326"><gml:coordinates>6,-3 10,1</gml:coordinates></gml:LineString></ogr:geometryProperty>
4242
</ogr:snap_lines_to_lines>
4343
</gml:featureMember>
4444
<gml:featureMember>
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<GMLFeatureClassList>
2+
<GMLFeatureClass>
3+
<Name>snap_point_to_lines_prefer_closest</Name>
4+
<ElementPath>snap_point_to_lines_prefer_closest</ElementPath>
5+
<!--POINT-->
6+
<GeometryType>1</GeometryType>
7+
<SRSName>EPSG:4326</SRSName>
8+
<DatasetSpecificInfo>
9+
<FeatureCount>9</FeatureCount>
10+
<ExtentXMin>0.20114</ExtentXMin>
11+
<ExtentXMax>7.97127</ExtentXMax>
12+
<ExtentYMin>-4.82759</ExtentYMin>
13+
<ExtentYMax>2.74139</ExtentYMax>
14+
</DatasetSpecificInfo>
15+
</GMLFeatureClass>
16+
</GMLFeatureClassList>
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?xml version="1.0" encoding="utf-8" ?>
2+
<ogr:FeatureCollection
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation=""
5+
xmlns:ogr="http://ogr.maptools.org/"
6+
xmlns:gml="http://www.opengis.net/gml">
7+
<gml:boundedBy>
8+
<gml:Box>
9+
<gml:coord><gml:X>0.201140250855188</gml:X><gml:Y>-4.827594070695552</gml:Y></gml:coord>
10+
<gml:coord><gml:X>7.971265678449257</gml:X><gml:Y>2.74139110604333</gml:Y></gml:coord>
11+
</gml:Box>
12+
</gml:boundedBy>
13+
14+
<gml:featureMember>
15+
<ogr:snap_point_to_lines_prefer_closest fid="points.0">
16+
<ogr:geometryProperty><gml:Point srsName="EPSG:4326"><gml:coordinates>1,1</gml:coordinates></gml:Point></ogr:geometryProperty>
17+
</ogr:snap_point_to_lines_prefer_closest>
18+
</gml:featureMember>
19+
<gml:featureMember>
20+
<ogr:snap_point_to_lines_prefer_closest fid="points.1">
21+
<ogr:geometryProperty><gml:Point srsName="EPSG:4326"><gml:coordinates>3.0,2.74139110604333</gml:coordinates></gml:Point></ogr:geometryProperty>
22+
</ogr:snap_point_to_lines_prefer_closest>
23+
</gml:featureMember>
24+
<gml:featureMember>
25+
<ogr:snap_point_to_lines_prefer_closest fid="points.2">
26+
<ogr:geometryProperty><gml:Point srsName="EPSG:4326"><gml:coordinates>2,2</gml:coordinates></gml:Point></ogr:geometryProperty>
27+
</ogr:snap_point_to_lines_prefer_closest>
28+
</gml:featureMember>
29+
<gml:featureMember>
30+
<ogr:snap_point_to_lines_prefer_closest fid="points.3">
31+
<ogr:geometryProperty><gml:Point srsName="EPSG:4326"><gml:coordinates>5,1</gml:coordinates></gml:Point></ogr:geometryProperty>
32+
</ogr:snap_point_to_lines_prefer_closest>
33+
</gml:featureMember>
34+
<gml:featureMember>
35+
<ogr:snap_point_to_lines_prefer_closest fid="points.4">
36+
<ogr:geometryProperty><gml:Point srsName="EPSG:4326"><gml:coordinates>3.683922462941847,1.0</gml:coordinates></gml:Point></ogr:geometryProperty>
37+
</ogr:snap_point_to_lines_prefer_closest>
38+
</gml:featureMember>
39+
<gml:featureMember>
40+
<ogr:snap_point_to_lines_prefer_closest fid="points.5">
41+
<ogr:geometryProperty><gml:Point srsName="EPSG:4326"><gml:coordinates>0.201140250855188,-4.827594070695552</gml:coordinates></gml:Point></ogr:geometryProperty>
42+
</ogr:snap_point_to_lines_prefer_closest>
43+
</gml:featureMember>
44+
<gml:featureMember>
45+
<ogr:snap_point_to_lines_prefer_closest fid="points.6">
46+
<ogr:geometryProperty><gml:Point srsName="EPSG:4326"><gml:coordinates>7.971265678449257,0.609122006841505</gml:coordinates></gml:Point></ogr:geometryProperty>
47+
</ogr:snap_point_to_lines_prefer_closest>
48+
</gml:featureMember>
49+
<gml:featureMember>
50+
<ogr:snap_point_to_lines_prefer_closest fid="points.7">
51+
<ogr:geometryProperty><gml:Point srsName="EPSG:4326"><gml:coordinates>7.456898517673889,-1.543101482326111</gml:coordinates></gml:Point></ogr:geometryProperty>
52+
</ogr:snap_point_to_lines_prefer_closest>
53+
</gml:featureMember>
54+
<gml:featureMember>
55+
<ogr:snap_point_to_lines_prefer_closest fid="points.8">
56+
<ogr:geometryProperty><gml:Point srsName="EPSG:4326"><gml:coordinates>0.220296465222349,-1.0</gml:coordinates></gml:Point></ogr:geometryProperty>
57+
</ogr:snap_point_to_lines_prefer_closest>
58+
</gml:featureMember>
59+
</ogr:FeatureCollection>
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<GMLFeatureClassList>
2+
<GMLFeatureClass>
3+
<Name>snap_point_to_lines_prefer_nodes</Name>
4+
<ElementPath>snap_point_to_lines_prefer_nodes</ElementPath>
5+
<!--POINT-->
6+
<GeometryType>1</GeometryType>
7+
<SRSName>EPSG:4326</SRSName>
8+
<DatasetSpecificInfo>
9+
<FeatureCount>9</FeatureCount>
10+
<ExtentXMin>0.20114</ExtentXMin>
11+
<ExtentXMax>7.97127</ExtentXMax>
12+
<ExtentYMin>-4.82759</ExtentYMin>
13+
<ExtentYMax>3.00000</ExtentYMax>
14+
</DatasetSpecificInfo>
15+
</GMLFeatureClass>
16+
</GMLFeatureClassList>
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?xml version="1.0" encoding="utf-8" ?>
2+
<ogr:FeatureCollection
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation=""
5+
xmlns:ogr="http://ogr.maptools.org/"
6+
xmlns:gml="http://www.opengis.net/gml">
7+
<gml:boundedBy>
8+
<gml:Box>
9+
<gml:coord><gml:X>0.201140250855188</gml:X><gml:Y>-4.827594070695552</gml:Y></gml:coord>
10+
<gml:coord><gml:X>7.971265678449257</gml:X><gml:Y>3</gml:Y></gml:coord>
11+
</gml:Box>
12+
</gml:boundedBy>
13+
14+
<gml:featureMember>
15+
<ogr:snap_point_to_lines_prefer_nodes fid="points.0">
16+
<ogr:geometryProperty><gml:Point srsName="EPSG:4326"><gml:coordinates>1,1</gml:coordinates></gml:Point></ogr:geometryProperty>
17+
</ogr:snap_point_to_lines_prefer_nodes>
18+
</gml:featureMember>
19+
<gml:featureMember>
20+
<ogr:snap_point_to_lines_prefer_nodes fid="points.1">
21+
<ogr:geometryProperty><gml:Point srsName="EPSG:4326"><gml:coordinates>3,3</gml:coordinates></gml:Point></ogr:geometryProperty>
22+
</ogr:snap_point_to_lines_prefer_nodes>
23+
</gml:featureMember>
24+
<gml:featureMember>
25+
<ogr:snap_point_to_lines_prefer_nodes fid="points.2">
26+
<ogr:geometryProperty><gml:Point srsName="EPSG:4326"><gml:coordinates>2,2</gml:coordinates></gml:Point></ogr:geometryProperty>
27+
</ogr:snap_point_to_lines_prefer_nodes>
28+
</gml:featureMember>
29+
<gml:featureMember>
30+
<ogr:snap_point_to_lines_prefer_nodes fid="points.3">
31+
<ogr:geometryProperty><gml:Point srsName="EPSG:4326"><gml:coordinates>5,1</gml:coordinates></gml:Point></ogr:geometryProperty>
32+
</ogr:snap_point_to_lines_prefer_nodes>
33+
</gml:featureMember>
34+
<gml:featureMember>
35+
<ogr:snap_point_to_lines_prefer_nodes fid="points.4">
36+
<ogr:geometryProperty><gml:Point srsName="EPSG:4326"><gml:coordinates>3,1</gml:coordinates></gml:Point></ogr:geometryProperty>
37+
</ogr:snap_point_to_lines_prefer_nodes>
38+
</gml:featureMember>
39+
<gml:featureMember>
40+
<ogr:snap_point_to_lines_prefer_nodes fid="points.5">
41+
<ogr:geometryProperty><gml:Point srsName="EPSG:4326"><gml:coordinates>0.201140250855188,-4.827594070695552</gml:coordinates></gml:Point></ogr:geometryProperty>
42+
</ogr:snap_point_to_lines_prefer_nodes>
43+
</gml:featureMember>
44+
<gml:featureMember>
45+
<ogr:snap_point_to_lines_prefer_nodes fid="points.6">
46+
<ogr:geometryProperty><gml:Point srsName="EPSG:4326"><gml:coordinates>7.971265678449257,0.609122006841505</gml:coordinates></gml:Point></ogr:geometryProperty>
47+
</ogr:snap_point_to_lines_prefer_nodes>
48+
</gml:featureMember>
49+
<gml:featureMember>
50+
<ogr:snap_point_to_lines_prefer_nodes fid="points.7">
51+
<ogr:geometryProperty><gml:Point srsName="EPSG:4326"><gml:coordinates>7.456898517673889,-1.543101482326111</gml:coordinates></gml:Point></ogr:geometryProperty>
52+
</ogr:snap_point_to_lines_prefer_nodes>
53+
</gml:featureMember>
54+
<gml:featureMember>
55+
<ogr:snap_point_to_lines_prefer_nodes fid="points.8">
56+
<ogr:geometryProperty><gml:Point srsName="EPSG:4326"><gml:coordinates>1,-1</gml:coordinates></gml:Point></ogr:geometryProperty>
57+
</ogr:snap_point_to_lines_prefer_nodes>
58+
</gml:featureMember>
59+
</ogr:FeatureCollection>

‎python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1431,4 +1431,36 @@ tests:
14311431
results:
14321432
OUTPUT:
14331433
name: expected/snap_points_to_points.gml
1434-
type: vector
1434+
type: vector
1435+
1436+
- algorithm: qgis:snapgeometriestolayer
1437+
name: Snap points to lines (prefer nodes)
1438+
params:
1439+
BEHAVIOUR: '0'
1440+
INPUT:
1441+
name: snap_points.gml
1442+
type: vector
1443+
REFERENCE_LAYER:
1444+
name: lines.gml
1445+
type: vector
1446+
TOLERANCE: 1.0
1447+
results:
1448+
OUTPUT:
1449+
name: expected/snap_point_to_lines_prefer_nodes.gml
1450+
type: vector
1451+
1452+
- algorithm: qgis:snapgeometriestolayer
1453+
name: Snap points to lines (prefer closest)
1454+
params:
1455+
BEHAVIOUR: '1'
1456+
INPUT:
1457+
name: snap_points.gml
1458+
type: vector
1459+
REFERENCE_LAYER:
1460+
name: lines.gml
1461+
type: vector
1462+
TOLERANCE: 1.0
1463+
results:
1464+
OUTPUT:
1465+
name: expected/snap_point_to_lines_prefer_closest.gml
1466+
type: vector

‎src/analysis/vector/qgsgeometrysnapper.cpp

Lines changed: 60 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,8 @@ void QgsSnapIndex::addGeometry( const QgsAbstractGeometry* geom )
347347
mCoordIdxs.append( idx );
348348
mCoordIdxs.append( idx1 );
349349
addPoint( idx );
350-
addSegment( idx, idx1 );
350+
if ( iVert < nVerts - 1 )
351+
addSegment( idx, idx1 );
351352
}
352353
}
353354
}
@@ -456,30 +457,29 @@ QgsSnapIndex::SnapItem* QgsSnapIndex::getSnapItem( const QgsPointV2& pos, double
456457

457458

458459

459-
QgsGeometrySnapper::QgsGeometrySnapper( QgsVectorLayer *referenceLayer, double snapTolerance )
460+
QgsGeometrySnapper::QgsGeometrySnapper( QgsVectorLayer *referenceLayer )
460461
: mReferenceLayer( referenceLayer )
461-
, mSnapTolerance( snapTolerance )
462462
{
463463
// Build spatial index
464464
QgsFeatureRequest req;
465465
req.setSubsetOfAttributes( QgsAttributeList() );
466466
mIndex = QgsSpatialIndex( mReferenceLayer->getFeatures( req ) );
467467
}
468468

469-
QgsFeatureList QgsGeometrySnapper::snapFeatures( const QgsFeatureList& features )
469+
QgsFeatureList QgsGeometrySnapper::snapFeatures( const QgsFeatureList& features, double snapTolerance, SnapMode mode )
470470
{
471471
QgsFeatureList list = features;
472-
QtConcurrent::blockingMap( list, ProcessFeatureWrapper( this ) );
472+
QtConcurrent::blockingMap( list, ProcessFeatureWrapper( this, snapTolerance, mode ) );
473473
return list;
474474
}
475475

476-
void QgsGeometrySnapper::processFeature( QgsFeature& feature )
476+
void QgsGeometrySnapper::processFeature( QgsFeature& feature, double snapTolerance, SnapMode mode )
477477
{
478478
if ( !feature.geometry().isEmpty() )
479-
feature.setGeometry( snapGeometry( feature.geometry() ) );
479+
feature.setGeometry( snapGeometry( feature.geometry(), snapTolerance, mode ) );
480480
}
481481

482-
QgsGeometry QgsGeometrySnapper::snapGeometry( const QgsGeometry& geometry ) const
482+
QgsGeometry QgsGeometrySnapper::snapGeometry( const QgsGeometry& geometry, double snapTolerance, SnapMode mode ) const
483483
{
484484
QgsPointV2 center = dynamic_cast< const QgsPointV2* >( geometry.geometry() ) ? *dynamic_cast< const QgsPointV2* >( geometry.geometry() ) :
485485
QgsPointV2( geometry.geometry()->boundingBox().center() );
@@ -488,7 +488,7 @@ QgsGeometry QgsGeometrySnapper::snapGeometry( const QgsGeometry& geometry ) cons
488488
QList<QgsGeometry> refGeometries;
489489
mIndexMutex.lock();
490490
QgsRectangle searchBounds = geometry.boundingBox();
491-
searchBounds.grow( mSnapTolerance );
491+
searchBounds.grow( snapTolerance );
492492
QgsFeatureIds refFeatureIds = mIndex.intersects( searchBounds ).toSet();
493493
mIndexMutex.unlock();
494494

@@ -504,7 +504,7 @@ QgsGeometry QgsGeometrySnapper::snapGeometry( const QgsGeometry& geometry ) cons
504504
mReferenceLayerMutex.unlock();
505505

506506

507-
QgsSnapIndex refSnapIndex( center, 10 * mSnapTolerance );
507+
QgsSnapIndex refSnapIndex( center, 10 * snapTolerance );
508508
Q_FOREACH ( const QgsGeometry& geom, refGeometries )
509509
{
510510
refSnapIndex.addGeometry( geom.geometry() );
@@ -530,22 +530,57 @@ QgsGeometry QgsGeometrySnapper::snapGeometry( const QgsGeometry& geometry ) cons
530530
QgsSnapIndex::SegmentSnapItem* snapSegment = nullptr;
531531
QgsVertexId vidx( iPart, iRing, iVert );
532532
QgsPointV2 p = subjGeom->vertexAt( vidx );
533-
if ( !refSnapIndex.getSnapItem( p, mSnapTolerance, &snapPoint, &snapSegment ) )
533+
if ( !refSnapIndex.getSnapItem( p, snapTolerance, &snapPoint, &snapSegment ) )
534534
{
535535
subjPointFlags[iPart][iRing].append( Unsnapped );
536536
}
537537
else
538538
{
539-
// Prefer snapping to point
540-
if ( snapPoint )
539+
switch ( mode )
541540
{
542-
subjGeom->moveVertex( vidx, snapPoint->getSnapPoint( p ) );
543-
subjPointFlags[iPart][iRing].append( SnappedToRefNode );
544-
}
545-
else if ( snapSegment )
546-
{
547-
subjGeom->moveVertex( vidx, snapSegment->getSnapPoint( p ) );
548-
subjPointFlags[iPart][iRing].append( SnappedToRefSegment );
541+
case PreferNodes:
542+
{
543+
// Prefer snapping to point
544+
if ( snapPoint )
545+
{
546+
subjGeom->moveVertex( vidx, snapPoint->getSnapPoint( p ) );
547+
subjPointFlags[iPart][iRing].append( SnappedToRefNode );
548+
}
549+
else if ( snapSegment )
550+
{
551+
subjGeom->moveVertex( vidx, snapSegment->getSnapPoint( p ) );
552+
subjPointFlags[iPart][iRing].append( SnappedToRefSegment );
553+
}
554+
break;
555+
}
556+
557+
case PreferClosest:
558+
{
559+
QgsPointV2 nodeSnap, segmentSnap;
560+
double distanceNode = DBL_MAX;
561+
double distanceSegment = DBL_MAX;
562+
if ( snapPoint )
563+
{
564+
nodeSnap = snapPoint->getSnapPoint( p );
565+
distanceNode = nodeSnap.distanceSquared( p );
566+
}
567+
if ( snapSegment )
568+
{
569+
segmentSnap = snapSegment->getSnapPoint( p );
570+
distanceSegment = segmentSnap.distanceSquared( p );
571+
}
572+
if ( snapPoint && distanceNode < distanceSegment )
573+
{
574+
subjGeom->moveVertex( vidx, nodeSnap );
575+
subjPointFlags[iPart][iRing].append( SnappedToRefNode );
576+
}
577+
else if ( snapSegment )
578+
{
579+
subjGeom->moveVertex( vidx, segmentSnap );
580+
subjPointFlags[iPart][iRing].append( SnappedToRefSegment );
581+
}
582+
break;
583+
}
549584
}
550585
}
551586
}
@@ -557,11 +592,11 @@ QgsGeometry QgsGeometrySnapper::snapGeometry( const QgsGeometry& geometry ) cons
557592
return QgsGeometry( subjGeom );
558593

559594
// SnapIndex for subject feature
560-
QgsSnapIndex* subjSnapIndex = new QgsSnapIndex( center, 10 * mSnapTolerance );
595+
QgsSnapIndex* subjSnapIndex = new QgsSnapIndex( center, 10 * snapTolerance );
561596
subjSnapIndex->addGeometry( subjGeom );
562597

563598
QgsAbstractGeometry* origSubjGeom = subjGeom->clone();
564-
QgsSnapIndex* origSubjSnapIndex = new QgsSnapIndex( center, 10 * mSnapTolerance );
599+
QgsSnapIndex* origSubjSnapIndex = new QgsSnapIndex( center, 10 * snapTolerance );
565600
origSubjSnapIndex->addGeometry( origSubjGeom );
566601

567602
// Pass 2: add missing vertices to subject geometry
@@ -577,7 +612,7 @@ QgsGeometry QgsGeometrySnapper::snapGeometry( const QgsGeometry& geometry ) cons
577612
QgsSnapIndex::PointSnapItem* snapPoint = nullptr;
578613
QgsSnapIndex::SegmentSnapItem* snapSegment = nullptr;
579614
QgsPointV2 point = refGeom.geometry()->vertexAt( QgsVertexId( iPart, iRing, iVert ) );
580-
if ( subjSnapIndex->getSnapItem( point, mSnapTolerance, &snapPoint, &snapSegment ) )
615+
if ( subjSnapIndex->getSnapItem( point, snapTolerance, &snapPoint, &snapSegment ) )
581616
{
582617
// Snap to segment, unless a subject point was already snapped to the reference point
583618
if ( snapPoint && QgsGeometryUtils::sqrDistance2D( snapPoint->getSnapPoint( point ), point ) < 1E-16 )
@@ -595,7 +630,7 @@ QgsGeometry QgsGeometrySnapper::snapGeometry( const QgsGeometry& geometry ) cons
595630
}
596631

597632
// If we are too far away from the original geometry, do nothing
598-
if ( !origSubjSnapIndex->getSnapItem( point, mSnapTolerance ) )
633+
if ( !origSubjSnapIndex->getSnapItem( point, snapTolerance ) )
599634
{
600635
continue;
601636
}
@@ -604,7 +639,7 @@ QgsGeometry QgsGeometrySnapper::snapGeometry( const QgsGeometry& geometry ) cons
604639
subjGeom->insertVertex( QgsVertexId( idx->vidx.part, idx->vidx.ring, idx->vidx.vertex + 1 ), point );
605640
subjPointFlags[idx->vidx.part][idx->vidx.ring].insert( idx->vidx.vertex + 1, SnappedToRefNode );
606641
delete subjSnapIndex;
607-
subjSnapIndex = new QgsSnapIndex( center, 10 * mSnapTolerance );
642+
subjSnapIndex = new QgsSnapIndex( center, 10 * snapTolerance );
608643
subjSnapIndex->addGeometry( subjGeom );
609644
}
610645
}

‎src/analysis/vector/qgsgeometrysnapper.h

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -40,26 +40,33 @@ class ANALYSIS_EXPORT QgsGeometrySnapper : public QObject
4040

4141
public:
4242

43+
//! Snapping modes
44+
enum SnapMode
45+
{
46+
PreferNodes = 0, //!< Prefer to snap to nodes, even when a segment may be closer than a node
47+
PreferClosest, //!< Snap to closest point, regardless of it is a node or a segment
48+
};
49+
4350
/**
4451
* Constructor for QgsGeometrySnapper. A reference layer which contains geometries to snap to must be
45-
* set. The snap tolerance is specified in the layer units for the
46-
* reference layer, and it is assumed that all geometries snapped using this object will have the
52+
* set. It is assumed that all geometries snapped using this object will have the
4753
* same CRS as the reference layer (ie, no reprojection is performed).
4854
*/
49-
QgsGeometrySnapper( QgsVectorLayer* referenceLayer, double snapTolerance );
55+
QgsGeometrySnapper( QgsVectorLayer* referenceLayer );
5056

5157
/**
5258
* Snaps a geometry to the reference layer and returns the result. The geometry must be in the same
53-
* CRS as the reference layer, and must have the same type as the reference layer geometry.
59+
* CRS as the reference layer, and must have the same type as the reference layer geometry. The snap tolerance
60+
* is specified in the layer units for the reference layer.
5461
*/
55-
QgsGeometry snapGeometry( const QgsGeometry& geometry ) const;
62+
QgsGeometry snapGeometry( const QgsGeometry& geometry, double snapTolerance, SnapMode mode = PreferNodes ) const;
5663

5764
/**
5865
* Snaps a set of features to the reference layer and returns the result. This operation is
5966
* multithreaded for performance. The featureSnapped() signal will be emitted each time a feature
60-
* is processed. This method is not safe to call from multiple threads concurrently.
67+
* is processed. The snap tolerance is specified in the layer units for the reference layer.
6168
*/
62-
QgsFeatureList snapFeatures( const QgsFeatureList& features );
69+
QgsFeatureList snapFeatures( const QgsFeatureList& features, double snapTolerance, SnapMode mode = PreferNodes );
6370

6471
signals:
6572

@@ -70,22 +77,26 @@ class ANALYSIS_EXPORT QgsGeometrySnapper : public QObject
7077
struct ProcessFeatureWrapper
7178
{
7279
QgsGeometrySnapper* instance;
73-
explicit ProcessFeatureWrapper( QgsGeometrySnapper* _instance ) : instance( _instance ) {}
74-
void operator()( QgsFeature& feature ) { return instance->processFeature( feature ); }
80+
double snapTolerance;
81+
SnapMode mode;
82+
explicit ProcessFeatureWrapper( QgsGeometrySnapper* _instance, double snapTolerance, SnapMode mode )
83+
: instance( _instance )
84+
, snapTolerance( snapTolerance )
85+
, mode( mode )
86+
{}
87+
void operator()( QgsFeature& feature ) { return instance->processFeature( feature, snapTolerance, mode ); }
7588
};
7689

7790
enum PointFlag { SnappedToRefNode, SnappedToRefSegment, Unsnapped };
7891

7992
QgsVectorLayer* mReferenceLayer;
8093
QgsFeatureList mInputFeatures;
8194

82-
double mSnapTolerance;
83-
8495
QgsSpatialIndex mIndex;
8596
mutable QMutex mIndexMutex;
8697
mutable QMutex mReferenceLayerMutex;
8798

88-
void processFeature( QgsFeature& feature );
99+
void processFeature( QgsFeature& feature, double snapTolerance, SnapMode mode );
89100

90101
int polyLineSize( const QgsAbstractGeometry* geom, int iPart, int iRing ) const;
91102
};

‎tests/src/analysis/testqgsgeometrysnapper.cpp

Lines changed: 72 additions & 49 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)
Please sign in to comment.