Skip to content

Commit

Permalink
[FEATURE][Processing] Minimal enclosing circle
Browse files Browse the repository at this point in the history
  • Loading branch information
lbartoletti authored and nyalldawson committed Sep 2, 2017
1 parent f51244c commit e30f704
Show file tree
Hide file tree
Showing 15 changed files with 595 additions and 3 deletions.
18 changes: 18 additions & 0 deletions python/core/geometry/qgscircle.sip
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,19 @@ class QgsCircle : QgsEllipse
:rtype: QgsCircle
%End

static QgsCircle minimalCircleFrom3Points( const QgsPoint &pt1, const QgsPoint &pt2, const QgsPoint &pt3, double epsilon = 1E-8 );
%Docstring
Constructs the smallest circle from 3 points.
Z and m values are dropped for the center point.
The azimuth always takes the default value.
If the points are colinear an empty circle is returned.
\param pt1 First point.
\param pt2 Second point.
\param pt3 Third point.
\param epsilon Value used to compare point.
:rtype: QgsCircle
%End

virtual double area() const;

virtual double perimeter() const;
Expand Down Expand Up @@ -159,6 +172,11 @@ Set the radius of the circle
:rtype: QgsCircularString
%End

bool contains( const QgsPoint &point, double epsilon = 1E-8 ) const;
%Docstring
Returns true if the circle contains the ``point``.
:rtype: bool
%End

virtual QgsRectangle boundingBox() const;

Expand Down
10 changes: 10 additions & 0 deletions python/core/geometry/qgsgeometry.sip
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,16 @@ Returns true if WKB of the geometry is of WKBMulti* type
:rtype: QgsGeometry
%End

QgsGeometry minimalEnclosingCircle( QgsPointXY &center /Out/, double &radius /Out/, unsigned int segments = 36 ) const;
%Docstring
Returns the minimal enclosing circle for the geometry.
\param center Center of the minimal enclosing circle returneds
\param radius Radius of the minimal enclosing circle returned
.. seealso:: QgsEllipse.toPolygon()
.. versionadded:: 3.0
:rtype: QgsGeometry
%End

QgsGeometry orthogonalize( double tolerance = 1.0E-8, int maxIterations = 1000, double angleThreshold = 15.0 ) const;
%Docstring
Attempts to orthogonalize a line or polygon geometry by shifting vertices to make the geometries
Expand Down
5 changes: 5 additions & 0 deletions python/plugins/processing/algs/help/qgis.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,11 @@ qgis:orientedminimumboundingbox: >

As an alternative, the output layer can contain not just a single rectangle, but one for each input feature, representing the minimum rectangle that covers each of them.

qgis:minimalenclosingcircle: >
This algorithm takes a vector layer and generate a new one with the minimum enclosing circle that covers all the input features.

As an alternative, the output layer can contain not just a single circle, but one for each input feature, representing the minimum enclosing circle that covers each of them.

qgis:orthogonalize: >
This algorithm takes a line or polygon layer and attempts to orthogonalize all the geometries in the layer. This process shifts the nodes in the geometries to try to make every angle in the geometry either a right angle or a straight line.

Expand Down
142 changes: 142 additions & 0 deletions python/plugins/processing/algs/qgis/MinimalEnclosingCircle.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# -*- coding: utf-8 -*-

"""
***************************************************************************
MinimalEnclosingCircle.py
---------------------
Date : September 2017
Copyright : (C) 2017, Loïc BARTOLETTI
Email : lbartoletti at tuxfamily dot org
***************************************************************************
* *
* 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. *
* *
***************************************************************************
"""

__author__ = 'Loïc BARTOLETTI'
__date__ = 'September 2017'
__copyright__ = '(C) 2017, Loïc BARTOLETTI'

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

__revision__ = '$Format:%H$'

from qgis.PyQt.QtCore import QVariant
from qgis.core import (QgsField,
QgsFields,
QgsFeatureSink,
QgsGeometry,
QgsFeature,
QgsWkbTypes,
QgsFeatureRequest,
QgsProcessing,
QgsProcessingParameterFeatureSource,
QgsProcessingParameterBoolean,
QgsProcessingParameterFeatureSink,
QgsProcessingException)
from processing.algs.qgis.QgisAlgorithm import QgisAlgorithm


class MinimalEnclosingCircle(QgisAlgorithm):

INPUT = 'INPUT'
BY_FEATURE = 'BY_FEATURE'

OUTPUT = 'OUTPUT'

def group(self):
return self.tr('Vector general')

def __init__(self):
super().__init__()

