From 4a0669714bbe8337f8edb9537259b6279117ae20 Mon Sep 17 00:00:00 2001 From: Martin Dobias Date: Wed, 6 Jun 2018 16:17:01 +0200 Subject: [PATCH 1/5] [FEATURE] Snap geometries algorithm Makes sure that any two vertices of the vector layer are at least at distance given by the threshold value. The algorithm moves nearby vertices to one location and adds vertices to segments that are passing around other vertices within the threshold. It does not remove any vertices. Also, it does not modify geometries unless needed (it does not snap coordinates to a grid). This algorithm comes handy when doing vector overlay operations such as intersection, union or difference to prevent possible topological errors caused by numerical errors if coordinates are very close to each other. After running the algorithm some previously valid geometries may become invalid and therefore it may be useful to run Fix geometries algorithm afterwards. --- .../testdata/custom/snap_geometries.geojson | 9 + .../testdata/expected/snap_geometries.gml | 24 + .../testdata/expected/snap_geometries.xsd | 23 + .../tests/testdata/qgis_algorithm_tests.yaml | 12 + src/analysis/CMakeLists.txt | 1 + .../processing/qgsalgorithmsnapgeometries.cpp | 411 ++++++++++++++++++ .../processing/qgsalgorithmsnapgeometries.h | 43 ++ .../processing/qgsnativealgorithms.cpp | 2 + 8 files changed, 525 insertions(+) create mode 100644 python/plugins/processing/tests/testdata/custom/snap_geometries.geojson create mode 100644 python/plugins/processing/tests/testdata/expected/snap_geometries.gml create mode 100644 python/plugins/processing/tests/testdata/expected/snap_geometries.xsd create mode 100644 src/analysis/processing/qgsalgorithmsnapgeometries.cpp create mode 100644 src/analysis/processing/qgsalgorithmsnapgeometries.h diff --git a/python/plugins/processing/tests/testdata/custom/snap_geometries.geojson b/python/plugins/processing/tests/testdata/custom/snap_geometries.geojson new file mode 100644 index 00000000000..c661eda464d --- /dev/null +++ b/python/plugins/processing/tests/testdata/custom/snap_geometries.geojson @@ -0,0 +1,9 @@ +{ +"type": "FeatureCollection", +"name": "snap_geometries", +"crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:EPSG::3857" } }, +"features": [ +{ "type": "Feature", "properties": { }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 20.123, 10.123 ], [ 20.123, 20.456 ], [ 20.2, 20.456 ], [ 20.2, 20.5 ], [ 30.0, 20.5 ], [ 30.0, 10.123 ], [ 29.8, 10.123 ], [ 21.0, 10.123 ], [ 20.123, 10.123 ] ] ] } }, +{ "type": "Feature", "properties": { }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 31.0, 10.0 ], [ 20.0, 10.0 ], [ 20.0, 5.0 ], [ 31.0, 10.0 ] ] ] } } +] +} diff --git a/python/plugins/processing/tests/testdata/expected/snap_geometries.gml b/python/plugins/processing/tests/testdata/expected/snap_geometries.gml new file mode 100644 index 00000000000..610846d3811 --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/snap_geometries.gml @@ -0,0 +1,24 @@ + + + + + 205 + 3120.5 + + + + + + 20.123,10.123 20.123,20.456 20.123,20.456 20.123,20.456 30.0,20.5 30.0,10.123 30.0,10.123 21.0,10.123 20.123,10.123 + + + + + 31,10 30.0,10.123 21.0,10.123 20.123,10.123 20,5 31,10 + + + diff --git a/python/plugins/processing/tests/testdata/expected/snap_geometries.xsd b/python/plugins/processing/tests/testdata/expected/snap_geometries.xsd new file mode 100644 index 00000000000..c27240ec294 --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/snap_geometries.xsd @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml b/python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml index 749d524d777..854038d5a91 100755 --- a/python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml +++ b/python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml @@ -5586,6 +5586,18 @@ tests: fields: fid: skip + - algorithm: native:snap + name: Test Snap Geometries (to each other) + params: + INPUT: + name: custom/snap_geometries.geojson + type: vector + THRESHOLD: 0.5 + results: + OUTPUT: + name: expected/snap_geometries.gml + type: vector + - algorithm: native:taperedbuffer name: Tapered buffers (lines) params: diff --git a/src/analysis/CMakeLists.txt b/src/analysis/CMakeLists.txt index f8b89ad2bfd..d82ef5f4b63 100755 --- a/src/analysis/CMakeLists.txt +++ b/src/analysis/CMakeLists.txt @@ -85,6 +85,7 @@ SET(QGIS_ANALYSIS_SRCS processing/qgsalgorithmshortestpathpointtopoint.cpp processing/qgsalgorithmsimplify.cpp processing/qgsalgorithmsmooth.cpp + processing/qgsalgorithmsnapgeometries.cpp processing/qgsalgorithmsnaptogrid.cpp processing/qgsalgorithmsplitwithlines.cpp processing/qgsalgorithmstringconcatenation.cpp diff --git a/src/analysis/processing/qgsalgorithmsnapgeometries.cpp b/src/analysis/processing/qgsalgorithmsnapgeometries.cpp new file mode 100644 index 00000000000..c0c1a53599a --- /dev/null +++ b/src/analysis/processing/qgsalgorithmsnapgeometries.cpp @@ -0,0 +1,411 @@ +/*************************************************************************** + qgsalgorithmsnapgeometries.cpp + --------------------- + Date : May 2018 + Copyright : (C) 2018 by Martin Dobias + Email : wonder dot sk at gmail dot com + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgsalgorithmsnapgeometries.h" + +#include "qgsgeometrycollection.h" +#include "qgsgeometryutils.h" +#include "qgslinestring.h" +#include "qgspolygon.h" + +///@cond PRIVATE + +QString QgsSnapGeometriesAlgorithm::name() const +{ + return QStringLiteral( "snap" ); +} + +QString QgsSnapGeometriesAlgorithm::displayName() const +{ + return QObject::tr( "Snap geometries" ); +} + +QString QgsSnapGeometriesAlgorithm::group() const +{ + return QObject::tr( "Vector geometry" ); +} + +QString QgsSnapGeometriesAlgorithm::groupId() const +{ + return QStringLiteral( "vectorgeometry" ); +} + +QString QgsSnapGeometriesAlgorithm::shortHelpString() const +{ + return QObject::tr( "Makes sure that any two vertices of the vector layer are at least at distance given by the threshold value. " + "The algorithm moves nearby vertices to one location and adds vertices to segments that are passing around other " + "vertices within the threshold. It does not remove any vertices. Also, it does not modify geometries unless " + "needed (it does not snap coordinates to a grid).\n\n" + "This algorithm comes handy when doing vector overlay operations such as intersection, union or difference " + "to prevent possible topological errors caused by numerical errors if coordinates are very close to each other.\n\n" + "After running the algorithm some previously valid geometries may become invalid and therefore it may be useful " + "to run Fix geometries algorithm afterwards." ); +} + +QgsProcessingAlgorithm *QgsSnapGeometriesAlgorithm::createInstance() const +{ + return new QgsSnapGeometriesAlgorithm(); +} + +void QgsSnapGeometriesAlgorithm::initAlgorithm( const QVariantMap & ) +{ + addParameter( new QgsProcessingParameterFeatureSource( QStringLiteral( "INPUT" ), QObject::tr( "Input layer" ) ) ); + addParameter( new QgsProcessingParameterNumber( QStringLiteral( "THRESHOLD" ), QObject::tr( "Threshold" ), QgsProcessingParameterNumber::Double, 0.01 ) ); + addParameter( new QgsProcessingParameterFeatureSink( QStringLiteral( "OUTPUT" ), QObject::tr( "Output layer" ) ) ); +} + +QVariantMap QgsSnapGeometriesAlgorithm::processAlgorithm( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback *feedback ) +{ + std::unique_ptr< QgsFeatureSource > source( parameterAsSource( parameters, QStringLiteral( "INPUT" ), context ) ); + if ( !source ) + throw QgsProcessingException( invalidSourceError( parameters, QStringLiteral( "INPUT" ) ) ); + + double thresh = parameterAsDouble( parameters, QStringLiteral( "THRESHOLD" ), context ); + + QString dest; + std::unique_ptr< QgsFeatureSink > sink( parameterAsSink( parameters, QStringLiteral( "OUTPUT" ), context, dest, source->fields(), source->wkbType(), source->sourceCrs() ) ); + if ( !sink ) + throw QgsProcessingException( invalidSinkError( parameters, QStringLiteral( "OUTPUT" ) ) ); + + QVariantMap outputs; + outputs.insert( QStringLiteral( "OUTPUT" ), dest ); + + // now go and snap vertices of geometries of source together + run( *source.get(), *sink.get(), thresh, feedback ); + + return outputs; +} + + +//! record about vertex coordinates and index of anchor to which it is snapped +typedef struct +{ + //! coordinates of the point + double x, y; + + /** + * Anchor information: + * 0+ - index of anchor to which this point should be snapped + * -1 - initial value (undefined) + * -2 - this point is an anchor, i.e. do not snap this point (snap others to this point) + */ + int anchor; +} AnchorPoint; + + +//! record about anchor being along a segment +typedef struct +{ + int anchor; //!< Index of the anchor point + double along; //!< Distance of the anchor point along the segment +} AnchorAlongSegment; + + +static void buildSnapIndex( QgsFeatureIterator &fi, QgsSpatialIndex &index, QVector &pnts, QgsProcessingFeedback *feedback, int &count, int totalCount ) +{ + QgsFeature f; + int pntId = 0; + + while ( fi.nextFeature( f ) ) + { + QgsGeometry g = f.geometry(); + + for ( auto it = g.vertices_begin(); it != g.vertices_end(); ++it ) + { + QgsPoint pt = *it; + QgsRectangle rect( pt.x(), pt.y(), pt.x(), pt.y() ); + + QList ids = index.intersects( rect ); + if ( ids.isEmpty() ) + { + // add to tree and to structure + index.insertFeature( pntId, pt.boundingBox() ); + + AnchorPoint xp; + xp.x = pt.x(); + xp.y = pt.y(); + xp.anchor = -1; + pnts.append( xp ); + pntId++; + } + } + + ++count; + feedback->setProgress( 100. * count / totalCount ); + } +} + + +static void assignAnchors( QgsSpatialIndex &index, QVector &pnts, double thresh ) +{ + double thresh2 = thresh * thresh; + int nanchors = 0, ntosnap = 0; + for ( int point = 0; point < pnts.count(); ++point ) + { + if ( pnts[point].anchor >= 0 ) + continue; + + pnts[point].anchor = -2; // make it anchor + nanchors++; + + // Find points in threshold + double x = pnts[point].x, y = pnts[point].y; + QgsRectangle rect( x - thresh, y - thresh, x + thresh, y + thresh ); + + const QList ids = index.intersects( rect ); + for ( QgsFeatureId pointb : ids ) + { + if ( pointb == point ) + continue; + + double dx = pnts[pointb].x - pnts[point].x; + double dy = pnts[pointb].y - pnts[point].y; + double dist2 = dx * dx + dy * dy; + if ( dist2 > thresh2 ) + continue; // outside threshold + + if ( pnts[pointb].anchor == -1 ) + { + // doesn't have an anchor yet + pnts[pointb].anchor = point; + ntosnap++; + } + else if ( pnts[pointb].anchor >= 0 ) + { + // check distance to previously assigned anchor + double dx2 = pnts[pnts[pointb].anchor].x - pnts[pointb].x; + double dy2 = pnts[pnts[pointb].anchor].y - pnts[pointb].y; + double dist2_a = dx2 * dx2 + dy2 * dy2; + if ( dist2 < dist2_a ) + pnts[pointb].anchor = point; // replace old anchor + } + } + } +} + + +static bool snapPoint( QgsPoint *pt, QgsSpatialIndex &index, QVector &pnts ) +{ + // Find point ( should always find one point ) + QList fids = index.intersects( QgsRectangle( pt->x(), pt->y(), pt->x(), pt->y() ) ); + Q_ASSERT( fids.count() == 1 ); + + int spoint = fids[0]; + int anchor = pnts[spoint].anchor; + + if ( anchor >= 0 ) + { + // to be snapped + pt->setX( pnts[anchor].x ); + pt->setY( pnts[anchor].y ); + return true; + } + + return false; +} + + +static bool snapLineString( QgsLineString *linestring, QgsSpatialIndex &index, QVector &pnts, double thresh ) +{ + QVector newPoints; + QVector anchors; // indexes of anchors for vertices + double thresh2 = thresh * thresh; + double minDistX, minDistY; // coordinates of the closest point on the segment line + bool changed = false; + + // snap vertices + for ( int v = 0; v < linestring->numPoints(); v++ ) + { + double x = linestring->xAt( v ); + double y = linestring->yAt( v ); + QgsRectangle rect( x, y, x, y ); + + // Find point ( should always find one point ) + QList fids = index.intersects( rect ); + Q_ASSERT( fids.count() == 1 ); + + int spoint = fids.first(); + int anchor = pnts[spoint].anchor; + if ( anchor >= 0 ) + { + // to be snapped + linestring->setXAt( v, pnts[anchor].x ); + linestring->setYAt( v, pnts[anchor].y ); + anchors.append( anchor ); // point on new location + changed = true; + } + else + { + anchors.append( spoint ); // old point + } + } + + // Snap all segments to anchors in threshold + for ( int v = 0; v < linestring->numPoints() - 1; v++ ) + { + double x1 = linestring->xAt( v ); + double x2 = linestring->xAt( v + 1 ); + double y1 = linestring->yAt( v ); + double y2 = linestring->yAt( v + 1 ); + + newPoints << linestring->pointN( v ); + + // Box + double xmin = x1, xmax = x2, ymin = y1, ymax = y2; + if ( xmin > xmax ) + std::swap( xmin, xmax ); + if ( ymin > ymax ) + std::swap( ymin, ymax ); + + QgsRectangle rect( xmin - thresh, ymin - thresh, xmax + thresh, ymax + thresh ); + + // Find points + const QList fids = index.intersects( rect ); + + QVector newVerticesAlongSegment; + + // Snap to anchor in threshold different from end points + for ( QgsFeatureId fid : fids ) + { + int spoint = fid; + + if ( spoint == anchors[v] || spoint == anchors[v + 1] ) + continue; // end point + if ( pnts[spoint].anchor >= 0 ) + continue; // point is not anchor + + // Check the distance + double dist2 = QgsGeometryUtils::sqrDistToLine( pnts[spoint].x, pnts[spoint].y, x1, y1, x2, y2, minDistX, minDistY, 0 ); + // skip points that are behind segment's endpoints or extremely close to them + double dx1 = minDistX - x1, dx2 = minDistX - x2; + double dy1 = minDistY - y1, dy2 = minDistY - y2; + bool isOnSegment = !qgsDoubleNear( dx1 * dx1 + dy1 * dy1, 0 ) && !qgsDoubleNear( dx2 * dx2 + dy2 * dy2, 0 ); + if ( isOnSegment && dist2 <= thresh2 ) + { + // an anchor is in the threshold + AnchorAlongSegment item; + item.anchor = spoint; + item.along = QgsPointXY( x1, y1 ).distance( minDistX, minDistY ); + newVerticesAlongSegment << item; + } + } + + if ( !newVerticesAlongSegment.isEmpty() ) + { + // sort by distance along the segment + std::sort( newVerticesAlongSegment.begin(), newVerticesAlongSegment.end(), []( const AnchorAlongSegment & p1, const AnchorAlongSegment & p2 ) + { + return ( p1.along < p2.along ? -1 : ( p1.along > p2.along ) ); + } ); + + // insert new vertices + for ( int i = 0; i < newVerticesAlongSegment.count(); i++ ) + { + int anchor = newVerticesAlongSegment[i].anchor; + newPoints << QgsPoint( pnts[anchor].x, pnts[anchor].y, 0 ); + } + changed = true; + } + } + + // append end point + newPoints << linestring->pointN( linestring->numPoints() - 1 ); + + // replace linestring's points + if ( changed ) + linestring->setPoints( newPoints ); + + return changed; +} + + +static bool snapGeometry( QgsAbstractGeometry *g, QgsSpatialIndex &index, QVector &pnts, double thresh ) +{ + bool changed = false; + if ( QgsLineString *linestring = qgsgeometry_cast( g ) ) + { + changed |= snapLineString( linestring, index, pnts, thresh ); + } + else if ( QgsPolygon *polygon = qgsgeometry_cast( g ) ) + { + if ( QgsLineString *exteriorRing = qgsgeometry_cast( polygon->exteriorRing() ) ) + changed |= snapLineString( exteriorRing, index, pnts, thresh ); + for ( int i = 0; i < polygon->numInteriorRings(); ++i ) + { + if ( QgsLineString *interiorRing = qgsgeometry_cast( polygon->interiorRing( i ) ) ) + changed |= snapLineString( interiorRing, index, pnts, thresh ); + } + } + else if ( QgsGeometryCollection *collection = qgsgeometry_cast( g ) ) + { + for ( int i = 0; i < collection->numGeometries(); ++i ) + changed |= snapGeometry( collection->geometryN( i ), index, pnts, thresh ); + } + else if ( QgsPoint *pt = qgsgeometry_cast( g ) ) + { + changed |= snapPoint( pt, index, pnts ); + } + + return changed; +} + + +void QgsSnapGeometriesAlgorithm::run( const QgsFeatureSource &source, QgsFeatureSink &sink, double thresh, QgsProcessingFeedback *feedback ) +{ + // the logic here comes from GRASS implementation of Vect_snap_lines_list() + + int count = 0; + int totalCount = source.featureCount() * 2; + + // step 1: record all point locations in a spatial index + extra data structure to keep + // reference to which other point they have been snapped to (in the next phase). + + QgsSpatialIndex index; + QVector pnts; + QgsFeatureIterator fi = source.getFeatures(); + buildSnapIndex( fi, index, pnts, feedback, count, totalCount ); + + // step 2: go through all registered points and if not yet marked mark it as anchor and + // assign this anchor to all not yet marked points in threshold + + assignAnchors( index, pnts, thresh ); + + // step 3: alignment of vertices and segments to the anchors + // Go through all lines and: + // 1) for all vertices: if not anchor snap it to its anchor + // 2) for all segments: snap it to all anchors in threshold (except anchors of vertices of course) + + int modified = 0; + QgsFeature f; + fi = source.getFeatures(); + while ( fi.nextFeature( f ) ) + { + QgsGeometry geom = f.geometry(); + if ( snapGeometry( geom.get(), index, pnts, thresh ) ) + { + f.setGeometry( geom ); + ++modified; + } + + sink.addFeature( f, QgsFeatureSink::FastInsert ); + + ++count; + feedback->setProgress( 100. * count / totalCount ); + } + + feedback->pushInfo( QObject::tr( "Snapped %1 geometries." ).arg( modified ) ); +} + +///@endcond PRIVATE diff --git a/src/analysis/processing/qgsalgorithmsnapgeometries.h b/src/analysis/processing/qgsalgorithmsnapgeometries.h new file mode 100644 index 00000000000..99bb1e411df --- /dev/null +++ b/src/analysis/processing/qgsalgorithmsnapgeometries.h @@ -0,0 +1,43 @@ +/*************************************************************************** + qgsalgorithmsnapgeometries.h + --------------------- + Date : May 2018 + Copyright : (C) 2018 by Martin Dobias + Email : wonder dot sk at gmail dot com + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef QGSALGORITHMSNAPGEOMETRIES_H +#define QGSALGORITHMSNAPGEOMETRIES_H + +#include "qgsprocessingalgorithm.h" + +///@cond PRIVATE + +class QgsSnapGeometriesAlgorithm : public QgsProcessingAlgorithm +{ + public: + QString name() const override; + QString displayName() const override; + QString group() const override; + QString groupId() const override; + QString shortHelpString() const override; + + protected: + QgsProcessingAlgorithm *createInstance() const override; + void initAlgorithm( const QVariantMap &configuration = QVariantMap() ) override; + QVariantMap processAlgorithm( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback *feedback ) override; + + private: + static void run( const QgsFeatureSource &source, QgsFeatureSink &sink, double thresh, QgsProcessingFeedback *feedback ); +}; + +///@endcond PRIVATE + +#endif // QGSALGORITHMSNAPGEOMETRIES_H diff --git a/src/analysis/processing/qgsnativealgorithms.cpp b/src/analysis/processing/qgsnativealgorithms.cpp index 0320ff6ed9e..8047987a908 100644 --- a/src/analysis/processing/qgsnativealgorithms.cpp +++ b/src/analysis/processing/qgsnativealgorithms.cpp @@ -82,6 +82,7 @@ #include "qgsalgorithmshortestpathpointtopoint.h" #include "qgsalgorithmsimplify.h" #include "qgsalgorithmsmooth.h" +#include "qgsalgorithmsnapgeometries.h" #include "qgsalgorithmsnaptogrid.h" #include "qgsalgorithmsplitwithlines.h" #include "qgsalgorithmstringconcatenation.h" @@ -212,6 +213,7 @@ void QgsNativeAlgorithms::loadAlgorithms() addAlgorithm( new QgsShortestPathPointToPointAlgorithm() ); addAlgorithm( new QgsSimplifyAlgorithm() ); addAlgorithm( new QgsSmoothAlgorithm() ); + addAlgorithm( new QgsSnapGeometriesAlgorithm() ); addAlgorithm( new QgsSnapToGridAlgorithm() ); addAlgorithm( new QgsSplitWithLinesAlgorithm() ); addAlgorithm( new QgsStringConcatenationAlgorithm() ); From cef3395e6e4c459a3c5451a5817b4223d5e2e712 Mon Sep 17 00:00:00 2001 From: Martin Dobias Date: Wed, 6 Jun 2018 17:56:38 +0200 Subject: [PATCH 2/5] Add SIP_NO_FILE to the alg header --- src/analysis/processing/qgsalgorithmsnapgeometries.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/analysis/processing/qgsalgorithmsnapgeometries.h b/src/analysis/processing/qgsalgorithmsnapgeometries.h index 99bb1e411df..d0f6a7bdac2 100644 --- a/src/analysis/processing/qgsalgorithmsnapgeometries.h +++ b/src/analysis/processing/qgsalgorithmsnapgeometries.h @@ -16,6 +16,8 @@ #ifndef QGSALGORITHMSNAPGEOMETRIES_H #define QGSALGORITHMSNAPGEOMETRIES_H +#define SIP_NO_FILE + #include "qgsprocessingalgorithm.h" ///@cond PRIVATE From 1af35b294e31c0428eb3dd38bda1be82a27c7302 Mon Sep 17 00:00:00 2001 From: Martin Dobias Date: Thu, 13 Sep 2018 16:24:37 +0200 Subject: [PATCH 3/5] Moved new snapping alg as another mode of "Snap geometries" processing alg --- python/analysis/analysis_auto.sip | 1 + .../qgsgeometrysnappersinglesource.sip.in | 52 +++++++++++ .../processing/algs/qgis/SnapGeometries.py | 11 ++- .../testdata/custom/circular_strings.gpkg | Bin 131072 -> 131072 bytes .../tests/testdata/qgis_algorithm_tests.yaml | 28 +++--- src/analysis/CMakeLists.txt | 3 +- .../processing/qgsalgorithmsnapgeometries.h | 45 --------- .../processing/qgsnativealgorithms.cpp | 2 - .../qgsgeometrysnappersinglesource.cpp} | 86 ++---------------- .../vector/qgsgeometrysnappersinglesource.h | 54 +++++++++++ 10 files changed, 145 insertions(+), 137 deletions(-) create mode 100644 python/analysis/auto_generated/vector/qgsgeometrysnappersinglesource.sip.in delete mode 100644 src/analysis/processing/qgsalgorithmsnapgeometries.h rename src/analysis/{processing/qgsalgorithmsnapgeometries.cpp => vector/qgsgeometrysnappersinglesource.cpp} (75%) create mode 100644 src/analysis/vector/qgsgeometrysnappersinglesource.h diff --git a/python/analysis/analysis_auto.sip b/python/analysis/analysis_auto.sip index 5d92e3cbaf8..00ec4069e26 100644 --- a/python/analysis/analysis_auto.sip +++ b/python/analysis/analysis_auto.sip @@ -13,6 +13,7 @@ %Include auto_generated/raster/qgsrastercalcnode.sip %Include auto_generated/raster/qgstotalcurvaturefilter.sip %Include auto_generated/vector/qgsgeometrysnapper.sip +%Include auto_generated/vector/qgsgeometrysnappersinglesource.sip %Include auto_generated/vector/qgszonalstatistics.sip %Include auto_generated/interpolation/qgsinterpolator.sip %Include auto_generated/interpolation/qgsgridfilewriter.sip diff --git a/python/analysis/auto_generated/vector/qgsgeometrysnappersinglesource.sip.in b/python/analysis/auto_generated/vector/qgsgeometrysnappersinglesource.sip.in new file mode 100644 index 00000000000..c3391561b94 --- /dev/null +++ b/python/analysis/auto_generated/vector/qgsgeometrysnappersinglesource.sip.in @@ -0,0 +1,52 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/analysis/vector/qgsgeometrysnappersinglesource.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + +class QgsGeometrySnapperSingleSource +{ +%Docstring + +Makes sure that any two vertices of the vector layer are at least at distance given by the threshold value. +The algorithm moves nearby vertices to one location and adds vertices to segments that are passing around other +vertices within the threshold. It does not remove any vertices. Also, it does not modify geometries unless +needed (it does not snap coordinates to a grid). + +This algorithm comes handy when doing vector overlay operations such as intersection, union or difference +to prevent possible topological errors caused by numerical errors if coordinates are very close to each other. + +After running the algorithm some previously valid geometries may become invalid and therefore it may be useful +to run Fix geometries algorithm afterwards. + +.. note:: + + Originally ported from GRASS implementation of Vect_snap_lines_list() + +.. versionadded:: 3.4 +%End + +%TypeHeaderCode +#include "qgsgeometrysnappersinglesource.h" +%End + public: + + static int run( const QgsFeatureSource &source, QgsFeatureSink &sink, double thresh, QgsFeedback *feedback ); +%Docstring +Run the algorithm on given source and output results to the sink, using threshold value in the source's map units. +Returns number of modified geometries. +%End +}; + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/analysis/vector/qgsgeometrysnappersinglesource.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/plugins/processing/algs/qgis/SnapGeometries.py b/python/plugins/processing/algs/qgis/SnapGeometries.py index 9bb85c4c24d..80c270697ca 100644 --- a/python/plugins/processing/algs/qgis/SnapGeometries.py +++ b/python/plugins/processing/algs/qgis/SnapGeometries.py @@ -26,6 +26,7 @@ __copyright__ = '(C) 2016, Nyall Dawson' __revision__ = '$Format:%H$' from qgis.analysis import (QgsGeometrySnapper, + QgsGeometrySnapperSingleSource, QgsInternalGeometrySnapper) from qgis.core import (QgsFeatureSink, QgsProcessing, @@ -71,7 +72,8 @@ class SnapGeometriesToLayer(QgisAlgorithm): self.tr('Prefer closest point, don\'t insert new vertices'), 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.tr('Snap end points to end points only'), + self.tr('Snap to anchor nodes (single layer only)')] self.addParameter(QgsProcessingParameterEnum( self.BEHAVIOR, self.tr('Behavior'), @@ -106,6 +108,9 @@ class SnapGeometriesToLayer(QgisAlgorithm): total = 100.0 / source.featureCount() if source.featureCount() else 0 if parameters[self.INPUT] != parameters[self.REFERENCE_LAYER]: + if mode == 7: + raise QgsProcessingException(self.tr('This mode applies when the input and reference layer are the same.')) + snapper = QgsGeometrySnapper(reference_source) processed = 0 for f in features: @@ -119,6 +124,10 @@ class SnapGeometriesToLayer(QgisAlgorithm): sink.addFeature(f) processed += 1 feedback.setProgress(processed * total) + elif mode == 7: + # input layer == ref layer + modified_count = QgsGeometrySnapperSingleSource.run(source, sink, tolerance, feedback) + feedback.pushInfo(self.tr('Snapped {} geometries.').format(modified_count)) else: # snapping internally snapper = QgsInternalGeometrySnapper(tolerance, mode) diff --git a/python/plugins/processing/tests/testdata/custom/circular_strings.gpkg b/python/plugins/processing/tests/testdata/custom/circular_strings.gpkg index fd342b87dd83b40b0c5800913c181f10a3d29ac2..ff47737efa6a8b639622c2fa9d33fac4b8615a97 100644 GIT binary patch delta 22 ecmZo@;Am*zm>|vgf1->t|vgbE1qh source( parameterAsSource( parameters, QStringLiteral( "INPUT" ), context ) ); - if ( !source ) - throw QgsProcessingException( invalidSourceError( parameters, QStringLiteral( "INPUT" ) ) ); - - double thresh = parameterAsDouble( parameters, QStringLiteral( "THRESHOLD" ), context ); - - QString dest; - std::unique_ptr< QgsFeatureSink > sink( parameterAsSink( parameters, QStringLiteral( "OUTPUT" ), context, dest, source->fields(), source->wkbType(), source->sourceCrs() ) ); - if ( !sink ) - throw QgsProcessingException( invalidSinkError( parameters, QStringLiteral( "OUTPUT" ) ) ); - - QVariantMap outputs; - outputs.insert( QStringLiteral( "OUTPUT" ), dest ); - - // now go and snap vertices of geometries of source together - run( *source.get(), *sink.get(), thresh, feedback ); - - return outputs; -} - +#include "qgsspatialindex.h" //! record about vertex coordinates and index of anchor to which it is snapped typedef struct @@ -113,7 +49,7 @@ typedef struct } AnchorAlongSegment; -static void buildSnapIndex( QgsFeatureIterator &fi, QgsSpatialIndex &index, QVector &pnts, QgsProcessingFeedback *feedback, int &count, int totalCount ) +static void buildSnapIndex( QgsFeatureIterator &fi, QgsSpatialIndex &index, QVector &pnts, QgsFeedback *feedback, int &count, int totalCount ) { QgsFeature f; int pntId = 0; @@ -362,7 +298,7 @@ static bool snapGeometry( QgsAbstractGeometry *g, QgsSpatialIndex &index, QVecto } -void QgsSnapGeometriesAlgorithm::run( const QgsFeatureSource &source, QgsFeatureSink &sink, double thresh, QgsProcessingFeedback *feedback ) +int QgsGeometrySnapperSingleSource::run(const QgsFeatureSource &source, QgsFeatureSink &sink, double thresh, QgsFeedback *feedback) { // the logic here comes from GRASS implementation of Vect_snap_lines_list() @@ -405,7 +341,5 @@ void QgsSnapGeometriesAlgorithm::run( const QgsFeatureSource &source, QgsFeature feedback->setProgress( 100. * count / totalCount ); } - feedback->pushInfo( QObject::tr( "Snapped %1 geometries." ).arg( modified ) ); + return modified; } - -///@endcond PRIVATE diff --git a/src/analysis/vector/qgsgeometrysnappersinglesource.h b/src/analysis/vector/qgsgeometrysnappersinglesource.h new file mode 100644 index 00000000000..f413d02cfba --- /dev/null +++ b/src/analysis/vector/qgsgeometrysnappersinglesource.h @@ -0,0 +1,54 @@ +/*************************************************************************** + qgsgeometrysnappersinglesource.h + --------------------- + Date : May 2018 + Copyright : (C) 2018 by Martin Dobias + Email : wonder dot sk at gmail dot com + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef QGSGEOMETRYSNAPPERSINGLESOURCE_H +#define QGSGEOMETRYSNAPPERSINGLESOURCE_H + +#include "qgis_analysis.h" + +class QgsFeatureSink; +class QgsFeatureSource; +class QgsFeedback; + +/** + * \ingroup analysis + * + * Makes sure that any two vertices of the vector layer are at least at distance given by the threshold value. + * The algorithm moves nearby vertices to one location and adds vertices to segments that are passing around other + * vertices within the threshold. It does not remove any vertices. Also, it does not modify geometries unless + * needed (it does not snap coordinates to a grid). + * + * This algorithm comes handy when doing vector overlay operations such as intersection, union or difference + * to prevent possible topological errors caused by numerical errors if coordinates are very close to each other. + * + * After running the algorithm some previously valid geometries may become invalid and therefore it may be useful + * to run Fix geometries algorithm afterwards. + * + * \note Originally ported from GRASS implementation of Vect_snap_lines_list() + * + * \since QGIS 3.4 + */ +class ANALYSIS_EXPORT QgsGeometrySnapperSingleSource +{ + public: + + /** + * Run the algorithm on given source and output results to the sink, using threshold value in the source's map units. + * Returns number of modified geometries. + */ + static int run( const QgsFeatureSource &source, QgsFeatureSink &sink, double thresh, QgsFeedback *feedback ); +}; + +#endif // QGSGEOMETRYSNAPPERSINGLESOURCE_H From c5e431cdbbf36e6aa29ced60537e9056b386351b Mon Sep 17 00:00:00 2001 From: Martin Dobias Date: Thu, 13 Sep 2018 18:24:49 +0200 Subject: [PATCH 4/5] prepare-commit somehow missed to indent the new file --- src/analysis/vector/qgsgeometrysnappersinglesource.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/analysis/vector/qgsgeometrysnappersinglesource.cpp b/src/analysis/vector/qgsgeometrysnappersinglesource.cpp index 4845d811e24..3429eee372f 100644 --- a/src/analysis/vector/qgsgeometrysnappersinglesource.cpp +++ b/src/analysis/vector/qgsgeometrysnappersinglesource.cpp @@ -298,7 +298,7 @@ static bool snapGeometry( QgsAbstractGeometry *g, QgsSpatialIndex &index, QVecto } -int QgsGeometrySnapperSingleSource::run(const QgsFeatureSource &source, QgsFeatureSink &sink, double thresh, QgsFeedback *feedback) +int QgsGeometrySnapperSingleSource::run( const QgsFeatureSource &source, QgsFeatureSink &sink, double thresh, QgsFeedback *feedback ) { // the logic here comes from GRASS implementation of Vect_snap_lines_list() From 2b55858618caea900ad8ca374dc6938f060607bd Mon Sep 17 00:00:00 2001 From: Martin Dobias Date: Fri, 14 Sep 2018 10:03:24 +0200 Subject: [PATCH 5/5] Improvements from review --- .../vector/qgsgeometrysnappersinglesource.cpp | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/analysis/vector/qgsgeometrysnappersinglesource.cpp b/src/analysis/vector/qgsgeometrysnappersinglesource.cpp index 3429eee372f..a08eaddb24c 100644 --- a/src/analysis/vector/qgsgeometrysnappersinglesource.cpp +++ b/src/analysis/vector/qgsgeometrysnappersinglesource.cpp @@ -26,7 +26,7 @@ #include "qgsspatialindex.h" //! record about vertex coordinates and index of anchor to which it is snapped -typedef struct +struct AnchorPoint { //! coordinates of the point double x, y; @@ -38,15 +38,15 @@ typedef struct * -2 - this point is an anchor, i.e. do not snap this point (snap others to this point) */ int anchor; -} AnchorPoint; +}; //! record about anchor being along a segment -typedef struct +struct AnchorAlongSegment { int anchor; //!< Index of the anchor point double along; //!< Distance of the anchor point along the segment -} AnchorAlongSegment; +}; static void buildSnapIndex( QgsFeatureIterator &fi, QgsSpatialIndex &index, QVector &pnts, QgsFeedback *feedback, int &count, int totalCount ) @@ -56,6 +56,9 @@ static void buildSnapIndex( QgsFeatureIterator &fi, QgsSpatialIndex &index, QVec while ( fi.nextFeature( f ) ) { + if ( feedback->isCanceled() ) + break; + QgsGeometry g = f.geometry(); for ( auto it = g.vertices_begin(); it != g.vertices_end(); ++it ) @@ -310,9 +313,14 @@ int QgsGeometrySnapperSingleSource::run( const QgsFeatureSource &source, QgsFeat QgsSpatialIndex index; QVector pnts; - QgsFeatureIterator fi = source.getFeatures(); + QgsFeatureRequest request; + request.setSubsetOfAttributes( QgsAttributeList() ); + QgsFeatureIterator fi = source.getFeatures( request ); buildSnapIndex( fi, index, pnts, feedback, count, totalCount ); + if ( feedback->isCanceled() ) + return 0; + // step 2: go through all registered points and if not yet marked mark it as anchor and // assign this anchor to all not yet marked points in threshold @@ -328,6 +336,9 @@ int QgsGeometrySnapperSingleSource::run( const QgsFeatureSource &source, QgsFeat fi = source.getFeatures(); while ( fi.nextFeature( f ) ) { + if ( feedback->isCanceled() ) + break; + QgsGeometry geom = f.geometry(); if ( snapGeometry( geom.get(), index, pnts, thresh ) ) {