Skip to content

Commit 37edb69

Browse files
authoredDec 7, 2016
Merge pull request #3843 from nyalldawson/oriented
Port minimum oriented bounding box to QgsGeometry
2 parents ed5a2bc + 1bdb35d commit 37edb69

File tree

11 files changed

+384
-74
lines changed

11 files changed

+384
-74
lines changed
 

‎python/core/geometry/qgsgeometry.sip

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -405,9 +405,21 @@ class QgsGeometry
405405
*/
406406
QgsGeometry makeDifference( const QgsGeometry& other ) const;
407407

408-
/** Returns the bounding box of this feature*/
408+
/**
409+
* Returns the bounding box of the geometry.
410+
* @see orientedMinimumBoundingBox()
411+
*/
409412
QgsRectangle boundingBox() const;
410413

414+
/**
415+
* Returns the oriented minimum bounding box for the geometry, which is the smallest (by area)
416+
* rotated rectangle which fully encompasses the geometry. The area, angle (clockwise in degrees from North),
417+
* width and height of the rotated bounding box will also be returned.
418+
* @note added in QGIS 3.0
419+
* @see boundingBox()
420+
*/
421+
QgsGeometry orientedMinimumBoundingBox( double& area /Out/, double &angle /Out/, double& width /Out/, double& height /Out/ ) const;
422+
411423
/** Test for intersection with a rectangle (uses GEOS) */
412424
bool intersects( const QgsRectangle& r ) const;
413425

‎python/plugins/processing/algs/qgis/OrientedMinimumBoundingBox.py

Lines changed: 24 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727

2828
from math import degrees, atan2
2929
from qgis.PyQt.QtCore import QVariant
30-
from qgis.core import Qgis, QgsField, QgsPoint, QgsGeometry, QgsFeature, QgsWkbTypes
30+
from qgis.core import Qgis, QgsField, QgsFields, QgsPoint, QgsGeometry, QgsFeature, QgsWkbTypes, QgsFeatureRequest
3131
from processing.core.GeoAlgorithm import GeoAlgorithm
3232
from processing.core.GeoAlgorithmExecutionException import GeoAlgorithmExecutionException
3333
from processing.core.parameters import ParameterVector
@@ -62,13 +62,15 @@ def processAlgorithm(self, progress):
6262
if byFeature and layer.geometryType() == QgsWkbTypes.PointGeometry and layer.featureCount() <= 2:
6363
raise GeoAlgorithmExecutionException(self.tr("Can't calculate an OMBB for each point, it's a point. The number of points must be greater than 2"))
6464

65-
fields = [
66-
QgsField('AREA', QVariant.Double),
67-
QgsField('PERIMETER', QVariant.Double),
68-
QgsField('ANGLE', QVariant.Double),
69-
QgsField('WIDTH', QVariant.Double),
70-
QgsField('HEIGHT', QVariant.Double),
71-
]
65+
if byFeature:
66+
fields = layer.fields()
67+
else:
68+
fields = QgsFields()
69+
fields.append(QgsField('area', QVariant.Double))
70+
fields.append(QgsField('perimeter', QVariant.Double))
71+
fields.append(QgsField('angle', QVariant.Double))
72+
fields.append(QgsField('width', QVariant.Double))
73+
fields.append(QgsField('height', QVariant.Double))
7274

7375
writer = self.getOutputFromName(self.OUTPUT).getVectorWriter(fields,
7476
QgsWkbTypes.Polygon, layer.crs())
@@ -81,30 +83,27 @@ def processAlgorithm(self, progress):
8183
del writer
8284

8385
def layerOmmb(self, layer, writer, progress):
84-
current = 0
85-
86-
fit = layer.getFeatures()
87-
inFeat = QgsFeature()
88-
total = 100.0 / layer.featureCount()
86+
req = QgsFeatureRequest().setSubsetOfAttributes([])
87+
features = vector.features(layer, req)
88+
total = 100.0 / len(features)
8989
newgeometry = QgsGeometry()
9090
first = True
91-
while fit.nextFeature(inFeat):
91+
for current, inFeat in enumerate(features):
9292
if first:
9393
newgeometry = inFeat.geometry()
9494
first = False
9595
else:
9696
newgeometry = newgeometry.combine(inFeat.geometry())
97-
current += 1
9897
progress.setPercentage(int(current * total))
9998