def initAlgorithm(self, config=None):
self.addParameter(QgsProcessingParameterFeatureSource(self.INPUT,
self.tr('Input layer'), [QgsProcessing.TypeVectorAnyGeometry]))
self.addParameter(QgsProcessingParameterBoolean(self.BY_FEATURE,
self.tr('Calculate bounds for each feature separately'), defaultValue=True))

self.addParameter(QgsProcessingParameterFeatureSink(self.OUTPUT, self.tr('Enclosing circles'), QgsProcessing.TypeVectorPolygon))

def name(self):
return 'minimalenclosingcircle'

def displayName(self):
return self.tr('Minimal enclosing circle')

def processAlgorithm(self, parameters, context, feedback):
source = self.parameterAsSource(parameters, self.INPUT, context)
by_feature = self.parameterAsBool(parameters, self.BY_FEATURE, context)

if not by_feature and QgsWkbTypes.geometryType(source.wkbType()) == QgsWkbTypes.PointGeometry and source.featureCount() <= 2:
raise QgsProcessingException(self.tr("Can't calculate a minimal enclosing circle for each point, it's a point. The number of points must be greater than 2"))

if by_feature:
fields = source.fields()
else:
fields = QgsFields()
fields.append(QgsField('center_x', QVariant.Double))
fields.append(QgsField('center_y', QVariant.Double))
fields.append(QgsField('radius', QVariant.Double))

(sink, dest_id) = self.parameterAsSink(parameters, self.OUTPUT, context,
fields, QgsWkbTypes.Polygon, source.sourceCrs())

if by_feature:
self.featureMec(source, context, sink, feedback)
else:
self.layerMec(source, context, sink, feedback)

return {self.OUTPUT: dest_id}

def layerMec(self, source, context, sink, feedback):
req = QgsFeatureRequest().setSubsetOfAttributes([])
features = source.getFeatures(req)
total = 100.0 / source.featureCount() if source.featureCount() else 0
newgeometry = QgsGeometry()
first = True
geometries = []
for current, inFeat in enumerate(features):
if feedback.isCanceled():
break

if inFeat.hasGeometry():
geometries.append(inFeat.geometry())
feedback.setProgress(int(current * total))

newgeometry = QgsGeometry.unaryUnion(geometries)
geometry, center, radius = newgeometry.minimalEnclosingCircle()

if geometry:
outFeat = QgsFeature()

outFeat.setGeometry(geometry)
outFeat.setAttributes([center.x(),
center.y(),
radius])
sink.addFeature(outFeat, QgsFeatureSink.FastInsert)

def featureMec(self, source, context, sink, feedback):
features = source.getFeatures()
total = 100.0 / source.featureCount() if source.featureCount() else 0
outFeat = QgsFeature()
for current, inFeat in enumerate(features):
if feedback.isCanceled():
break

