[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
This commit is contained in:
Nyall Dawson 2017-03-30 15:46:07 +10:00
parent 92249c1a33
commit 636e9c5ea3
5 changed files with 95 additions and 16 deletions

View File

@ -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,
};
/**

View File

@ -52,7 +52,10 @@ class SnapGeometriesToLayer(GeoAlgorithm):
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'),

View File

@ -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 ) );

View File

@ -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 );

View File

@ -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)" ) );