100-
geometry, area, perim, angle, width, height = self.OMBBox(newgeometry)
99+
geometry, area, angle, width, height = newgeometry.orientedMinimumBoundingBox()
101100

102101
if geometry:
103102
outFeat = QgsFeature()
104103

105104
outFeat.setGeometry(geometry)
106105
outFeat.setAttributes([area,
107-
perim,
106+
width * 2 + height * 2,
108107
angle,
109108
width,
110109
height])
@@ -115,62 +114,17 @@ def featureOmbb(self, layer, writer, progress):
115114
total = 100.0 / len(features)
116115
outFeat = QgsFeature()
117116
for current, inFeat in enumerate(features):
118-
geometry, area, perim, angle, width, height = self.OMBBox(
119-
inFeat.geometry())
117+
geometry, area, angle, width, height = inFeat.geometry().orientedMinimumBoundingBox()
120118
if geometry:
121119
outFeat.setGeometry(geometry)
122-
outFeat.setAttributes([area,
123-
perim,
124-
angle,
125-
width,
126-
height])
120+
attrs = inFeat.attributes()
121+
attrs.extend([area,
122+
width * 2 + height * 2,
123+
angle,
124+
width,
125+
height])
126+
outFeat.setAttributes(attrs)
127127
writer.addFeature(outFeat)
128128
else:
129129
progress.setInfo(self.tr("Can't calculate an OMBB for feature {0}.").format(inFeat.id()))
130130
progress.setPercentage(int(current * total))
131-
132-
def GetAngleOfLineBetweenTwoPoints(self, p1, p2, angle_unit="degrees"):
133-
xDiff = p2.x() - p1.x()
134-
yDiff = p2.y() - p1.y()
135-
if angle_unit == "radians":
136-
return atan2(yDiff, xDiff)
137-
else:
138-
return degrees(atan2(yDiff, xDiff))
139-
140-
def OMBBox(self, geom):
141-
g = geom.convexHull()
142-
143-
if g.type() != QgsWkbTypes.PolygonGeometry:
144-
return None, None, None, None, None, None
145-
r = g.asPolygon()[0]
146-
147-
p0 = QgsPoint(r[0][0], r[0][1])
148-
149-
i = 0
150-
l = len(r)
151-
OMBBox = QgsGeometry()
152-
gBBox = g.boundingBox()
153-
OMBBox_area = gBBox.height() * gBBox.width()
154-
OMBBox_angle = 0
155-
OMBBox_width = 0
156-
OMBBox_heigth = 0
157-
OMBBox_perim = 0
158-
while i < l - 1:
159-
x = QgsGeometry(g)
160-
angle = self.GetAngleOfLineBetweenTwoPoints(r[i], r[i + 1])
161-
x.rotate(angle, p0)
162-
bbox = x.boundingBox()
163-
bb = QgsGeometry.fromWkt(bbox.asWktPolygon())
164-
bb.rotate(-angle, p0)
165-
166-
areabb = bb.area()
167-
if areabb <= OMBBox_area:
168-
OMBBox = QgsGeometry(bb)
169-
OMBBox_area = areabb
170-
OMBBox_angle = angle
171-
OMBBox_width = bbox.width()
172-
OMBBox_heigth = bbox.height()
173-
OMBBox_perim = 2 * OMBBox_width + 2 * OMBBox_heigth
174-
i += 1
175-
176-
return OMBBox, OMBBox_area, OMBBox_perim, OMBBox_angle, OMBBox_width, OMBBox_heigth
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<GMLFeatureClassList>
2+
<GMLFeatureClass>
3+
<Name>oriented_bbox</Name>
4+
<ElementPath>oriented_bbox</ElementPath>
5+
<!--POLYGON-->
6+
<GeometryType>3</GeometryType>
7+
<SRSName>EPSG:4326</SRSName>
8+
<DatasetSpecificInfo>
9+
<FeatureCount>6</FeatureCount>
10+
<ExtentXMin>-1.00000</ExtentXMin>
11+
<ExtentXMax>10.00000</ExtentXMax>
12+
<ExtentYMin>-3.00000</ExtentYMin>
13+
<ExtentYMax>6.00000</ExtentYMax>
14+
</DatasetSpecificInfo>
15+
<PropertyDefn>
16+
<Name>intval</Name>
17+
<ElementPath>intval</ElementPath>
18+
<Type>Integer</Type>
19+
</PropertyDefn>
20+
<PropertyDefn>
21+
<Name>floatval</Name>
22+
<ElementPath>floatval</ElementPath>
23+
<Type>Real</Type>
24+
</PropertyDefn>
25+
<PropertyDefn>
26+
<Name>name</Name>
27+
<ElementPath>name</ElementPath>
28+
<Type>String</Type>
29+
<Width>5</Width>
30+
</PropertyDefn>
31+
</GMLFeatureClass>
32+
</GMLFeatureClassList>
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?xml version="1.0" encoding="utf-8" ?>
2+
<ogr:FeatureCollection
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation=""
5+
xmlns:ogr="http://ogr.maptools.org/"
6+
xmlns:gml="http://www.opengis.net/gml">
7+
<gml:boundedBy>
8+
<gml:Box>
9+
<gml:coord><gml:X>-1</gml:X><gml:Y>-3</gml:Y></gml:coord>
10+
<gml:coord><gml:X>10</gml:X><gml:Y>6</gml:Y></gml:coord>
11+
</gml:Box>
12+
</gml:boundedBy>
13+
14+
<gml:featureMember>
15+
<ogr:oriented_bbox fid="polys.4">
16+
<ogr:geometryProperty><gml:Polygon srsName="EPSG:4326"><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates>3.8,2.2 6,1 6,-3 2.4,-1.0 3.8,2.2</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs></gml:Polygon></ogr:geometryProperty>
17+
<ogr:intval>120</ogr:intval>
18+
<ogr:floatval>-100291.43213</ogr:floatval>
19+
</ogr:oriented_bbox>
20+
</gml:featureMember>
21+
<gml:featureMember>
22+
<ogr:oriented_bbox fid="polys.1">
23+
<ogr:geometryProperty><gml:Polygon srsName="EPSG:4326"><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates>5.4,5.0 6,4 5.2,3.8 4,4 5.4,5.0</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs></gml:Polygon></ogr:geometryProperty>
24+
<ogr:name>Aaaaa</ogr:name>
25+
<ogr:intval>-33</ogr:intval>
26+
<ogr:floatval>0</ogr:floatval>
27+
</ogr:oriented_bbox>
28+
</gml:featureMember>
29+
<gml:featureMember>
30+
<ogr:oriented_bbox fid="polys.0">
31+
<ogr:geometryProperty><gml:Polygon srsName="EPSG:4326"><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates>-1,-1 -1,3 3,3 3,2 2,2 2,-1 -1,-1</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs></gml:Polygon></ogr:geometryProperty>
32+
<ogr:name>aaaaa</ogr:name>
33+
<ogr:intval>33</ogr:intval>
34+
<ogr:floatval>44.12346</ogr:floatval>
35+
</ogr:oriented_bbox>
36+
</gml:featureMember>
37+
<gml:featureMember>
38+
<ogr:oriented_bbox fid="polys.3">
39+
<ogr:geometryProperty><gml:Polygon srsName="EPSG:4326"><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates>6.8,1.8 10,1 9.6,-2.2 6.4,-3.0 7.2,-0.6 6.8,1.8</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs><gml:innerBoundaryIs><gml:LinearRing><gml:coordinates>8.0,-0.6 7,-2 9,-2 9,0 8.0,-0.6</gml:coordinates></gml:LinearRing></gml:innerBoundaryIs></gml:Polygon></ogr:geometryProperty>
40+
<ogr:name>ASDF</ogr:name>
41+
<ogr:intval>0</ogr:intval>
42+
</ogr:oriented_bbox>
43+
</gml:featureMember>
44+
<gml:featureMember>
45+
<ogr:oriented_bbox fid="polys.2">
46+
<ogr:geometryProperty><gml:Polygon srsName="EPSG:4326"><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates>1.6,4.8 2,6 3,6 1.6,4.8</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs></gml:Polygon></ogr:geometryProperty>
47+
<ogr:name>bbaaa</ogr:name>
48+
<ogr:floatval>0.123</ogr:floatval>
49+
</ogr:oriented_bbox>
50+
</gml:featureMember>
51+
<gml:featureMember>
52+
<ogr:oriented_bbox fid="polys.5">
53+
<ogr:geometryProperty><gml:Polygon srsName="EPSG:4326"><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates>3.8,2.2 6,1 6,-3 2.4,-1.0 3.8,2.2</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs></gml:Polygon></ogr:geometryProperty>
54+
<ogr:name>elim</ogr:name>
55+
<ogr:intval>2</ogr:intval>
56+
<ogr:floatval>3.33</ogr:floatval>
57+
</ogr:oriented_bbox>
58+
</gml:featureMember>
59+
</ogr:FeatureCollection>
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<GMLFeatureClassList>
2+
<GMLFeatureClass>
3+
<Name>oriented_bounds</Name>
4+
<ElementPath>oriented_bounds</ElementPath>
5+
<!--POLYGON-->
6+
<GeometryType>3</GeometryType>
7+
<SRSName>EPSG:4326</SRSName>
8+
<DatasetSpecificInfo>
9+
<FeatureCount>6</FeatureCount>
10+
<ExtentXMin>-1.00000</ExtentXMin>
11+
<ExtentXMax>10.04414</ExtentXMax>
12+
<ExtentYMin>-3.27034</ExtentYMin>
13+
<ExtentYMax>6.44118</ExtentYMax>
14+
</DatasetSpecificInfo>
15+
<PropertyDefn>
16+
<Name>intval</Name>
17+
<ElementPath>intval</ElementPath>
18+
<Type>Integer</Type>
19+
</PropertyDefn>
20+
<PropertyDefn>
21+
<Name>floatval</Name>
22+
<ElementPath>floatval</ElementPath>
23+
<Type>Real</Type>
24+
</PropertyDefn>
25+
<PropertyDefn>
26+
<Name>area</Name>
27+
<ElementPath>area</ElementPath>
28+
<Type>Real</Type>
29+
</PropertyDefn>
30+
<PropertyDefn>
31+
<Name>perimeter</Name>
32+
<ElementPath>perimeter</ElementPath>
33+
<Type>Real</Type>
34+
</PropertyDefn>
35+
<PropertyDefn>
36+
<Name>angle</Name>
37+
<ElementPath>angle</ElementPath>
38+
<Type>Real</Type>
39+
</PropertyDefn>
40+
<PropertyDefn>
41+
<Name>width</Name>
42+
<ElementPath>width</ElementPath>
43+
<Type>Real</Type>
44+
</PropertyDefn>
45+
<PropertyDefn>
46+
<Name>height</Name>
47+
<ElementPath>height</ElementPath>
48+
<Type>Real</Type>
49+
</PropertyDefn>
50+
<PropertyDefn>
51+
<Name>name</Name>
52+
<ElementPath>name</ElementPath>
53+
<Type>String</Type>
54+
<Width>5</Width>
55+
</PropertyDefn>
56+
</GMLFeatureClass>
57+
</GMLFeatureClassList>
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
<?xml version="1.0" encoding="utf-8" ?>
2+
<ogr:FeatureCollection
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation=""
5+
xmlns:ogr="http://ogr.maptools.org/"
6+
xmlns:gml="http://www.opengis.net/gml">
7+
<gml:boundedBy>
8+
<gml:Box>
9+
<gml:coord><gml:X>-1</gml:X><gml:Y>-3.270344827586206</gml:Y></gml:coord>
10+
<gml:coord><gml:X>10.04413793103448</gml:X><gml:Y>6.441176470588236</gml:Y></gml:coord>
11+
</gml:Box>
12+
</gml:boundedBy>
13+
14+
<gml:featureMember>
15+
<ogr:oriented_bounds fid="polys.4">
16+
<ogr:geometryProperty><gml:Polygon srsName="EPSG:4326"><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates>6,-3 7.69811320754717,0.056603773584906 3.80943396226415,2.21698113207547 2.11132075471698,-0.839622641509436 6,-3</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs></gml:Polygon></ogr:geometryProperty>
17+
<ogr:intval>120</ogr:intval>
18+
<ogr:floatval>-100291.43213</ogr:floatval>
19+
<ogr:area>15.5547169811321</ogr:area>
20+
<ogr:perimeter>15.8902367081648</ogr:perimeter>
21+
<ogr:angle>119.054604099077</ogr:angle>
22+
<ogr:width>3.49662910448615</ogr:width>
23+
<ogr:height>4.44848924959627</ogr:height>
24+
</ogr:oriented_bounds>
25+
</gml:featureMember>
26+
<gml:featureMember>
27+
<ogr:oriented_bounds fid="polys.1">
28+
<ogr:geometryProperty><gml:Polygon srsName="EPSG:4326"><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates>4.11764705882353,3.52941176470588 6.0,4.0 5.72941176470588,5.08235294117647 3.84705882352941,4.61176470588235 4.11764705882353,3.52941176470588</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs></gml:Polygon></ogr:geometryProperty>
29+
<ogr:intval>-33</ogr:intval>
30+
<ogr:floatval>0</ogr:floatval>
31+
<ogr:name>Aaaaa</ogr:name>
32+
<ogr:area>2.16470588235294</ogr:area>
33+
<ogr:perimeter>6.11189775091559</ogr:perimeter>
34+
<ogr:angle>165.963756532073</ogr:angle>
35+
<ogr:width>1.94028500029066</ogr:width>
36+
<ogr:height>1.11566387516713</ogr:height>
37+
</ogr:oriented_bounds>
38+
</gml:featureMember>
39+
<gml:featureMember>
40+
<ogr:oriented_bounds fid="polys.0">
41+
<ogr:geometryProperty><gml:Polygon srsName="EPSG:4326"><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates>-1.0,3.0 -1.0,-1.0 3.0,-1.0 3.0,3.0 -1.0,3.0</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs></gml:Polygon></ogr:geometryProperty>
42+
<ogr:intval>33</ogr:intval>
43+
<ogr:floatval>44.12346</ogr:floatval>
44+
<ogr:name>aaaaa</ogr:name>
45+
<ogr:area>16</ogr:area>
46+
<ogr:perimeter>16</ogr:perimeter>
47+
<ogr:angle>90</ogr:angle>
48+
<ogr:width>4</ogr:width>
49+
<ogr:height>4</ogr:height>
50+
</ogr:oriented_bounds>
51+
</gml:featureMember>
52+
<gml:featureMember>
53+
<ogr:oriented_bounds fid="polys.3">
54+
<ogr:geometryProperty><gml:Polygon srsName="EPSG:4326"><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates>6.4,-3.0 9.64413793103449,-3.27034482758621 10.0441379310345,1.52965517241379 6.8,1.8 6.4,-3.0</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs></gml:Polygon></ogr:geometryProperty>
55+
<ogr:intval>0</ogr:intval>
56+
<ogr:name>ASDF</ogr:name>
57+
<ogr:area>15.68</ogr:area>
58+
<ogr:perimeter>16.1440412835671</ogr:perimeter>
59+
<ogr:angle>4.76364169072617</ogr:angle>
60+
<ogr:width>3.25538281026661</ogr:width>
61+
<ogr:height>4.81663783151692</ogr:height>
62+
</ogr:oriented_bounds>
63+
</gml:featureMember>
64+
<gml:featureMember>
65+
<ogr:oriented_bounds fid="polys.2">
66+
<ogr:geometryProperty><gml:Polygon srsName="EPSG:4326"><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates>1.36470588235294,4.94117647058824 2.1,4.5 3.0,6.0 2.26470588235294,6.44117647058824 1.36470588235294,4.94117647058824</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs></gml:Polygon></ogr:geometryProperty>
67+
<ogr:floatval>0.123</ogr:floatval>
68+
<ogr:name>bbaaa</ogr:name>
69+
<ogr:area>1.5</ogr:area>
70+
<ogr:perimeter>5.21355698833227</ogr:perimeter>
71+
<ogr:angle>30.9637565320735</ogr:angle>
72+
<ogr:width>0.857492925712544</ogr:width>
73+
<ogr:height>1.74928556845359</ogr:height>
74+
</ogr:oriented_bounds>
75+
</gml:featureMember>
76+
<gml:featureMember>
77+
<ogr:oriented_bounds fid="polys.5">
78+
<ogr:geometryProperty><gml:Polygon srsName="EPSG:4326"><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates>6,-3 7.69811320754717,0.056603773584906 3.80943396226415,2.21698113207547 2.11132075471698,-0.839622641509436 6,-3</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs></gml:Polygon></ogr:geometryProperty>
79+
<ogr:intval>2</ogr:intval>
80+
<ogr:floatval>3.33</ogr:floatval>
81+
<ogr:name>elim</ogr:name>
82+
<ogr:area>15.5547169811321</ogr:area>
83+
<ogr:perimeter>15.8902367081648</ogr:perimeter>
84+
<ogr:angle>119.054604099077</ogr:angle>
85+
<ogr:width>3.49662910448615</ogr:width>
86+
<ogr:height>4.44848924959627</ogr:height>
87+
</ogr:oriented_bounds>
88+
</gml:featureMember>
89+
</ogr:FeatureCollection>

