Skip to content
Permalink
Browse files
[FEATURE] Add geometry snapper modes to only snap end points of lines
Allows snapping of end points only, or end point to end point only

Also update processing algorithm to match
  • Loading branch information
nyalldawson committed Mar 30, 2017
1 parent 92249c1 commit 636e9c5ea3c5a2656867159c35d0d801c96664fd
@@ -18,8 +18,11 @@ class QgsGeometrySnapper : QObject
//! Snapping modes
enum SnapMode
{
PreferNodes, //!< Prefer to snap to nodes, even when a segment may be closer than a node
PreferClosest, //!< Snap to closest point, regardless of it is a node or a segment
PreferNodes,
PreferClosest,
EndPointPreferNodes,
EndPointPreferClosest,
EndPointToEndPoint,
};

/**
@@ -52,7 +52,10 @@ def defineCharacteristics(self):
self.addParameter(ParameterNumber(self.TOLERANCE, self.tr('Tolerance (layer units)'), 0.00000001, 9999999999, default=10.0))

self.modes = [self.tr('Prefer aligning nodes'),
self.tr('Prefer closest point')]
self.tr('Prefer closest point'),
self.tr('Move end points only, prefer aligning nodes'),
self.tr('Move end points only, prefer closest point'),
self.tr('Snap end points to end points only')]
self.addParameter(ParameterSelection(
self.BEHAVIOR,
self.tr('Behavior'),
@@ -29,8 +29,8 @@

///@cond PRIVATE

QgsSnapIndex::PointSnapItem::PointSnapItem( const QgsSnapIndex::CoordIdx *_idx )
: SnapItem( QgsSnapIndex::SnapPoint )
QgsSnapIndex::PointSnapItem::PointSnapItem( const QgsSnapIndex::CoordIdx *_idx, bool isEndPoint )
: SnapItem( isEndPoint ? QgsSnapIndex::SnapEndPoint : QgsSnapIndex::SnapPoint )
, idx( _idx )
{}

@@ -299,12 +299,12 @@ QgsSnapIndex::Cell &QgsSnapIndex::getCreateCell( int col, int row )
}
}

void QgsSnapIndex::addPoint( const CoordIdx *idx )
void QgsSnapIndex::addPoint( const CoordIdx *idx, bool isEndPoint )
{
QgsPointV2 p = idx->point();
int col = qFloor( ( p.x() - mOrigin.x() ) / mCellSize );
int row = qFloor( ( p.y() - mOrigin.y() ) / mCellSize );
getCreateCell( col, row ).append( new PointSnapItem( idx ) );
getCreateCell( col, row ).append( new PointSnapItem( idx, isEndPoint ) );
}

void QgsSnapIndex::addSegment( const CoordIdx *idxFrom, const CoordIdx *idxTo )
@@ -346,7 +346,7 @@ void QgsSnapIndex::addGeometry( const QgsAbstractGeometry *geom )
CoordIdx *idx1 = new CoordIdx( geom, QgsVertexId( iPart, iRing, iVert + 1 ) );
mCoordIdxs.append( idx );
mCoordIdxs.append( idx1 );
addPoint( idx );
addPoint( idx, iVert == 0 || iVert == nVerts - 1 );
if ( iVert < nVerts - 1 )
addSegment( idx, idx1 );
}
@@ -398,7 +398,7 @@ QgsPointV2 QgsSnapIndex::getClosestSnapToPoint( const QgsPointV2 &p, const QgsPo
return pMin;
}

QgsSnapIndex::SnapItem *QgsSnapIndex::getSnapItem( const QgsPointV2 &pos, double tol, QgsSnapIndex::PointSnapItem **pSnapPoint, QgsSnapIndex::SegmentSnapItem **pSnapSegment ) const
QgsSnapIndex::SnapItem *QgsSnapIndex::getSnapItem( const QgsPointV2 &pos, double tol, QgsSnapIndex::PointSnapItem **pSnapPoint, QgsSnapIndex::SegmentSnapItem **pSnapSegment, bool endPointOnly ) const
{
int colStart = qFloor( ( pos.x() - tol - mOrigin.x() ) / mCellSize );
int rowStart = qFloor( ( pos.y() - tol - mOrigin.y() ) / mCellSize );
@@ -421,7 +421,7 @@ QgsSnapIndex::SnapItem *QgsSnapIndex::getSnapItem( const QgsPointV2 &pos, double

Q_FOREACH ( QgsSnapIndex::SnapItem *item, items )
{
if ( item->type == SnapPoint )
if ( ( ! endPointOnly && item->type == SnapPoint ) || item->type == SnapEndPoint )
{
double dist = QgsGeometryUtils::sqrDistance2D( item->getSnapPoint( pos ), pos );
if ( dist < minDistPoint )
@@ -430,7 +430,7 @@ QgsSnapIndex::SnapItem *QgsSnapIndex::getSnapItem( const QgsPointV2 &pos, double
snapPoint = static_cast<PointSnapItem *>( item );
}
}
else if ( item->type == SnapSegment )
else if ( item->type == SnapSegment && !endPointOnly )
{
QgsPointV2 pProj;
if ( !static_cast<SegmentSnapItem *>( item )->getProjection( pos, pProj ) )
@@ -509,6 +509,10 @@ QgsGeometry QgsGeometrySnapper::snapGeometry( const QgsGeometry &geometry, doubl

QgsGeometry QgsGeometrySnapper::snapGeometry( const QgsGeometry &geometry, double snapTolerance, const QList<QgsGeometry> &referenceGeometries, QgsGeometrySnapper::SnapMode mode )
{
if ( QgsWkbTypes::geometryType( geometry.wkbType() ) == QgsWkbTypes::PolygonGeometry &&
( mode == EndPointPreferClosest || mode == EndPointPreferNodes || mode == EndPointToEndPoint ) )
return geometry;

QgsPointV2 center = dynamic_cast< const QgsPointV2 * >( geometry.geometry() ) ? *static_cast< const QgsPointV2 * >( geometry.geometry() ) :
QgsPointV2( geometry.geometry()->boundingBox().center() );

@@ -533,12 +537,19 @@ QgsGeometry QgsGeometrySnapper::snapGeometry( const QgsGeometry &geometry, doubl

for ( int iVert = 0, nVerts = polyLineSize( subjGeom, iPart, iRing ); iVert < nVerts; ++iVert )
{
if ( ( mode == EndPointPreferClosest || mode == EndPointPreferNodes || mode == EndPointToEndPoint ) &&
QgsWkbTypes::geometryType( subjGeom->wkbType() ) == QgsWkbTypes::LineGeometry && ( iVert > 0 && iVert < nVerts - 1 ) )
{
//endpoint mode and not at an endpoint, skip
subjPointFlags[iPart][iRing].append( Unsnapped );
continue;
}

QgsSnapIndex::PointSnapItem *snapPoint = nullptr;
QgsSnapIndex::SegmentSnapItem *snapSegment = nullptr;
QgsVertexId vidx( iPart, iRing, iVert );
QgsPointV2 p = subjGeom->vertexAt( vidx );
if ( !refSnapIndex.getSnapItem( p, snapTolerance, &snapPoint, &snapSegment ) )
if ( !refSnapIndex.getSnapItem( p, snapTolerance, &snapPoint, &snapSegment, mode == EndPointToEndPoint ) )
{
subjPointFlags[iPart][iRing].append( Unsnapped );
}
@@ -547,6 +558,8 @@ QgsGeometry QgsGeometrySnapper::snapGeometry( const QgsGeometry &geometry, doubl
switch ( mode )
{
case PreferNodes:
case EndPointPreferNodes:
case EndPointToEndPoint:
{
// Prefer snapping to point
if ( snapPoint )
@@ -563,6 +576,7 @@ QgsGeometry QgsGeometrySnapper::snapGeometry( const QgsGeometry &geometry, doubl
}

case PreferClosest:
case EndPointPreferClosest:
{
QgsPointV2 nodeSnap, segmentSnap;
double distanceNode = DBL_MAX;
@@ -598,6 +612,9 @@ QgsGeometry QgsGeometrySnapper::snapGeometry( const QgsGeometry &geometry, doubl
//nothing more to do for points
if ( dynamic_cast< const QgsPointV2 * >( subjGeom ) )
return QgsGeometry( subjGeom );
//or for end point snapping
if ( mode == EndPointPreferClosest || mode == EndPointPreferNodes || mode == EndPointToEndPoint )
return QgsGeometry( subjGeom );

// SnapIndex for subject feature
std::unique_ptr< QgsSnapIndex > subjSnapIndex( new QgsSnapIndex( center, 10 * snapTolerance ) );
@@ -47,6 +47,9 @@ class ANALYSIS_EXPORT QgsGeometrySnapper : public QObject
{
PreferNodes = 0, //!< Prefer to snap to nodes, even when a segment may be closer than a node
PreferClosest, //!< Snap to closest point, regardless of it is a node or a segment
EndPointPreferNodes, //!< Only snap start/end points of lines (point features will also be snapped, polygon features will not be modified), prefer to snap to nodes
EndPointPreferClosest, //!< Only snap start/end points of lines (point features will also be snapped, polygon features will not be modified), snap to closest point
EndPointToEndPoint, //!< Only snap the start/end points of lines to other start/end points of lines
};

/**
@@ -172,7 +175,7 @@ class QgsSnapIndex
QgsVertexId vidx;
};

enum SnapType { SnapPoint, SnapSegment };
enum SnapType { SnapPoint, SnapEndPoint, SnapSegment };

class SnapItem
{
@@ -188,7 +191,7 @@ class QgsSnapIndex
class PointSnapItem : public QgsSnapIndex::SnapItem
{
public:
explicit PointSnapItem( const CoordIdx *_idx );
explicit PointSnapItem( const CoordIdx *_idx, bool isEndPoint );
QgsPointV2 getSnapPoint( const QgsPointV2 &/*p*/ ) const override;
const CoordIdx *idx = nullptr;
};
@@ -208,7 +211,7 @@ class QgsSnapIndex
~QgsSnapIndex();
void addGeometry( const QgsAbstractGeometry *geom );
QgsPointV2 getClosestSnapToPoint( const QgsPointV2 &p, const QgsPointV2 &q );
SnapItem *getSnapItem( const QgsPointV2 &pos, double tol, PointSnapItem **pSnapPoint = nullptr, SegmentSnapItem **pSnapSegment = nullptr ) const;
SnapItem *getSnapItem( const QgsPointV2 &pos, double tol, PointSnapItem **pSnapPoint = nullptr, SegmentSnapItem **pSnapSegment = nullptr, bool endPointOnly = false ) const;

