mirror of
				https://github.com/qgis/QGIS.git
				synced 2025-11-04 00:04:25 -05:00 
			
		
		
		
	Merge pull request #7194 from wonder-sk/snap-geometries-alg
[FEATURE] Snap geometries algorithm
This commit is contained in:
		
						commit
						9bcd21fb19
					
				@ -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
 | 
			
		||||
 | 
			
		||||
@ -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   *
 | 
			
		||||
 ************************************************************************/
 | 
			
		||||
@ -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)
 | 
			
		||||
 | 
			
		||||
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										9
									
								
								python/plugins/processing/tests/testdata/custom/snap_geometries.geojson
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								python/plugins/processing/tests/testdata/custom/snap_geometries.geojson
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -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 ] ] ] } }
 | 
			
		||||
]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										24
									
								
								python/plugins/processing/tests/testdata/expected/snap_geometries.gml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								python/plugins/processing/tests/testdata/expected/snap_geometries.gml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,24 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8" ?>
 | 
			
		||||
<ogr:FeatureCollection
 | 
			
		||||
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 | 
			
		||||
     xsi:schemaLocation="http://ogr.maptools.org/ snap_geometries.xsd"
 | 
			
		||||
     xmlns:ogr="http://ogr.maptools.org/"
 | 
			
		||||
     xmlns:gml="http://www.opengis.net/gml">
 | 
			
		||||
  <gml:boundedBy>
 | 
			
		||||
    <gml:Box>
 | 
			
		||||
      <gml:coord><gml:X>20</gml:X><gml:Y>5</gml:Y></gml:coord>
 | 
			
		||||
      <gml:coord><gml:X>31</gml:X><gml:Y>20.5</gml:Y></gml:coord>
 | 
			
		||||
    </gml:Box>
 | 
			
		||||
  </gml:boundedBy>
 | 
			
		||||
                                                                                                                                                           
 | 
			
		||||
  <gml:featureMember>
 | 
			
		||||
    <ogr:snap_geometries fid="snap_geometries.0">
 | 
			
		||||
      <ogr:geometryProperty><gml:Polygon srsName="EPSG:3857"><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates>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</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs></gml:Polygon></ogr:geometryProperty>
 | 
			
		||||
    </ogr:snap_geometries>
 | 
			
		||||
  </gml:featureMember>
 | 
			
		||||
  <gml:featureMember>
 | 
			
		||||
    <ogr:snap_geometries fid="snap_geometries.1">
 | 
			
		||||
      <ogr:geometryProperty><gml:Polygon srsName="EPSG:3857"><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates>31,10 30.0,10.123 21.0,10.123 20.123,10.123 20,5 31,10</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs></gml:Polygon></ogr:geometryProperty>
 | 
			
		||||
    </ogr:snap_geometries>
 | 
			
		||||
  </gml:featureMember>
 | 
			
		||||
</ogr:FeatureCollection>
 | 
			
		||||
							
								
								
									
										23
									
								
								python/plugins/processing/tests/testdata/expected/snap_geometries.xsd
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								python/plugins/processing/tests/testdata/expected/snap_geometries.xsd
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,23 @@
 | 
			
		||||
<?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
<xs:schema targetNamespace="http://ogr.maptools.org/" xmlns:ogr="http://ogr.maptools.org/" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:gml="http://www.opengis.net/gml" elementFormDefault="qualified" version="1.0">
 | 
			
		||||
<xs:import namespace="http://www.opengis.net/gml" schemaLocation="http://schemas.opengis.net/gml/2.1.2/feature.xsd"/>
 | 
			
		||||
<xs:element name="FeatureCollection" type="ogr:FeatureCollectionType" substitutionGroup="gml:_FeatureCollection"/>
 | 
			
		||||
<xs:complexType name="FeatureCollectionType">
 | 
			
		||||
  <xs:complexContent>
 | 
			
		||||
    <xs:extension base="gml:AbstractFeatureCollectionType">
 | 
			
		||||
      <xs:attribute name="lockId" type="xs:string" use="optional"/>
 | 
			
		||||
      <xs:attribute name="scope" type="xs:string" use="optional"/>
 | 
			
		||||
    </xs:extension>
 | 
			
		||||
  </xs:complexContent>
 | 
			
		||||
</xs:complexType>
 | 
			
		||||