‎python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1854,3 +1854,15 @@ tests:
18541854
OUTPUT:
18551855
hash: fe6e018be13c5a3c17f3f4d0f0dc7686c628cb440b74c4642aa0c939
18561856
type: rasterhash
1857+
1858+
- algorithm: qgis:orientedminimumboundingbox
1859+
name: Oriented minimum bounding box polys
1860+
params:
1861+
BY_FEATURE: true
1862+
INPUT_LAYER:
1863+
name: custom/oriented_bbox.gml
1864+
type: vector
1865+
results:
1866+
OUTPUT:
1867+
name: expected/oriented_bounds.gml
1868+
type: vector

‎python/testing/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,6 @@ def assertLayersEqual(self, layer_expected, layer_result, **kwargs):
102102
)
103103

104104
for attr_expected, field_expected in zip(feats[0].attributes(), layer_expected.fields().toList()):
105-
attr_result = feats[1][field_expected.name()]
106-
field_result = [fld for fld in layer_expected.fields().toList() if fld.name() == field_expected.name()][0]
107105
try:
108106
cmp = compare['fields'][field_expected.name()]
109107
except KeyError:
@@ -116,6 +114,9 @@ def assertLayersEqual(self, layer_expected, layer_result, **kwargs):
116114
if 'skip' in cmp:
117115
continue
118116