geometry, center, radius = inFeat.geometry().minimalEnclosingCircle()
if geometry:
outFeat.setGeometry(geometry)
attrs = inFeat.attributes()
attrs.extend([center.x(),
center.y(),
radius])
outFeat.setAttributes(attrs)
sink.addFeature(outFeat, QgsFeatureSink.FastInsert)
else:
feedback.pushInfo(self.tr("Can't calculate a minimal enclosing circle for feature {0}.").format(inFeat.id()))
feedback.setProgress(int(current * total))
2 changes: 2 additions & 0 deletions python/plugins/processing/algs/qgis/QGISAlgorithmProvider.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@
from .MeanCoords import MeanCoords
from .Merge import Merge
from .MergeLines import MergeLines
from .MinimalEnclosingCircle import MinimalEnclosingCircle
from .NearestNeighbourAnalysis import NearestNeighbourAnalysis
from .OffsetLine import OffsetLine
from .OrientedMinimumBoundingBox import OrientedMinimumBoundingBox
Expand Down Expand Up @@ -253,6 +254,7 @@ def getAlgs(self):
MeanCoords(),
Merge(),
MergeLines(),
MinimalEnclosingCircle(),
NearestNeighbourAnalysis(),
OffsetLine(),
OrientedMinimumBoundingBox(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<GMLFeatureClassList>
<GMLFeatureClass>
<Name>enclosing_circles_all</Name>
<ElementPath>enclosing_circles_all</ElementPath>
<!--POLYGON-->
<GeometryType>3</GeometryType>
<SRSName>EPSG:4326</SRSName>
<DatasetSpecificInfo>
<FeatureCount>1</FeatureCount>
<ExtentXMin>-1.50891</ExtentXMin>
<ExtentXMax>10.30638</ExtentXMax>
<ExtentYMin>-5.30638</ExtentYMin>
<ExtentYMax>6.50891</ExtentYMax>
</DatasetSpecificInfo>
<PropertyDefn>
<Name>center_x</Name>
<ElementPath>center_x</ElementPath>
<Type>Real</Type>
</PropertyDefn>
<PropertyDefn>
<Name>center_y</Name>
<ElementPath>center_y</ElementPath>
<Type>Real</Type>
</PropertyDefn>
<PropertyDefn>
<Name>radius</Name>
<ElementPath>radius</ElementPath>
<Type>Real</Type>
</PropertyDefn>
</GMLFeatureClass>
</GMLFeatureClassList>
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?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.508909716010908</gml:X><gml:Y>-5.306378070441288</gml:Y></gml:coord>
<gml:coord><gml:X>10.30637807044129</gml:X><gml:Y>6.508909716010908</gml:Y></gml:coord>
</gml:Box>
</gml:boundedBy>

<gml:featureMember>
<ogr:enclosing_circles_all fid="enclosing_circles_all.0">
<ogr:geometryProperty><gml:Polygon srsName="EPSG:4326"><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates>4.39873417721519,6.50890971601091 5.42458577357907,6.4191593308691 6.41926738829339,6.15263519548031 7.35255612382824,5.71743551083061 8.19609447422128,5.12678359911643 8.92425195354681,4.39862611979089 9.51490386526099,3.55508776939786 9.95010354991069,2.62179903386301 10.2166276852995,1.62711741914869 10.3063780704413,0.601265822784808 10.2166276852995,-0.424585773579072 9.95010354991069,-1.41926738829339 9.51490386526099,-2.35255612382824 8.92425195354681,-3.19609447422128 8.19609447422127,-3.92425195354681 7.35255612382824,-4.514903865261 6.41926738829339,-4.95010354991069 5.42458577357907,-5.21662768529948 4.39873417721519,-5.30637807044129 3.37288258085131,-5.21662768529948 2.37820096613699,-4.95010354991069 1.44491223060214,-4.51490386526099 0.601373880209104,-3.92425195354681 -0.126783599116428,-3.19609447422127 -0.717435510830614,-2.35255612382824 -1.15263519548031,-1.41926738829339 -1.4191593308691,-0.42458577357907 -1.50890971601091,0.60126582278481 -1.4191593308691,1.62711741914869 -1.15263519548031,2.62179903386301 -0.717435510830614,3.55508776939786 -0.126783599116427,4.3986261197909 0.601373880209104,5.12678359911643 1.44491223060214,5.71743551083061 2.37820096613699,6.15263519548031 3.37288258085131,6.4191593308691 4.39873417721519,6.50890971601091</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs></gml:Polygon></ogr:geometryProperty>
<ogr:center_x>4.39873417721519</ogr:center_x>
<ogr:center_y>0.60126582278481</ogr:center_y>
<ogr:radius>5.9076438932261</ogr:radius>
</ogr:enclosing_circles_all>
</gml:featureMember>
</ogr:FeatureCollection>
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<GMLFeatureClassList>
<GMLFeatureClass>
<Name>enclosing_circles_each</Name>
<ElementPath>enclosing_circles_each</ElementPath>
<!--POLYGON-->
<GeometryType>3</GeometryType>
<SRSName>EPSG:4326</SRSName>
<DatasetSpecificInfo>
<FeatureCount>6</FeatureCount>
<ExtentXMin>-1.81766</ExtentXMin>
<ExtentXMax>10.59982</ExtentXMax>
<ExtentYMin>-3.43247</ExtentYMin>
<ExtentYMax>6.32190</ExtentYMax>
</DatasetSpecificInfo>
<PropertyDefn>
<Name>intval</Name>
<ElementPath>intval</ElementPath>
<Type>Integer</Type>
</PropertyDefn>
<PropertyDefn>
<Name>floatval</Name>
<ElementPath>floatval</ElementPath>
<Type>Real</Type>
</PropertyDefn>
<PropertyDefn>
<Name>center_x</Name>
<ElementPath>center_x</ElementPath>
<Type>Real</Type>
</PropertyDefn>
<PropertyDefn>
<Name>center_y</Name>
<ElementPath>center_y</ElementPath>
<Type>Real</Type>
</PropertyDefn>
<PropertyDefn>
<Name>radius</Name>
<ElementPath>radius</ElementPath>
<Type>Real</Type>
</PropertyDefn>
<PropertyDefn>
<Name>name</Name>
<ElementPath>name</ElementPath>
<Type>String</Type>
<Width>5</Width>
</PropertyDefn>
</GMLFeatureClass>
</GMLFeatureClassList>
Loading

0 comments on commit e30f704

Please sign in to comment.