private:
typedef QList<SnapItem *> Cell;
@@ -235,7 +238,7 @@ class QgsSnapIndex
QList<GridRow> mGridRows;
int mRowsStartIdx;

void addPoint( const CoordIdx *idx );
void addPoint( const CoordIdx *idx, bool isEndPoint );
void addSegment( const CoordIdx *idxFrom, const CoordIdx *idxTo );
const Cell *getCell( int col, int row ) const;
Cell &getCreateCell( int col, int row );
@@ -45,6 +45,8 @@ class TestQgsGeometrySnapper : public QObject
void snapPointToLine();
void snapPointToLinePreferNearest();
void snapPointToPolygon();
void endPointSnap();
void endPointToEndPoint();
void internalSnapper();
};

@@ -410,6 +412,57 @@ void TestQgsGeometrySnapper::snapPointToPolygon()
QCOMPARE( result.exportToWkt(), QStringLiteral( "Point (10 0)" ) );
}

void TestQgsGeometrySnapper::endPointSnap()
{
QgsVectorLayer *rl = new QgsVectorLayer( QStringLiteral( "Linestring" ), QStringLiteral( "x" ), QStringLiteral( "memory" ) );
QgsFeature ff( 0 );

QgsGeometry refGeom = QgsGeometry::fromWkt( QStringLiteral( "LineString(0 0, 100 0, 100 100, 0 100)" ) );
ff.setGeometry( refGeom );
QgsFeatureList flist;
flist << ff;
rl->dataProvider()->addFeatures( flist );

QgsGeometry lineGeom = QgsGeometry::fromWkt( QStringLiteral( "LineString(1 -1, 102 0, 98 102, 0 101)" ) );
QgsGeometrySnapper snapper( rl );
QgsGeometry result = snapper.snapGeometry( lineGeom, 10, QgsGeometrySnapper::EndPointPreferNodes );
QCOMPARE( result.exportToWkt(), QStringLiteral( "LineString (0 0, 102 0, 98 102, 0 100)" ) );

QgsGeometry lineGeom2 = QgsGeometry::fromWkt( QStringLiteral( "LineString(50 0, 102 0, 98 102, 0 50)" ) );
result = snapper.snapGeometry( lineGeom2, 1, QgsGeometrySnapper::EndPointPreferNodes );
QCOMPARE( result.exportToWkt(), QStringLiteral( "LineString (50 0, 102 0, 98 102, 0 50)" ) );

QgsGeometry lineGeom3 = QgsGeometry::fromWkt( QStringLiteral( "LineString(50 -10, 50 -1)" ) );
result = snapper.snapGeometry( lineGeom3, 2, QgsGeometrySnapper::EndPointPreferNodes );
QCOMPARE( result.exportToWkt(), QStringLiteral( "LineString (50 -10, 50 0)" ) );
}