117+
attr_result = feats[1][field_expected.name()]
118+
field_result = [fld for fld in layer_expected.fields().toList() if fld.name() == field_expected.name()][0]
119+
119120
# Cast field to a given type
120121
if 'cast' in cmp:
121122
if cmp['cast'] == 'int':

‎src/core/geometry/qgsgeometry.cpp

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -862,6 +862,65 @@ QgsRectangle QgsGeometry::boundingBox() const
862862
return QgsRectangle();
863863
}
864864

865+
QgsGeometry QgsGeometry::orientedMinimumBoundingBox( double& area, double &angle, double& width, double& height ) const
866+
{
867+
QgsRectangle minRect;
868+
area = DBL_MAX;
869+
angle = 0;
870+
width = DBL_MAX;
871+
height = DBL_MAX;
872+
873+
if ( !d->geometry || d->geometry->nCoordinates() < 2 )
874+
return QgsGeometry();
875+
876+
QgsGeometry hull = convexHull();
877+
if ( hull.isEmpty() )
878+
return QgsGeometry();
879+
880+
QgsVertexId vertexId;
881+
QgsPointV2 pt0;
882+
QgsPointV2 pt1;
883+
QgsPointV2 pt2;
884+
// get first point
885+
hull.geometry()->nextVertex( vertexId, pt0 );
886+
pt1 = pt0;
887+
double prevAngle = 0.0;
888+
while ( hull.geometry()->nextVertex( vertexId, pt2 ) )
889+
{
890+
double currentAngle = QgsGeometryUtils::lineAngle( pt1.x(), pt1.y(), pt2.x(), pt2.y() );
891+
double rotateAngle = 180.0 / M_PI * ( currentAngle - prevAngle );
892+
prevAngle = currentAngle;
893+
894+
QTransform t = QTransform::fromTranslate( pt0.x(), pt0.y() );
895+
t.rotate( rotateAngle );
896+
t.translate( -pt0.x(), -pt0.y() );
897+
898+
hull.geometry()->transform( t );
899+
900+
QgsRectangle bounds = hull.geometry()->boundingBox();
901+
double currentArea = bounds.width() * bounds.height();
902+
if ( currentArea < area )
903+
{
904+
minRect = bounds;
905+
area = currentArea;
906+
angle = 180.0 / M_PI * currentAngle;
907+
width = bounds.width();
908+
height = bounds.height();
909+
}
910+
911+
pt2 = pt1;
912+
}
913+
914+
QgsGeometry minBounds = QgsGeometry::fromRect( minRect );
915+
minBounds.rotate( angle, QgsPoint( pt0.x(), pt0.y() ) );
916+
917+
// constrain angle to 0 - 180
918+
if ( angle > 180.0 )
919+
angle = fmod( angle, 180.0 );
920+
921+
return minBounds;
922+
}
923+
865924
bool QgsGeometry::intersects( const QgsRectangle& r ) const
866925
{
867926
QgsGeometry g = fromRect( r );

‎src/core/geometry/qgsgeometry.h

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -458,9 +458,21 @@ class CORE_EXPORT QgsGeometry
458458
*/
459459
QgsGeometry makeDifference( const QgsGeometry& other ) const;
460460

461-
//! Returns the bounding box of this feature
461+
/**
462+
* Returns the bounding box of the geometry.
463+
* @see orientedMinimumBoundingBox()
464+
*/
462465
QgsRectangle boundingBox() const;
463466

467+
/**
468+
* Returns the oriented minimum bounding box for the geometry, which is the smallest (by area)
469+
* rotated rectangle which fully encompasses the geometry. The area, angle (clockwise in degrees from North),
470+
* width and height of the rotated bounding box will also be returned.
471+
* @note added in QGIS 3.0
472+
* @see boundingBox()
473+
*/
474+
QgsGeometry orientedMinimumBoundingBox( double& area, double &angle, double& width, double& height ) const;
475+
464476
//! Test for intersection with a rectangle (uses GEOS)
465477
bool intersects( const QgsRectangle& r ) const;
466478

‎tests/src/python/test_qgsgeometry.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3645,6 +3645,29 @@ def testRemoveRings(self):
36453645
self.assertTrue(compareWkt(result, exp, 0.00001),
36463646
"Extend line: mismatch Expected:\n{}\nGot:\n{}\n".format(exp, result))
36473647

3648+
def testMinimumOrientedBoundingBox(self):
3649+
empty = QgsGeometry()
3650+
bbox, area, angle, width, height = empty.orientedMinimumBoundingBox()
3651+
self.assertFalse(bbox)
3652+
3653+
# not a useful geometry
3654+
point = QgsGeometry.fromWkt('Point(1 2)')
3655+
bbox, area, angle, width, height = point.orientedMinimumBoundingBox()
3656+
self.assertFalse(bbox)
3657+
3658+
# polygon
3659+
polygon = QgsGeometry.fromWkt('Polygon((-0.1 -1.3, 2.1 1, 3 2.8, 6.7 0.2, 3 -1.8, 0.3 -2.7, -0.1 -1.3))')
3660+
bbox, area, angle, width, height = polygon.orientedMinimumBoundingBox()
3661+
exp = 'Polygon ((-0.94905660 -1.571698, 2.3817055 -4.580453, 6.7000000 0.1999999, 3.36923 3.208754, -0.949056 -1.57169))'
3662+
3663+
result = bbox.exportToWkt()
3664+
self.assertTrue(compareWkt(result, exp, 0.00001),
3665+
"Oriented MBBR: mismatch Expected:\n{}\nGot:\n{}\n".format(exp, result))
3666+
self.assertAlmostEqual(area, 28.9152, places=3)
3667+
self.assertAlmostEqual(angle, 42.0922, places=3)
3668+
self.assertAlmostEqual(width, 4.4884, places=3)
3669+
self.assertAlmostEqual(height, 6.4420, places=3)
3670+
36483671

36493672
if __name__ == '__main__':
36503673
unittest.main()

0 commit comments

Comments
 (0)
Please sign in to comment.