Skip to content

Commit

Permalink
[FEATURE][processing] Snap geometries to layer algorithm
Browse files Browse the repository at this point in the history
Port the Geometry Snapper plugin across to the analysis lib, and
expose to python bindings

Add a new algorithm which performs the snapping to layers
  • Loading branch information
nyalldawson committed Nov 7, 2016
1 parent b4bca5b commit c3a978b
Show file tree
Hide file tree
Showing 17 changed files with 1,271 additions and 1 deletion.
1 change: 1 addition & 0 deletions python/analysis/analysis.sip
Expand Up @@ -9,6 +9,7 @@
%Import core/core.sip %Import core/core.sip


%Include vector/qgsgeometryanalyzer.sip %Include vector/qgsgeometryanalyzer.sip
%Include vector/qgsgeometrysnapper.sip
%Include vector/qgsoverlayanalyzer.sip %Include vector/qgsoverlayanalyzer.sip
%Include vector/qgspointsample.sip %Include vector/qgspointsample.sip
%Include vector/qgstransectsample.sip %Include vector/qgstransectsample.sip
Expand Down
43 changes: 43 additions & 0 deletions python/analysis/vector/qgsgeometrysnapper.sip
@@ -0,0 +1,43 @@
/**
* \class QgsGeometrySnapper
* \ingroup analysis
* QgsGeometrySnapper allows a geometry to be snapped to the geometries within a
* different refence layer. Vertices in the geometries will be modified to
* match the reference layer features within a specified snap tolerance.
* \note added in QGIS 3.0
*/

class QgsGeometrySnapper : QObject
{
%TypeHeaderCode
#include <qgsgeometrysnapper.h>
%End

public:

/**
* Constructor for QgsGeometrySnapper. A reference layer which contains geometries to snap to must be
* set. The snap tolerance is specified in the layer units for the
* reference layer, and it is assumed that all geometries snapped using this object will have the
* same CRS as the reference layer (ie, no reprojection is performed).
*/
QgsGeometrySnapper( QgsVectorLayer* referenceLayer, double snapTolerance );

/**
* Snaps a geometry to the reference layer and returns the result. The geometry must be in the same
* CRS as the reference layer.
*/
QgsGeometry snapGeometry( const QgsGeometry& geometry ) const;

/**
* Snaps a set of features to the reference layer and returns the result. This operation is
* multithreaded for performance. The featureSnapped() signal will be emitted each time a feature
* is processed. This method is not safe to call from multiple threads concurrently.
*/
QgsFeatureList snapFeatures( const QgsFeatureList& features );

signals:

//! Emitted each time a feature has been processed when calling snapFeatures()
void featureSnapped();
};
3 changes: 3 additions & 0 deletions python/plugins/processing/algs/help/qgis.yaml
Expand Up @@ -481,6 +481,9 @@ qgis:smoothgeometry: >


The maximum angle parameter can be used to prevent smoothing of nodes with large angles. Any node where the angle of the segments to either side is larger then this will not be smoothed. Eg setting the maximum angle to 90 degrees or lower would preserve right angles in the geometry. The maximum angle parameter can be used to prevent smoothing of nodes with large angles. Any node where the angle of the segments to either side is larger then this will not be smoothed. Eg setting the maximum angle to 90 degrees or lower would preserve right angles in the geometry.


qgis:snapgeometriestolayer: >
This algorithm snaps the geometries in a layer to the geometries in another layer. A tolerance is specified in layer units to control how close vertices need to be to the reference layer geometries before they are snapped. Snapping occurs to both vertices and edges, but snapping to a vertex is preferred. Vertices will be inserted or removed as required to make the geometries match the reference geometries.

qgis:snappointstogrid: > qgis:snappointstogrid: >
This algorithm modifies the position of points in a vector layer, so they fall in the coordinates of a grid. This algorithm modifies the position of points in a vector layer, so they fall in the coordinates of a grid.


Expand Down
3 changes: 2 additions & 1 deletion python/plugins/processing/algs/qgis/QGISAlgorithmProvider.py
Expand Up @@ -175,6 +175,7 @@
from .ExtendLines import ExtendLines from .ExtendLines import ExtendLines
from .ExtractSpecificNodes import ExtractSpecificNodes from .ExtractSpecificNodes import ExtractSpecificNodes
from .GeometryByExpression import GeometryByExpression from .GeometryByExpression import GeometryByExpression
from .SnapGeometries import SnapGeometriesToLayer


