From e30f7044c9af22096dda8722d188d452228d420c Mon Sep 17 00:00:00 2001 From: lbartoletti Date: Sat, 2 Sep 2017 23:37:53 +0200 Subject: [PATCH] [FEATURE][Processing] Minimal enclosing circle --- python/core/geometry/qgscircle.sip | 18 +++ python/core/geometry/qgsgeometry.sip | 10 ++ python/plugins/processing/algs/help/qgis.yaml | 5 + .../algs/qgis/MinimalEnclosingCircle.py | 142 ++++++++++++++++++ .../algs/qgis/QGISAlgorithmProvider.py | 2 + .../expected/enclosing_circles_all.gfs | 31 ++++ .../expected/enclosing_circles_all.gml | 22 +++ .../expected/enclosing_circles_each.gfs | 47 ++++++ .../expected/enclosing_circles_each.gml | 77 ++++++++++ .../tests/testdata/qgis_algorithm_tests.yaml | 30 ++++ src/core/geometry/qgscircle.cpp | 23 ++- src/core/geometry/qgscircle.h | 14 ++ src/core/geometry/qgsgeometry.cpp | 76 ++++++++++ src/core/geometry/qgsgeometry.h | 9 ++ tests/src/core/testqgsgeometry.cpp | 92 +++++++++++- 15 files changed, 595 insertions(+), 3 deletions(-) create mode 100644 python/plugins/processing/algs/qgis/MinimalEnclosingCircle.py create mode 100644 python/plugins/processing/tests/testdata/expected/enclosing_circles_all.gfs create mode 100644 python/plugins/processing/tests/testdata/expected/enclosing_circles_all.gml create mode 100644 python/plugins/processing/tests/testdata/expected/enclosing_circles_each.gfs create mode 100644 python/plugins/processing/tests/testdata/expected/enclosing_circles_each.gml diff --git a/python/core/geometry/qgscircle.sip b/python/core/geometry/qgscircle.sip index 29dbf3dd767..b88835e6e69 100644 --- a/python/core/geometry/qgscircle.sip +++ b/python/core/geometry/qgscircle.sip @@ -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; @@ -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; diff --git a/python/core/geometry/qgsgeometry.sip b/python/core/geometry/qgsgeometry.sip index ce610614ffa..b575a037630 100644 --- a/python/core/geometry/qgsgeometry.sip +++ b/python/core/geometry/qgsgeometry.sip @@ -577,6 +577,16 @@ Returns true if WKB of the geometry is of WKBMulti* type :rtype: QgsGeometry %End + QgsGeometry minimalEnclosingCircle( QgsPointXY ¢er /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 diff --git a/python/plugins/processing/algs/help/qgis.yaml b/python/plugins/processing/algs/help/qgis.yaml index e2afbfd3174..9fa8531bd82 100755 --- a/python/plugins/processing/algs/help/qgis.yaml +++ b/python/plugins/processing/algs/help/qgis.yaml @@ -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. diff --git a/python/plugins/processing/algs/qgis/MinimalEnclosingCircle.py b/python/plugins/processing/algs/qgis/MinimalEnclosingCircle.py new file mode 100644 index 00000000000..a31e8b48526 --- /dev/null +++ b/python/plugins/processing/algs/qgis/MinimalEnclosingCircle.py @@ -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)) diff --git a/python/plugins/processing/algs/qgis/QGISAlgorithmProvider.py b/python/plugins/processing/algs/qgis/QGISAlgorithmProvider.py index 9a8539cd641..1a532f004dc 100644 --- a/python/plugins/processing/algs/qgis/QGISAlgorithmProvider.py +++ b/python/plugins/processing/algs/qgis/QGISAlgorithmProvider.py @@ -100,6 +100,7 @@ from .LinesToPolygons import LinesToPolygons 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 @@ -253,6 +254,7 @@ class QGISAlgorithmProvider(QgsProcessingProvider): MeanCoords(), Merge(), MergeLines(), + MinimalEnclosingCircle(), NearestNeighbourAnalysis(), OffsetLine(), OrientedMinimumBoundingBox(), diff --git a/python/plugins/processing/tests/testdata/expected/enclosing_circles_all.gfs b/python/plugins/processing/tests/testdata/expected/enclosing_circles_all.gfs new file mode 100644 index 00000000000..eaa4f1732da --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/enclosing_circles_all.gfs @@ -0,0 +1,31 @@ + + + enclosing_circles_all + enclosing_circles_all + + 3 + EPSG:4326 + + 1 + -1.50891 + 10.30638 + -5.30638 + 6.50891 + + + center_x + center_x + Real + + + center_y + center_y + Real + + + radius + radius + Real + + + diff --git a/python/plugins/processing/tests/testdata/expected/enclosing_circles_all.gml b/python/plugins/processing/tests/testdata/expected/enclosing_circles_all.gml new file mode 100644 index 00000000000..fb9e4694a96 --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/enclosing_circles_all.gml @@ -0,0 +1,22 @@ + + + + + -1.508909716010908-5.306378070441288 + 10.306378070441296.508909716010908 + + + + + + 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 + 4.39873417721519 + 0.60126582278481 + 5.9076438932261 + + + diff --git a/python/plugins/processing/tests/testdata/expected/enclosing_circles_each.gfs b/python/plugins/processing/tests/testdata/expected/enclosing_circles_each.gfs new file mode 100644 index 00000000000..9d8dc0c421c --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/enclosing_circles_each.gfs @@ -0,0 +1,47 @@ + + + enclosing_circles_each + enclosing_circles_each + + 3 + EPSG:4326 + + 6 + -1.81766 + 10.59982 + -3.43247 + 6.32190 + + + intval + intval + Integer + + + floatval + floatval + Real + + + center_x + center_x + Real + + + center_y + center_y + Real + + + radius + radius + Real + + + name + name + String + 5 + + + diff --git a/python/plugins/processing/tests/testdata/expected/enclosing_circles_each.gml b/python/plugins/processing/tests/testdata/expected/enclosing_circles_each.gml new file mode 100644 index 00000000000..0a23fc8539b --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/enclosing_circles_each.gml @@ -0,0 +1,77 @@ + + + + + -1.817664105611035-3.43247280352443 + 10.599819742299946.321903675995209 + + + + + + 3.8,2.2 4.26819673362059,2.35151315326536 4.75559048978224,2.4194229717016 5.24737205583712,2.40166604983954 5.72859889775413,2.29878192276454 6.18464918145415,2.11389667261588 6.60166604983954,1.85262794416288 6.96697865638513,1.52291425551124 7.26948716239812,1.13477379024745 7.5,0.7 7.65151315326536,0.23180326637941 7.7194229717016,-0.255590489782239 7.70166604983954,-0.747372055837116 7.59878192276454,-1.22859889775413 7.41389667261588,-1.68464918145415 7.15262794416288,-2.10166604983954 6.82291425551124,-2.46697865638513 6.43477379024745,-2.76948716239812 6.0,-3.0 5.53180326637941,-3.15151315326536 5.04440951021776,-3.2194229717016 4.55262794416288,-3.20166604983954 4.07140110224587,-3.09878192276454 3.61535081854585,-2.91389667261588 3.19833395016046,-2.65262794416288 2.83302134361487,-2.32291425551124 2.53051283760188,-1.93477379024745 2.3,-1.5 2.14848684673464,-1.03180326637941 2.0805770282984,-0.544409510217762 2.09833395016046,-0.052627944162882 2.20121807723546,0.428598897754127 2.38610332738412,0.884649181454149 2.64737205583712,1.30166604983954 2.97708574448876,1.66697865638513 3.36522620975255,1.96948716239812 3.8,2.2 + 120 + -100291.43213 + 4.9 + -0.4 + 2.82311884269862 + + + + + 5.0,5.08319489631876 5.17420296559052,5.06795411167699 5.34311286222252,5.02269484128082 5.50159744815938,4.94879226515894 5.64484124945447,4.8484918756903 5.7684918756903,4.72484124945447 5.86879226515894,4.58159744815938 5.94269484128082,4.42311286222252 5.98795411167699,4.25420296559052 6.00319489631876,4.08 5.98795411167699,3.90579703440948 5.94269484128082,3.73688713777748 5.86879226515894,3.57840255184062 5.7684918756903,3.43515875054553 5.64484124945447,3.3115081243097 5.50159744815938,3.21120773484106 5.34311286222252,3.13730515871918 5.17420296559052,3.09204588832301 5.0,3.07680510368124 4.82579703440948,3.09204588832301 4.65688713777748,3.13730515871918 4.49840255184062,3.21120773484106 4.35515875054553,3.3115081243097 4.2315081243097,3.43515875054553 4.13120773484106,3.57840255184062 4.05730515871918,3.73688713777748 4.01204588832301,3.90579703440948 3.99680510368124,4.08 4.01204588832301,4.25420296559052 4.05730515871918,4.42311286222252 4.13120773484106,4.58159744815938 4.2315081243097,4.72484124945447 4.35515875054553,4.8484918756903 4.49840255184062,4.94879226515894 4.65688713777748,5.02269484128082 4.82579703440948,5.06795411167699 5.0,5.08319489631876 + -33 + 0 + Aaaaa + 5 + 4.08 + 1.00319489631876 + + + + + 3.0,3.0 3.31691186135828,2.62231915069056 3.56342552822315,2.19534495492048 3.73205080756888,1.73205080756888 3.81766410561104,1.24651366686488 3.81766410561104,0.753486333135122 3.73205080756888,0.267949192431123 3.56342552822315,-0.195344954920481 3.31691186135828,-0.622319150690556 3.0,-1.0 2.62231915069056,-1.31691186135828 2.19534495492048,-1.56342552822315 1.73205080756888,-1.73205080756888 1.24651366686488,-1.81766410561103 0.753486333135123,-1.81766410561103 0.267949192431122,-1.73205080756888 -0.195344954920479,-1.56342552822315 -0.622319150690555,-1.31691186135828 -1.0,-1.0 -1.31691186135828,-0.622319150690556 -1.56342552822315,-0.195344954920479 -1.73205080756888,0.267949192431122 -1.81766410561103,0.753486333135123 -1.81766410561103,1.24651366686488 -1.73205080756888,1.73205080756888 -1.56342552822316,2.19534495492048 -1.31691186135828,2.62231915069056 -1.0,3.0 -0.622319150690556,3.31691186135828 -0.195344954920481,3.56342552822315 0.267949192431124,3.73205080756888 0.753486333135123,3.81766410561104 1.24651366686488,3.81766410561104 1.73205080756888,3.73205080756888 2.19534495492048,3.56342552822316 2.62231915069056,3.31691186135828 3.0,3.0 + 33 + 44.12346 + aaaaa + 1 + 1 + 2.82842712474619 + + + + + 7.8734693877551,2.02022790556525 8.3468951585034,1.97880851760375 8.80593612677252,1.85580886086324 9.23664456502752,1.65496621767295 9.62593361532103,1.38238309011494 9.96197492684963,1.04634177858634 10.2345580544076,0.657052728292829 10.4354006975979,0.226344290037822 10.5584003543384,-0.232696678231291 10.5998197422999,-0.706122448979591 10.5584003543384,-1.17954821972789 10.4354006975979,-1.638589187997 10.2345580544076,-2.06929762625201 9.96197492684963,-2.45858667654552 9.62593361532103,-2.79462798807412 9.23664456502752,-3.06721111563213 8.80593612677252,-3.26805375882242 8.3468951585034,-3.39105341556293 7.8734693877551,-3.43247280352443 7.4000436170068,-3.39105341556293 6.94100264873769,-3.26805375882242 6.51029421048268,-3.06721111563213 6.12100516018918,-2.79462798807412 5.78496384866057,-2.45858667654552 5.51238072110256,-2.06929762625201 5.31153807791227,-1.63858918799701 5.18853842117176,-1.17954821972789 5.14711903321026,-0.70612244897959 5.18853842117176,-0.23269667823129 5.31153807791227,0.226344290037823 5.51238072110256,0.65705272829283 5.78496384866057,1.04634177858634 6.12100516018918,1.38238309011494 6.51029421048268,1.65496621767295 6.94100264873769,1.85580886086324 7.4000436170068,1.97880851760375 7.8734693877551,2.02022790556525 + 0 + ASDF + 7.8734693877551 + -0.706122448979591 + 2.72635035454484 + + + + + 3,6 3.0935543337087,5.86933092744047 3.16299692054554,5.72440147214358 3.20621778264911,5.56961524227066 3.22190367599521,5.40967533909081 3.20957799265196,5.24944145562864 3.16961524227066,5.09378221735089 3.10322967279951,4.94742725144526 3.01243837617418,4.81482347949161 2.9,4.7 2.76933092744047,4.6064456662913 2.62440147214358,4.53700307945446 2.46961524227066,4.49378221735089 2.30967533909081,4.47809632400479 2.14944145562864,4.49042200734804 1.99378221735089,4.53038475772934 1.84742725144526,4.59677032720049 1.71482347949161,4.68756162382582 1.6,4.8 1.5064456662913,4.93066907255953 1.43700307945446,5.07559852785642 1.39378221735089,5.23038475772934 1.37809632400479,5.39032466090919 1.39042200734804,5.55055854437136 1.43038475772934,5.70621778264911 1.49677032720049,5.85257274855473 1.58756162382582,5.98517652050839 1.7,6.1 1.83066907255953,6.1935543337087 1.97559852785642,6.26299692054554 2.13038475772934,6.30621778264911 2.29032466090919,6.32190367599521 2.45055854437136,6.30957799265196 2.60621778264911,6.26961524227066 2.75257274855473,6.20322967279951 2.88517652050839,6.11243837617418 3,6 + 0.123 + bbaaa + 2.3 + 5.4 + 0.921954445729289 + + + + + 3.8,2.2 4.26819673362059,2.35151315326536 4.75559048978224,2.4194229717016 5.24737205583712,2.40166604983954 5.72859889775413,2.29878192276454 6.18464918145415,2.11389667261588 6.60166604983954,1.85262794416288 6.96697865638513,1.52291425551124 7.26948716239812,1.13477379024745 7.5,0.7 7.65151315326536,0.23180326637941 7.7194229717016,-0.255590489782239 7.70166604983954,-0.747372055837116 7.59878192276454,-1.22859889775413 7.41389667261588,-1.68464918145415 7.15262794416288,-2.10166604983954 6.82291425551124,-2.46697865638513 6.43477379024745,-2.76948716239812 6.0,-3.0 5.53180326637941,-3.15151315326536 5.04440951021776,-3.2194229717016 4.55262794416288,-3.20166604983954 4.07140110224587,-3.09878192276454 3.61535081854585,-2.91389667261588 3.19833395016046,-2.65262794416288 2.83302134361487,-2.32291425551124 2.53051283760188,-1.93477379024745 2.3,-1.5 2.14848684673464,-1.03180326637941 2.0805770282984,-0.544409510217762 2.09833395016046,-0.052627944162882 2.20121807723546,0.428598897754127 2.38610332738412,0.884649181454149 2.64737205583712,1.30166604983954 2.97708574448876,1.66697865638513 3.36522620975255,1.96948716239812 3.8,2.2 + 2 + 3.33 + elim + 4.9 + -0.4 + 2.82311884269862 + + + diff --git a/python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml b/python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml index 570ba2d95a3..f4e544cafe8 100644 --- a/python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml +++ b/python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml @@ -3239,3 +3239,33 @@ tests: OUTPUT: name: expected/raster_extent.gml type: vector + + - algorithm: qgis:minimalenclosingcircle + name: Minimal enclosing circle each features + params: + BY_FEATURE: true + INPUT: + name: custom/oriented_bbox.gml + type: vector + results: + OUTPUT: + name: expected/enclosing_circles_each.gml + type: vector + compare: + geometry: + precision: 7 + + - algorithm: qgis:minimalenclosingcircle + name: Minimal enclosing circle all features + params: + BY_FEATURE: false + INPUT: + name: custom/oriented_bbox.gml + type: vector + results: + OUTPUT: + name: expected/enclosing_circles_all.gml + type: vector + compare: + geometry: + precision: 7 diff --git a/src/core/geometry/qgscircle.cpp b/src/core/geometry/qgscircle.cpp index c65909d174d..2f3c3211236 100644 --- a/src/core/geometry/qgscircle.cpp +++ b/src/core/geometry/qgscircle.cpp @@ -38,7 +38,7 @@ QgsCircle QgsCircle::from2Points( const QgsPoint &pt1, const QgsPoint &pt2 ) { QgsPoint center = QgsGeometryUtils::midpoint( pt1, pt2 ); double azimuth = QgsGeometryUtils::lineAngle( pt1.x(), pt1.y(), pt2.x(), pt2.y() ) * 180.0 / M_PI; - double radius = pt1.distance( pt2 ); + double radius = pt1.distance( pt2 ) / 2.0; return QgsCircle( center, radius, azimuth ); } @@ -189,6 +189,22 @@ QgsCircle QgsCircle::from3Tangents( const QgsPoint &pt1_tg1, const QgsPoint &pt2 return QgsTriangle( p1, p2, p3 ).inscribedCircle(); } +QgsCircle QgsCircle::minimalCircleFrom3Points( const QgsPoint &pt1, const QgsPoint &pt2, const QgsPoint &pt3, double epsilon ) +{ + double l1 = pt2.distance( pt3 ); + double l2 = pt3.distance( pt1 ); + double l3 = pt1.distance( pt2 ); + + if ( ( l1 * l1 ) - ( l2 * l2 + l3 * l3 ) >= epsilon ) + return QgsCircle().from2Points( pt2, pt3 ); + else if ( ( l2 * l2 ) - ( l1 * l1 + l3 * l3 ) >= epsilon ) + return QgsCircle().from2Points( pt3, pt1 ); + else if ( ( l3 * l3 ) - ( l1 * l1 + l2 * l2 ) >= epsilon ) + return QgsCircle().from2Points( pt1, pt2 ); + else + return QgsCircle().from3Points( pt1, pt2, pt3, epsilon ); +} + QgsCircle QgsCircle::fromExtent( const QgsPoint &pt1, const QgsPoint &pt2 ) { double delta_x = std::fabs( pt1.x() - pt2.x() ); @@ -245,6 +261,11 @@ QgsCircularString *QgsCircle::toCircularString( bool oriented ) const return circString.release(); } +bool QgsCircle::contains( const QgsPoint &point, double epsilon ) const +{ + return ( mCenter.distance( point ) <= mSemiMajorAxis + epsilon ); +} + QgsRectangle QgsCircle::boundingBox() const { return QgsRectangle( mCenter.x() - mSemiMajorAxis, mCenter.y() - mSemiMajorAxis, mCenter.x() + mSemiMajorAxis, mCenter.y() + mSemiMajorAxis ); diff --git a/src/core/geometry/qgscircle.h b/src/core/geometry/qgscircle.h index b15b9d4572c..27fd07f67eb 100644 --- a/src/core/geometry/qgscircle.h +++ b/src/core/geometry/qgscircle.h @@ -119,6 +119,18 @@ class CORE_EXPORT QgsCircle : public QgsEllipse */ static QgsCircle fromExtent( const QgsPoint &pt1, const QgsPoint &pt2 ); + /** + * 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. + */ + static QgsCircle minimalCircleFrom3Points( const QgsPoint &pt1, const QgsPoint &pt2, const QgsPoint &pt3, double epsilon = 1E-8 ); + double area() const override; double perimeter() const override; @@ -168,6 +180,8 @@ class CORE_EXPORT QgsCircle : public QgsEllipse */ QgsCircularString *toCircularString( bool oriented = false ) const; + //! Returns true if the circle contains the \a point. + bool contains( const QgsPoint &point, double epsilon = 1E-8 ) const; QgsRectangle boundingBox() const override; diff --git a/src/core/geometry/qgsgeometry.cpp b/src/core/geometry/qgsgeometry.cpp index a4b48a2ce13..a96ee086d5d 100644 --- a/src/core/geometry/qgsgeometry.cpp +++ b/src/core/geometry/qgsgeometry.cpp @@ -44,6 +44,7 @@ email : morb at ozemail dot com dot au #include "qgspoint.h" #include "qgspolygon.h" #include "qgslinestring.h" +#include "qgscircle.h" struct QgsGeometryPrivate { @@ -1008,6 +1009,81 @@ QgsGeometry QgsGeometry::orientedMinimumBoundingBox( double &area, double &angle return minBounds; } +static QgsCircle __recMinimalEnclosingCircle( QgsMultiPoint points, QgsMultiPoint boundary ) +{ + auto l_boundary = boundary.length(); + QgsCircle circ_mec; + if ( ( points.length() == 0 ) || ( l_boundary == 3 ) ) + { + switch ( l_boundary ) + { + case 0: + circ_mec = QgsCircle(); + break; + case 1: + circ_mec = QgsCircle( QgsPoint( boundary.last() ), 0 ); + boundary.pop_back(); + break; + case 2: + { + QgsPointXY p1 = boundary.last(); + boundary.pop_back(); + QgsPointXY p2 = boundary.last(); + boundary.pop_back(); + circ_mec = QgsCircle().from2Points( QgsPoint( p1 ), QgsPoint( p2 ) ); + } + break; + default: + QgsPoint p1( boundary.at( 0 ) ); + QgsPoint p2( boundary.at( 1 ) ); + QgsPoint p3( boundary.at( 2 ) ); + circ_mec = QgsCircle().minimalCircleFrom3Points( p1, p2, p3 ); + break; + } + return circ_mec; + } + else + { + QgsPointXY pxy = points.last(); + points.pop_back(); + circ_mec = __recMinimalEnclosingCircle( points, boundary ); + QgsPoint p( pxy ); + if ( !circ_mec.contains( p ) ) + { + boundary.append( pxy ); + circ_mec = __recMinimalEnclosingCircle( points, boundary ); + } + } + return circ_mec; +} + +QgsGeometry QgsGeometry::minimalEnclosingCircle( QgsPointXY ¢er, double &radius, unsigned int segments ) const +{ + center = QgsPointXY( ); + radius = 0; + + if ( !d->geometry ) + { + return QgsGeometry(); + } + + /* optimization */ + QgsGeometry hull = convexHull(); + if ( hull.isNull() ) + return QgsGeometry(); + + QgsMultiPoint P = hull.convertToPoint( true ).asMultiPoint(); + QgsMultiPoint R; + + QgsCircle circ = __recMinimalEnclosingCircle( P, R ); + center = QgsPointXY( circ.center() ); + radius = circ.radius(); + QgsGeometry geom; + geom.setGeometry( circ.toPolygon( segments ) ); + return geom; + +} + QgsGeometry QgsGeometry::orthogonalize( double tolerance, int maxIterations, double angleThreshold ) const { QgsInternalGeometryEngine engine( *this ); diff --git a/src/core/geometry/qgsgeometry.h b/src/core/geometry/qgsgeometry.h index 0103cbc14ca..6c6cabae11c 100644 --- a/src/core/geometry/qgsgeometry.h +++ b/src/core/geometry/qgsgeometry.h @@ -599,6 +599,15 @@ class CORE_EXPORT QgsGeometry */ QgsGeometry orientedMinimumBoundingBox( double &area SIP_OUT, double &angle SIP_OUT, double &width SIP_OUT, double &height SIP_OUT ) const; + /** + * 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 + * \param segments Number of segments used to segment geometry. \see QgsEllipse::toPolygon() + * \since QGIS 3.0 + */ + QgsGeometry minimalEnclosingCircle( QgsPointXY ¢er SIP_OUT, double &radius SIP_OUT, unsigned int segments = 36 ) const; + /** * Attempts to orthogonalize a line or polygon geometry by shifting vertices to make the geometries * angles either right angles or flat lines. This is an iterative algorithm which will loop until diff --git a/tests/src/core/testqgsgeometry.cpp b/tests/src/core/testqgsgeometry.cpp index 5a8ef17b3e8..dfbdcf91bef 100644 --- a/tests/src/core/testqgsgeometry.cpp +++ b/tests/src/core/testqgsgeometry.cpp @@ -128,6 +128,8 @@ class TestQgsGeometry : public QObject void reshapeGeometryLineMerge(); void createCollectionOfType(); + void minimalEnclosingCircle( ); + private: //! A helper method to do a render check to see if the geometry op is as expected bool renderCheck( const QString &testName, const QString &comment = QLatin1String( QLatin1String( "" ) ), int mismatchCount = 0 ); @@ -4161,8 +4163,8 @@ void TestQgsGeometry::circle() //test "alt" constructors // by2Points - QVERIFY( QgsCircle().from2Points( QgsPoint( -5, 0 ), QgsPoint( 5, 0 ) ) == QgsCircle( QgsPoint( 0, 0 ), 10, 90 ) ); - QVERIFY( QgsCircle().from2Points( QgsPoint( 0, -5 ), QgsPoint( 0, 5 ) ) == QgsCircle( QgsPoint( 0, 0 ), 10, 0 ) ); + QVERIFY( QgsCircle().from2Points( QgsPoint( -5, 0 ), QgsPoint( 5, 0 ) ) == QgsCircle( QgsPoint( 0, 0 ), 5, 90 ) ); + QVERIFY( QgsCircle().from2Points( QgsPoint( 0, -5 ), QgsPoint( 0, 5 ) ) == QgsCircle( QgsPoint( 0, 0 ), 5, 0 ) ); // byExtent QVERIFY( QgsCircle().fromExtent( QgsPoint( -5, -5 ), QgsPoint( 5, 5 ) ) == QgsCircle( QgsPoint( 0, 0 ), 5, 0 ) ); QVERIFY( QgsCircle().fromExtent( QgsPoint( -7.5, -2.5 ), QgsPoint( 2.5, 200.5 ) ) == QgsCircle() ); @@ -4182,6 +4184,15 @@ void TestQgsGeometry::circle() QgsCircle circ_tgt = QgsCircle().from3Tangents( QgsPoint( 0, 0 ), QgsPoint( 0, 1 ), QgsPoint( 2, 0 ), QgsPoint( 3, 0 ), QgsPoint( 5, 0 ), QgsPoint( 0, 5 ) ); QGSCOMPARENEARPOINT( circ_tgt.center(), QgsPoint( 1.4645, 1.4645 ), 0.0001 ); QGSCOMPARENEAR( circ_tgt.radius(), 1.4645, 0.0001 ); + // minimalCircleFrom3points + QgsCircle minCircle3Points = QgsCircle().minimalCircleFrom3Points( QgsPoint( 0, 5 ), QgsPoint( 0, -5 ), QgsPoint( 1, 2 ) ); + QGSCOMPARENEARPOINT( minCircle3Points.center(), QgsPoint( 0, 0 ), 0.0001 ); + QGSCOMPARENEAR( minCircle3Points.radius(), 5.0, 0.0001 ); + minCircle3Points = QgsCircle().minimalCircleFrom3Points( QgsPoint( 0, 5 ), QgsPoint( 5, 0 ), QgsPoint( -5, 0 ) ); + QGSCOMPARENEARPOINT( minCircle3Points.center(), QgsPoint( 0, 0 ), 0.0001 ); + QGSCOMPARENEAR( minCircle3Points.radius(), 5.0, 0.0001 ); + + // test quadrant QVector quad = QgsCircle( QgsPoint( 0, 0 ), 5 ).northQuadrant(); @@ -4286,6 +4297,15 @@ void TestQgsGeometry::circle() QGSCOMPARENEAR( 314.1593, QgsCircle( QgsPoint( 0, 0 ), 10 ).area(), 0.0001 ); // perimeter QGSCOMPARENEAR( 31.4159, QgsCircle( QgsPoint( 0, 0 ), 5 ).perimeter(), 0.0001 ); + + // contains + QgsPoint pc; + pc = QgsPoint( 1, 1 ); + QVERIFY( QgsCircle( QgsPoint( 0, 0 ), 5 ).contains( pc ) ); + pc = QgsPoint( 0, 5 ); + QVERIFY( QgsCircle( QgsPoint( 0, 0 ), 5 ).contains( pc ) ); + pc = QgsPoint( 6, 1 ); + QVERIFY( !QgsCircle( QgsPoint( 0, 0 ), 5 ).contains( pc ) ); } void TestQgsGeometry::regularPolygon() @@ -5577,5 +5597,73 @@ void TestQgsGeometry::createCollectionOfType() QVERIFY( dynamic_cast< QgsMultiSurface *>( collect.get() ) ); } +void TestQgsGeometry::minimalEnclosingCircle() +{ + QgsGeometry geomTest; + QgsGeometry result, resultTest; + QgsPointXY center; + double radius; + + // empty + result = geomTest.minimalEnclosingCircle( center, radius ); + QCOMPARE( center, QgsPointXY() ); + QCOMPARE( radius, 0.0 ); + QCOMPARE( result, QgsGeometry() ); + + // caase 1 + geomTest = QgsGeometry::fromPoint( QgsPointXY( 5, 5 ) ); + result = geomTest.minimalEnclosingCircle( center, radius ); + QCOMPARE( center, QgsPointXY( 5, 5 ) ); + QCOMPARE( radius, 0.0 ); + resultTest.setGeometry( QgsCircle( QgsPoint( center ), radius ).toPolygon( 36 ) ); + QCOMPARE( result, resultTest ); + + // case 2 + geomTest = QgsGeometry::fromWkt( QString( "MULTIPOINT( 3 8, 7 4 )" ) ); + result = geomTest.minimalEnclosingCircle( center, radius ); + QGSCOMPARENEARPOINT( center, QgsPointXY( 5, 6 ), 0.0001 ); + QGSCOMPARENEAR( radius, sqrt( 2 ) * 2, 0.0001 ); + resultTest.setGeometry( QgsCircle( QgsPoint( center ), radius ).toPolygon( 36 ) ); + QCOMPARE( result, resultTest ); + + geomTest = QgsGeometry::fromWkt( QString( "LINESTRING( 0 5, 2 2, 0 -5, -1 -1 )" ) ); + result = geomTest.minimalEnclosingCircle( center, radius ); + QGSCOMPARENEARPOINT( center, QgsPointXY( 0, 0 ), 0.0001 ); + QGSCOMPARENEAR( radius, 5, 0.0001 ); + resultTest.setGeometry( QgsCircle( QgsPoint( center ), radius ).toPolygon( 36 ) ); + QCOMPARE( result, resultTest ); + + geomTest = QgsGeometry::fromWkt( QString( "MULTIPOINT( 0 5, 2 2, 0 -5, -1 -1 )" ) ); + result = geomTest.minimalEnclosingCircle( center, radius ); + QGSCOMPARENEARPOINT( center, QgsPointXY( 0, 0 ), 0.0001 ); + QGSCOMPARENEAR( radius, 5, 0.0001 ); + resultTest.setGeometry( QgsCircle( QgsPoint( center ), radius ).toPolygon( 36 ) ); + QCOMPARE( result, resultTest ); + + geomTest = QgsGeometry::fromWkt( QString( "POLYGON(( 0 5, 2 2, 0 -5, -1 -1 ))" ) ); + result = geomTest.minimalEnclosingCircle( center, radius ); + QGSCOMPARENEARPOINT( center, QgsPointXY( 0, 0 ), 0.0001 ); + QGSCOMPARENEAR( radius, 5, 0.0001 ); + resultTest.setGeometry( QgsCircle( QgsPoint( center ), radius ).toPolygon( 36 ) ); + QCOMPARE( result, resultTest ); + + geomTest = QgsGeometry::fromWkt( QString( "MULTIPOINT( 0 5, 0 -5, 0 0 )" ) ); + result = geomTest.minimalEnclosingCircle( center, radius ); + QGSCOMPARENEARPOINT( center, QgsPointXY( 0, 0 ), 0.0001 ); + QGSCOMPARENEAR( radius, 5, 0.0001 ); + resultTest.setGeometry( QgsCircle( QgsPoint( center ), radius ).toPolygon( 36 ) ); + QCOMPARE( result, resultTest ); + + // case 3 + geomTest = QgsGeometry::fromWkt( QString( "MULTIPOINT((0 0), (5 5), (0 -5), (0 5), (-5 0))" ) ); + result = geomTest.minimalEnclosingCircle( center, radius ); + QGSCOMPARENEARPOINT( center, QgsPointXY( 0.8333, 0.8333 ), 0.0001 ); + QGSCOMPARENEAR( radius, 5.8926, 0.0001 ); + resultTest.setGeometry( QgsCircle( QgsPoint( center ), radius ).toPolygon( 36 ) ); + QCOMPARE( result, resultTest ); + +} + + QGSTEST_MAIN( TestQgsGeometry ) #include "testqgsgeometry.moc"