void TestQgsGeometrySnapper::endPointToEndPoint()
{
QgsVectorLayer *rl = new QgsVectorLayer( QStringLiteral( "Linestring" ), QStringLiteral( "x" ), QStringLiteral( "memory" ) );
QgsFeature ff( 0 );

// closed linestrings
QgsGeometry refGeom = QgsGeometry::fromWkt( QStringLiteral( "LineString(0 0, 100 0, 100 100, 0 100)" ) );
ff.setGeometry( refGeom );
QgsFeatureList flist;
flist << ff;
rl->dataProvider()->addFeatures( flist );

QgsGeometry lineGeom = QgsGeometry::fromWkt( QStringLiteral( "LineString(1 -1, 102 0, 98 102, 0 101)" ) );
QgsGeometrySnapper snapper( rl );
QgsGeometry result = snapper.snapGeometry( lineGeom, 10, QgsGeometrySnapper::EndPointToEndPoint );
QCOMPARE( result.exportToWkt(), QStringLiteral( "LineString (0 0, 102 0, 98 102, 0 100)" ) );

QgsGeometry lineGeom2 = QgsGeometry::fromWkt( QStringLiteral( "LineString(50 0, 102 0, 98 102)" ) );
result = snapper.snapGeometry( lineGeom2, 1, QgsGeometrySnapper::EndPointToEndPoint );
QCOMPARE( result.exportToWkt(), QStringLiteral( "LineString (50 0, 102 0, 98 102)" ) );

QgsGeometry lineGeom3 = QgsGeometry::fromWkt( QStringLiteral( "LineString(50 -10, 50 -1)" ) );
result = snapper.snapGeometry( lineGeom3, 2, QgsGeometrySnapper::EndPointToEndPoint );
QCOMPARE( result.exportToWkt(), QStringLiteral( "LineString (50 -10, 50 -1)" ) );
}

void TestQgsGeometrySnapper::internalSnapper()
{
QgsGeometry refGeom = QgsGeometry::fromWkt( QStringLiteral( "LineString(0 0, 10 0, 10 10)" ) );

0 comments on commit 636e9c5

Please sign in to comment.