pluginPath = os.path.normpath(os.path.join( pluginPath = os.path.normpath(os.path.join(
os.path.split(os.path.dirname(__file__))[0], os.pardir)) os.path.split(os.path.dirname(__file__))[0], os.pardir))
Expand Down Expand Up @@ -237,7 +238,7 @@ def __init__(self):
IdwInterpolationZValue(), IdwInterpolationAttribute(), IdwInterpolationZValue(), IdwInterpolationAttribute(),
TinInterpolationZValue(), TinInterpolationAttribute(), TinInterpolationZValue(), TinInterpolationAttribute(),
RemoveNullGeometry(), ExtractByExpression(), ExtendLines(), RemoveNullGeometry(), ExtractByExpression(), ExtendLines(),
ExtractSpecificNodes(), GeometryByExpression() ExtractSpecificNodes(), GeometryByExpression(), SnapGeometriesToLayer()
] ]


if hasMatplotlib: if hasMatplotlib:
Expand Down
83 changes: 83 additions & 0 deletions python/plugins/processing/algs/qgis/SnapGeometries.py
@@ -0,0 +1,83 @@
# -*- coding: utf-8 -*-

"""
***************************************************************************
SnapGeometries.py
-----------------
Date : October 2016
Copyright : (C) 2016 by Nyall Dawson
Email : nyall dot dawson 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. *
* *
***************************************************************************
"""
from builtins import str

__author__ = 'Nyall Dawson'
__date__ = 'October 2016'
__copyright__ = '(C) 2016, Nyall Dawson'

# This will get replaced with a git SHA1 when you do a git archive

__revision__ = '$Format:%H$'

from qgis.analysis import QgsGeometrySnapper
from qgis.core import QgsFeature

from processing.core.GeoAlgorithm import GeoAlgorithm
from processing.core.parameters import ParameterVector, ParameterNumber
from processing.core.GeoAlgorithmExecutionException import GeoAlgorithmExecutionException
from processing.core.outputs import OutputVector
from processing.tools import dataobjects, vector


class SnapGeometriesToLayer(GeoAlgorithm):

INPUT = 'INPUT'
REFERENCE_LAYER = 'REFERENCE_LAYER'
TOLERANCE = 'TOLERANCE'
OUTPUT = 'OUTPUT'

def defineCharacteristics(self):
self.name, self.i18n_name = self.trAlgorithm('Snap geometries to layer')
self.group, self.i18n_group = self.trAlgorithm('Vector geometry tools')

self.addParameter(ParameterVector(self.INPUT, self.tr('Input layer')))
self.addParameter(ParameterVector(self.REFERENCE_LAYER, self.tr('Reference layer')))
self.addParameter(ParameterNumber(self.TOLERANCE, self.tr('Tolerance (layer units)'), 0.00000001, 9999999999, default=10.0))
self.addOutput(OutputVector(self.OUTPUT, self.tr('Snapped geometries')))

def processAlgorithm(self, progress):
layer = dataobjects.getObjectFromUri(self.getParameterValue(self.INPUT))
reference_layer = dataobjects.getObjectFromUri(self.getParameterValue(self.REFERENCE_LAYER))
tolerance = self.getParameterValue(self.TOLERANCE)

if not layer.geometryType() == reference_layer.geometryType():
raise GeoAlgorithmExecutionException(
self.tr('Input layer and reference layer must have the same geometry type (eg both are line layers)'))

writer = self.getOutputFromName(self.OUTPUT).getVectorWriter(
layer.fields(), layer.wkbType(), layer.crs())

features = vector.features(layer)

self.processed = 0
self.progress = progress
self.total = 100.0 / len(features)

snapper = QgsGeometrySnapper(reference_layer, tolerance)
snapper.featureSnapped.connect(self.featureSnapped)
snapped_features = snapper.snapFeatures(features)
for f in snapped_features:
writer.addFeature(QgsFeature(f))

del writer