<xs:element name="snap_geometries" type="ogr:snap_geometries_Type" substitutionGroup="gml:_Feature"/>
 | 
			
		||||
<xs:complexType name="snap_geometries_Type">
 | 
			
		||||
  <xs:complexContent>
 | 
			
		||||
    <xs:extension base="gml:AbstractFeatureType">
 | 
			
		||||
      <xs:sequence>
 | 
			
		||||
        <xs:element name="geometryProperty" type="gml:PolygonPropertyType" nillable="true" minOccurs="0" maxOccurs="1"/>
 | 
			
		||||
      </xs:sequence>
 | 
			
		||||
    </xs:extension>
 | 
			
		||||
  </xs:complexContent>
 | 
			
		||||
</xs:complexType>
 | 
			
		||||
</xs:schema>
 | 
			
		||||
@ -2045,6 +2045,22 @@ tests:
 | 
			
		||||
        name: expected/snap_internal.gml
 | 
			
		||||
        type: vector
 | 
			
		||||
 | 
			
		||||
  - algorithm: qgis:snapgeometries
 | 
			
		||||
    name: Test Snap Geometries (to each other)
 | 
			
		||||
    params:
 | 
			
		||||
      BEHAVIOR: '7'
 | 
			
		||||
      INPUT:
 | 
			
		||||
        name: custom/snap_geometries.geojson
 | 
			
		||||
        type: vector
 | 
			
		||||
      REFERENCE_LAYER:
 | 
			
		||||
        name: custom/snap_geometries.geojson
 | 
			
		||||
        type: vector
 | 
			
		||||
      TOLERANCE: 0.5
 | 
			
		||||
    results:
 | 
			
		||||
      OUTPUT:
 | 
			
		||||
        name: expected/snap_geometries.gml
 | 
			
		||||
        type: vector
 | 
			
		||||
 | 
			
		||||
  - algorithm: qgis:poleofinaccessibility
 | 
			
		||||
    name: Pole of inaccessibility (polygons)
 | 
			
		||||
    params:
 | 
			
		||||
 | 
			
		||||