def featureSnapped(self):
self.processed += 1
self.progress.setPercentage(int(self.processed * self.total))
@@ -0,0 +1,14 @@
<GMLFeatureClassList>
<GMLFeatureClass>
<Name>snap_lines_to_lines</Name>
<ElementPath>snap_lines_to_lines</ElementPath>
<SRSName>EPSG:4326</SRSName>
<DatasetSpecificInfo>
<FeatureCount>7</FeatureCount>
<ExtentXMin>-1.00000</ExtentXMin>
<ExtentXMax>11.34679</ExtentXMax>
<ExtentYMin>-5.00000</ExtentYMin>
<ExtentYMax>5.28899</ExtentYMax>
</DatasetSpecificInfo>
</GMLFeatureClass>
</GMLFeatureClassList>
@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8" ?>
<ogr:FeatureCollection
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation=""
xmlns:ogr="http://ogr.maptools.org/"
xmlns:gml="http://www.opengis.net/gml">
<gml:boundedBy>
<gml:Box>
<gml:coord><gml:X>-1</gml:X><gml:Y>-5</gml:Y></gml:coord>
<gml:coord><gml:X>11.34678899082569</gml:X><gml:Y>5.288990825688074</gml:Y></gml:coord>
</gml:Box>
</gml:boundedBy>

<gml:featureMember>
<ogr:snap_lines_to_lines fid="lines.0">
<ogr:geometryProperty><gml:LineString srsName="EPSG:4326"><gml:coordinates>6,2 9,2 9,3 11,5 11.346788990825686,5.288990825688074</gml:coordinates></gml:LineString></ogr:geometryProperty>
</ogr:snap_lines_to_lines>
</gml:featureMember>
<gml:featureMember>
<ogr:snap_lines_to_lines fid="lines.1">
<ogr:geometryProperty><gml:LineString srsName="EPSG:4326"><gml:coordinates>-1,-1 1,-1</gml:coordinates></gml:LineString></ogr:geometryProperty>
</ogr:snap_lines_to_lines>
</gml:featureMember>
<gml:featureMember>
<ogr:snap_lines_to_lines fid="lines.2">
<ogr:geometryProperty><gml:LineString srsName="EPSG:4326"><gml:coordinates>2,0 2,2 2,2 3,2 3,2</gml:coordinates></gml:LineString></ogr:geometryProperty>
</ogr:snap_lines_to_lines>
</gml:featureMember>
<gml:featureMember>
<ogr:snap_lines_to_lines fid="lines.3">
<ogr:geometryProperty><gml:LineString srsName="EPSG:4326"><gml:coordinates>3,-3 3,-5</gml:coordinates></gml:LineString></ogr:geometryProperty>
</ogr:snap_lines_to_lines>
</gml:featureMember>
<gml:featureMember>
<ogr:snap_lines_to_lines fid="lines.4">
<ogr:geometryProperty><gml:LineString srsName="EPSG:4326"><gml:coordinates>7,-3 10,-3</gml:coordinates></gml:LineString></ogr:geometryProperty>
</ogr:snap_lines_to_lines>
</gml:featureMember>
<gml:featureMember>
<ogr:snap_lines_to_lines fid="lines.5">
<ogr:geometryProperty><gml:LineString srsName="EPSG:4326"><gml:coordinates>6,-3 10,1 10.208073394495411,0.849724770642202</gml:coordinates></gml:LineString></ogr:geometryProperty>
</ogr:snap_lines_to_lines>
</gml:featureMember>
<gml:featureMember>
<ogr:snap_lines_to_lines fid="lines.6">
</ogr:snap_lines_to_lines>
</gml:featureMember>
</ogr:FeatureCollection>
@@ -0,0 +1,32 @@
<GMLFeatureClassList>
<GMLFeatureClass>
<Name>snap_polys_to_polys</Name>
<ElementPath>snap_polys_to_polys</ElementPath>
<!--POLYGON-->
<GeometryType>3</GeometryType>
<SRSName>EPSG:4326</SRSName>
<DatasetSpecificInfo>
<FeatureCount>4</FeatureCount>
<ExtentXMin>-1.00000</ExtentXMin>
<ExtentXMax>10.00000</ExtentXMax>
<ExtentYMin>-3.00000</ExtentYMin>
<ExtentYMax>5.00000</ExtentYMax>
</DatasetSpecificInfo>
<PropertyDefn>
<Name>name</Name>
<ElementPath>name</ElementPath>
<Type>String</Type>
<Width>5</Width>
</PropertyDefn>
<PropertyDefn>
<Name>intval</Name>
<ElementPath>intval</ElementPath>
<Type>Integer</Type>
</PropertyDefn>
<PropertyDefn>
<Name>floatval</Name>
<ElementPath>floatval</ElementPath>
<Type>Real</Type>
</PropertyDefn>
</GMLFeatureClass>
</GMLFeatureClassList>
@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8" ?>
<ogr:FeatureCollection
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation=""
xmlns:ogr="http://ogr.maptools.org/"
xmlns:gml="http://www.opengis.net/gml">
<gml:boundedBy>
<gml:Box>
<gml:coord><gml:X>-1</gml:X><gml:Y>-3</gml:Y></gml:coord>
<gml:coord><gml:X>10</gml:X><gml:Y>5</gml:Y></gml:coord>
</gml:Box>
</gml:boundedBy>