@ -123,6 +123,7 @@ SET(QGIS_ANALYSIS_SRCS
 | 
			
		||||
  raster/qgsrastermatrix.cpp
 | 
			
		||||
  vector/mersenne-twister.cpp
 | 
			
		||||
  vector/qgsgeometrysnapper.cpp
 | 
			
		||||
  vector/qgsgeometrysnappersinglesource.cpp
 | 
			
		||||
  vector/qgszonalstatistics.cpp
 | 
			
		||||
 | 
			
		||||
  network/qgsgraph.cpp
 | 
			
		||||
@ -231,6 +232,7 @@ SET(QGIS_ANALYSIS_HDRS
 | 
			
		||||
 | 
			
		||||
  vector/mersenne-twister.h
 | 
			
		||||
  vector/qgsgeometrysnapper.h
 | 
			
		||||
  vector/qgsgeometrysnappersinglesource.h
 | 
			
		||||
  vector/qgszonalstatistics.h
 | 
			
		||||
  vector/geometry_checker/qgsgeometrycheckerutils.h
 | 
			
		||||
  vector/geometry_checker/qgsfeaturepool.h
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										356
									
								
								src/analysis/vector/qgsgeometrysnappersinglesource.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										356
									
								
								src/analysis/vector/qgsgeometrysnappersinglesource.cpp
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,356 @@
 | 
			
		||||
/***************************************************************************
 | 
			
		||||
  qgsgeometrysnappersinglesource.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 "qgsgeometrysnappersinglesource.h"
 | 
			
		||||
 | 
			
		||||
#include "qgsfeatureiterator.h"
 | 
			
		||||
#include "qgsfeaturesink.h"
 | 
			
		||||
#include "qgsfeaturesource.h"
 | 
			
		||||
#include "qgsfeedback.h"
 | 
			
		||||
#include "qgsgeometrycollection.h"
 | 
			
		||||
#include "qgsgeometryutils.h"
 | 
			
		||||
#include "qgslinestring.h"
 | 
			
		||||
#include "qgspolygon.h"
 | 
			
		||||
#include "qgsspatialindex.h"
 | 
			
		||||
 | 
			
		||||
//! record about vertex coordinates and index of anchor to which it is snapped
 | 
			
		||||
struct AnchorPoint
 | 
			
		||||
{
 | 
			
		||||
  //! 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;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
//! record about anchor being along a segment
 | 
			
		||||
struct AnchorAlongSegment
 | 
			
		||||
{
 | 
			
		||||
  int anchor;    //!< Index of the anchor point
 | 
			
		||||
  double along;  //!< Distance of the anchor point along the segment
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
static void buildSnapIndex( QgsFeatureIterator &fi, QgsSpatialIndex &index, QVector<AnchorPoint> &pnts, QgsFeedback *feedback, int &count, int totalCount )
 | 
			
		||||
{
 | 
			
		||||
  QgsFeature f;
 | 
			
		||||
  int pntId = 0;
 | 
			
		||||
 | 
			
		||||
  while ( fi.nextFeature( f ) )
 | 
			
		||||
  {
 | 
			
		||||
    if ( feedback->isCanceled() )
 | 
			
		||||
      break;
 | 
			
		||||
 | 
			
		||||
    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<QgsFeatureId> 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<AnchorPoint> &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<QgsFeatureId> 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<AnchorPoint> &pnts )
 | 
			
		||||
{
 | 
			
		||||
  // Find point ( should always find one point )
 | 
			
		||||
  QList<QgsFeatureId> 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<AnchorPoint> &pnts, double thresh )
 | 
			
		||||
{
 | 
			
		||||
  QVector<QgsPoint> newPoints;
 | 
			
		||||
  QVector<int> 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<QgsFeatureId> 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<QgsFeatureId> fids = index.intersects( rect );
 | 
			
		||||
 | 
			
		||||
    QVector<AnchorAlongSegment> 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<AnchorPoint> &pnts, double thresh )
 | 
			
		||||
{
 | 
			
		||||
  bool changed = false;
 | 
			
		||||
  if ( QgsLineString *linestring = qgsgeometry_cast<QgsLineString *>( g ) )
 | 
			
		||||
  {
 | 
			
		||||
    changed |= snapLineString( linestring, index, pnts, thresh );
 | 
			
		||||
  }
 | 
			
		||||
  else if ( QgsPolygon *polygon = qgsgeometry_cast<QgsPolygon *>( g ) )
 | 
			
		||||
  {
 | 
			
		||||
    if ( QgsLineString *exteriorRing = qgsgeometry_cast<QgsLineString *>( polygon->exteriorRing() ) )
 | 
			
		||||
      changed |= snapLineString( exteriorRing, index, pnts, thresh );
 | 
			
		||||
    for ( int i = 0; i < polygon->numInteriorRings(); ++i )
 | 
			
		||||
    {
 | 
			
		||||
      if ( QgsLineString *interiorRing = qgsgeometry_cast<QgsLineString *>( polygon->interiorRing( i ) ) )
 | 
			
		||||
        changed |= snapLineString( interiorRing, index, pnts, thresh );
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  else if ( QgsGeometryCollection *collection = qgsgeometry_cast<QgsGeometryCollection *>( g ) )
 | 
			
		||||
  {
 | 
			
		||||
    for ( int i = 0; i < collection->numGeometries(); ++i )
 | 
			
		||||
      changed |= snapGeometry( collection->geometryN( i ), index, pnts, thresh );
 | 
			
		||||
  }
 | 
			
		||||
  else if ( QgsPoint *pt = qgsgeometry_cast<QgsPoint *>( g ) )
 | 
			
		||||
  {
 | 
			
		||||
    changed |= snapPoint( pt, index, pnts );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return changed;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
int QgsGeometrySnapperSingleSource::run( const QgsFeatureSource &source, QgsFeatureSink &sink, double thresh, QgsFeedback *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<AnchorPoint> pnts;
 | 
			
		||||
  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
 | 
			
		||||
 | 
			
		||||
  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 ) )
 | 
			
		||||
  {
 | 
			
		||||
    if ( feedback->isCanceled() )
 | 
			
		||||
      break;
 | 
			
		||||
 | 
			
		||||
    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 );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return modified;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										54
									
								
								src/analysis/vector/qgsgeometrysnappersinglesource.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								src/analysis/vector/qgsgeometrysnappersinglesource.h
									
									
									
									
									
										Normal file
									
								
							@ -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
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user