<gml:featureMember>
<ogr:snap_polys_to_polys fid="polys.0">
<ogr:geometryProperty><gml:Polygon srsName="EPSG:4326"><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates>-1,-1 -1,3 3,3 3,2 2,2 2,2 2,2 2,-1 -1,-1</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs></gml:Polygon></ogr:geometryProperty>
<ogr:name>aaaaa</ogr:name>
<ogr:intval>33</ogr:intval>
<ogr:floatval>44.123456</ogr:floatval>
</ogr:snap_polys_to_polys>
</gml:featureMember>
<gml:featureMember>
<ogr:snap_polys_to_polys fid="polys.1">
<ogr:geometryProperty><gml:Polygon srsName="EPSG:4326"><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates>5,5 6,4 4,4 5,5</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs></gml:Polygon></ogr:geometryProperty>
<ogr:name>Aaaaa</ogr:name>
<ogr:intval>-33</ogr:intval>
<ogr:floatval>0</ogr:floatval>
</ogr:snap_polys_to_polys>
</gml:featureMember>
<gml:featureMember>
<ogr:snap_polys_to_polys fid="polys.3">
<ogr:geometryProperty><gml:Polygon srsName="EPSG:4326"><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates>6,1 10,1 10,-3 6,-3 6,1</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs><gml:innerBoundaryIs><gml:LinearRing><gml:coordinates>7,0 7,-2 9,0 7,0</gml:coordinates></gml:LinearRing></gml:innerBoundaryIs></gml:Polygon></ogr:geometryProperty>
<ogr:name>ASDF</ogr:name>
<ogr:intval>0</ogr:intval>
</ogr:snap_polys_to_polys>
</gml:featureMember>
<gml:featureMember>
<ogr:snap_polys_to_polys fid="polys.4">
<ogr:intval>120</ogr:intval>
<ogr:floatval>-100291.43213</ogr:floatval>
</ogr:snap_polys_to_polys>
</gml:featureMember>
</ogr:FeatureCollection>
30 changes: 30 additions & 0 deletions python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml
Expand Up @@ -1387,3 +1387,33 @@ tests:
OUTPUT_LAYER: OUTPUT_LAYER:
name: expected/geometry_by_expression_line.gml name: expected/geometry_by_expression_line.gml
type: vector type: vector

- algorithm: qgis:snapgeometriestolayer
name: Snap lines to lines
params:
INPUT:
name: snap_lines.gml
type: vector
REFERENCE_LAYER:
name: lines.gml
type: vector
TOLERANCE: 1.0
results:
OUTPUT:
name: expected/snap_lines_to_lines.gml
type: vector

- algorithm: qgis:snapgeometriestolayer
name: Snap polygons to polygons
params:
INPUT:
name: snap_polys.gml
type: vector
REFERENCE_LAYER:
name: polys.gml
type: vector
TOLERANCE: 1.0
results:
OUTPUT:
name: expected/snap_polys_to_polys.gml
type: vector
16 changes: 16 additions & 0 deletions python/plugins/processing/tests/testdata/snap_lines.gfs
@@ -0,0 +1,16 @@
<GMLFeatureClassList>
<GMLFeatureClass>
<Name>snap_lines</Name>
<ElementPath>snap_lines</ElementPath>
<!--LINESTRING-->
<GeometryType>2</GeometryType>
<SRSName>EPSG:4326</SRSName>
<DatasetSpecificInfo>
<FeatureCount>7</FeatureCount>
<ExtentXMin>-1.00000</ExtentXMin>
<ExtentXMax>11.34679</ExtentXMax>
<ExtentYMin>-5.00000</ExtentYMin>
<ExtentYMax>5.28899</ExtentYMax>
</DatasetSpecificInfo>
</GMLFeatureClass>
</GMLFeatureClassList>

0 comments on commit c3a978b

Please sign in to comment.