Skip to content

Commit b6a2646

Browse files
authoredMay 10, 2018
Merge pull request #6961 from wonder-sk/union-single-layer
[FEATURE] Union algorithm with a single layer
2 parents 9690d80 + c3279ee commit b6a2646

File tree

9 files changed

+648
-111
lines changed

9 files changed

+648
-111
lines changed
 

‎python/plugins/processing/tests/AlgorithmsTestBase.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -283,13 +283,14 @@ def check_results(self, results, context, params, expected):
283283

284284
compare = expected_result.get('compare', {})
285285
pk = expected_result.get('pk', None)
286+
topo_equal_check = expected_result.get('topo_equal_check', False)
286287

287288
if len(expected_lyrs) == 1:
288-
self.assertLayersEqual(expected_lyrs[0], result_lyr, compare=compare, pk=pk)
289+
self.assertLayersEqual(expected_lyrs[0], result_lyr, compare=compare, pk=pk, geometry={'topo_equal_check': topo_equal_check})
289290
else:
290291
res = False
291292
for l in expected_lyrs:
292-
if self.checkLayersEqual(l, result_lyr, compare=compare, pk=pk):
293+
if self.checkLayersEqual(l, result_lyr, compare=compare, pk=pk, geometry={'topo_equal_check': topo_equal_check}):
293294
res = True
294295
break
295296
self.assertTrue(res, 'Could not find matching layer in expected results')
@@ -319,6 +320,7 @@ def check_results(self, results, context, params, expected):
319320

320321

321322
class GenericAlgorithmsTest(unittest.TestCase):
323+
322324
"""
323325
General (non-provider specific) algorithm tests
324326
"""
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"type": "FeatureCollection",
3+
"name": "overlay0",
4+
"crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:EPSG::3857" } },
5+
"features": [
6+
{ "type": "Feature", "properties": { "id_a": "A1" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 1.0, 1.0 ], [ 5.0, 1.0 ], [ 5.0, 5.0 ], [ 1.0, 5.0 ], [ 1.0, 1.0 ] ] ] } },
7+
{ "type": "Feature", "properties": { "id_a": "A2" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 2.0, 2.0 ], [ 4.0, 2.0 ], [ 4.0, 4.0 ], [ 2.0, 4.0 ], [ 2.0, 2.0 ] ] ] } },
8+
{ "type": "Feature", "properties": { "id_a": "A3" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 7.0, 2.0 ], [ 9.0, 2.0 ], [ 9.0, 4.0 ], [ 7.0, 4.0 ], [ 7.0, 2.0 ] ] ] } },
9+
{ "type": "Feature", "properties": { "id_a": "A4" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 6.0, 1.0 ], [ 10.0, 1.0 ], [ 10.0, 5.0 ], [ 6.0, 5.0 ], [ 6.0, 1.0 ] ] ] } },
10+
{ "type": "Feature", "properties": { "id_a": "A5" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 1.0, 6.0 ], [ 3.0, 6.0 ], [ 3.0, 10.0 ], [ 1.0, 10.0 ], [ 1.0, 6.0 ] ] ] } },
11+
{ "type": "Feature", "properties": { "id_a": "A6" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 3.0, 6.0 ], [ 5.0, 6.0 ], [ 5.0, 10.0 ], [ 3.0, 10.0 ], [ 3.0, 6.0 ] ] ] } },
12+
{ "type": "Feature", "properties": { "id_a": "A7" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 6.0, 6.0 ], [ 10.0, 6.0 ], [ 10.0, 10.0 ], [ 6.0, 10.0 ], [ 6.0, 6.0 ] ] ] } },
13+
{ "type": "Feature", "properties": { "id_a": "A8" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 1.0, 11.0 ], [ 4.0, 11.0 ], [ 4.0, 14.0 ], [ 1.0, 14.0 ], [ 1.0, 11.0 ] ] ] } },
14+
{ "type": "Feature", "properties": { "id_a": "A9" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 1.0, 13.0 ], [ 4.0, 13.0 ], [ 4.0, 16.0 ], [ 1.0, 16.0 ], [ 1.0, 13.0 ] ] ] } },
15+
{ "type": "Feature", "properties": { "id_a": "A10" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 2.0, 12.0 ], [ 5.0, 12.0 ], [ 5.0, 15.0 ], [ 2.0, 15.0 ], [ 2.0, 12.0 ] ] ] } },
16+
{ "type": "Feature", "properties": { "id_a": "A11" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 6.0, 11.0 ], [ 10.0, 11.0 ], [ 10.0, 12.0 ], [ 7.0, 12.0 ], [ 7.0, 15.0 ], [ 6.0, 15.0 ], [ 6.0, 11.0 ] ] ] } },
17+
{ "type": "Feature", "properties": { "id_a": "A12" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 8.0, 13.0 ], [ 10.0, 13.0 ], [ 10.0, 15.0 ], [ 8.0, 15.0 ], [ 8.0, 13.0 ] ] ] } }
18+
]
19+
}
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
<?xml version="1.0" encoding="utf-8" ?>
2+
<ogr:FeatureCollection
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://ogr.maptools.org/ union0.xsd"
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>1</gml:Y></gml:coord>
10+
<gml:coord><gml:X>10</gml:X><gml:Y>16</gml:Y></gml:coord>
11+
</gml:Box>
12+
</gml:boundedBy>
13+
14+
<gml:featureMember>
15+
<ogr:union0 fid="union0.0">
16+
<ogr:geometryProperty><gml:MultiPolygon srsName="EPSG:3857"><gml:polygonMember><gml:Polygon><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates>2,2 2,4 4,4 4,2 2,2</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs></gml:Polygon></gml:polygonMember></gml:MultiPolygon></ogr:geometryProperty>
17+
<ogr:id_a>A2</ogr:id_a>
18+
</ogr:union0>
19+
</gml:featureMember>
20+
<gml:featureMember>
21+
<ogr:union0 fid="union0.1">
22+
<ogr:geometryProperty><gml:MultiPolygon srsName="EPSG:3857"><gml:polygonMember><gml:Polygon><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates>2,2 2,4 4,4 4,2 2,2</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs></gml:Polygon></gml:polygonMember></gml:MultiPolygon></ogr:geometryProperty>
23+
<ogr:id_a>A1</ogr:id_a>
24+
</ogr:union0>
25+
</gml:featureMember>
26+
<gml:featureMember>
27+
<ogr:union0 fid="union0.2">
28+
<ogr:geometryProperty><gml:MultiPolygon srsName="EPSG:3857"><gml:polygonMember><gml:Polygon><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates>1,1 1,5 5,5 5,1 1,1</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs><gml:innerBoundaryIs><gml:LinearRing><gml:coordinates>2,2 4,2 4,4 2,4 2,2</gml:coordinates></gml:LinearRing></gml:innerBoundaryIs></gml:Polygon></gml:polygonMember></gml:MultiPolygon></ogr:geometryProperty>
29+
<ogr:id_a>A1</ogr:id_a>
30+
</ogr:union0>
31+
</gml:featureMember>
32+
<gml:featureMember>
33+
<ogr:union0 fid="union0.3">
34+
<ogr:geometryProperty><gml:MultiPolygon srsName="EPSG:3857"><gml:polygonMember><gml:Polygon><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates>7,2 7,4 9,4 9,2 7,2</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs></gml:Polygon></gml:polygonMember></gml:MultiPolygon></ogr:geometryProperty>
35+
<ogr:id_a>A4</ogr:id_a>
36+
</ogr:union0>
37+
</gml:featureMember>
38+
<gml:featureMember>
39+
<ogr:union0 fid="union0.4">
40+
<ogr:geometryProperty><gml:MultiPolygon srsName="EPSG:3857"><gml:polygonMember><gml:Polygon><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates>7,2 7,4 9,4 9,2 7,2</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs></gml:Polygon></gml:polygonMember></gml:MultiPolygon></ogr:geometryProperty>
41+
<ogr:id_a>A3</ogr:id_a>
42+
</ogr:union0>
43+
</gml:featureMember>
44+
<gml:featureMember>
45+
<ogr:union0 fid="union0.5">
46+
<ogr:geometryProperty><gml:MultiPolygon srsName="EPSG:3857"><gml:polygonMember><gml:Polygon><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates>2,13 1,13 1,14 2,14 2,13</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs></gml:Polygon></gml:polygonMember></gml:MultiPolygon></ogr:geometryProperty>
47+
<ogr:id_a>A9</ogr:id_a>
48+
</ogr:union0>
49+
</gml:featureMember>
50+
<gml:featureMember>
51+
<ogr:union0 fid="union0.6">
52+
<ogr:geometryProperty><gml:MultiPolygon srsName="EPSG:3857"><gml:polygonMember><gml:Polygon><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates>2,13 1,13 1,14 2,14 2,13</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs></gml:Polygon></gml:polygonMember></gml:MultiPolygon></ogr:geometryProperty>
53+
<ogr:id_a>A8</ogr:id_a>
54+
</ogr:union0>
55+
</gml:featureMember>
56+
<gml:featureMember>
57+
<ogr:union0 fid="union0.7">
58+
<ogr:geometryProperty><gml:MultiPolygon srsName="EPSG:3857"><gml:polygonMember><gml:Polygon><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates>6,1 6,5 10,5 10,1 6,1</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs><gml:innerBoundaryIs><gml:LinearRing><gml:coordinates>7,2 9,2 9,4 7,4 7,2</gml:coordinates></gml:LinearRing></gml:innerBoundaryIs></gml:Polygon></gml:polygonMember></gml:MultiPolygon></ogr:geometryProperty>
59+
<ogr:id_a>A4</ogr:id_a>
60+
</ogr:union0>
61+
</gml:featureMember>
62+
<gml:featureMember>
63+
<ogr:union0 fid="union0.8">
64+
<ogr:geometryProperty><gml:MultiPolygon srsName="EPSG:3857"><gml:polygonMember><gml:Polygon><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates>4,12 2,12 2,13 4,13 4,12</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs></gml:Polygon></gml:polygonMember></gml:MultiPolygon></ogr:geometryProperty>
65+
<ogr:id_a>A10</ogr:id_a>
66+
</ogr:union0>
67+
</gml:featureMember>
68+
<gml:featureMember>
69+
<ogr:union0 fid="union0.9">
70+
<ogr:geometryProperty><gml:MultiPolygon srsName="EPSG:3857"><gml:polygonMember><gml:Polygon><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates>4,12 2,12 2,13 4,13 4,12</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs></gml:Polygon></gml:polygonMember></gml:MultiPolygon></ogr:geometryProperty>
71+
<ogr:id_a>A8</ogr:id_a>
72+
</ogr:union0>
73+
</gml:featureMember>
74+
<gml:featureMember>
75+
<ogr:union0 fid="union0.10">
76+
<ogr:geometryProperty><gml:MultiPolygon srsName="EPSG:3857"><gml:polygonMember><gml:Polygon><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates>1,6 3,6 3,10 1,10 1,6</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs></gml:Polygon></gml:polygonMember></gml:MultiPolygon></ogr:geometryProperty>
77+
<ogr:id_a>A5</ogr:id_a>
78+
</ogr:union0>
79+
</gml:featureMember>
80+
<gml:featureMember>
81+
<ogr:union0 fid="union0.11">
82+
<ogr:geometryProperty><gml:MultiPolygon srsName="EPSG:3857"><gml:polygonMember><gml:Polygon><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates>2,13 2,14 4,14 4,13 2,13</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs></gml:Polygon></gml:polygonMember></gml:MultiPolygon></ogr:geometryProperty>
83+
<ogr:id_a>A10</ogr:id_a>
84+
</ogr:union0>
85+
</gml:featureMember>
86+
<gml:featureMember>
87+
<ogr:union0 fid="union0.12">
88+
<ogr:geometryProperty><gml:MultiPolygon srsName="EPSG:3857"><gml:polygonMember><gml:Polygon><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates>2,13 2,14 4,14 4,13 2,13</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs></gml:Polygon></gml:polygonMember></gml:MultiPolygon></ogr:geometryProperty>
89+
<ogr:id_a>A9</ogr:id_a>
90+
</ogr:union0>
91+
</gml:featureMember>
92+
<gml:featureMember>
93+
<ogr:union0 fid="union0.13">
94+
<ogr:geometryProperty><gml:MultiPolygon srsName="EPSG:3857"><gml:polygonMember><gml:Polygon><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates>2,13 2,14 4,14 4,13 2,13</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs></gml:Polygon></gml:polygonMember></gml:MultiPolygon></ogr:geometryProperty>
95+
<ogr:id_a>A8</ogr:id_a>
96+
</ogr:union0>
97+
</gml:featureMember>
98+
<gml:featureMember>
99+
<ogr:union0 fid="union0.14">
100+
<ogr:geometryProperty><gml:MultiPolygon srsName="EPSG:3857"><gml:polygonMember><gml:Polygon><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates>3,6 5,6 5,10 3,10 3,6</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs></gml:Polygon></gml:polygonMember></gml:MultiPolygon></ogr:geometryProperty>
101+
<ogr:id_a>A6</ogr:id_a>
102+
</ogr:union0>
103+
</gml:featureMember>
104+
<gml:featureMember>
105+
<ogr:union0 fid="union0.15">
106+
<ogr:geometryProperty><gml:MultiPolygon srsName="EPSG:3857"><gml:polygonMember><gml:Polygon><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates>2,14 2,15 4,15 4,14 2,14</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs></gml:Polygon></gml:polygonMember></gml:MultiPolygon></ogr:geometryProperty>
107+
<ogr:id_a>A10</ogr:id_a>
108+
</ogr:union0>
109+
</gml:featureMember>
110+
<gml:featureMember>
111+
<ogr:union0 fid="union0.16">
112+
<ogr:geometryProperty><gml:MultiPolygon srsName="EPSG:3857"><gml:polygonMember><gml:Polygon><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates>2,14 2,15 4,15 4,14 2,14</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs></gml:Polygon></gml:polygonMember></gml:MultiPolygon></ogr:geometryProperty>
113+
<ogr:id_a>A9</ogr:id_a>
114+
</ogr:union0>
115+
</gml:featureMember>
116+
<gml:featureMember>
117+
<ogr:union0 fid="union0.17">
118+
<ogr:geometryProperty><gml:MultiPolygon srsName="EPSG:3857"><gml:polygonMember><gml:Polygon><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates>6,6 10,6 10,10 6,10 6,6</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs></gml:Polygon></gml:polygonMember></gml:MultiPolygon></ogr:geometryProperty>
119+
<ogr:id_a>A7</ogr:id_a>
120+
</ogr:union0>
121+
</gml:featureMember>
122+
<gml:featureMember>
123+
<ogr:union0 fid="union0.18">
124+
<ogr:geometryProperty><gml:MultiPolygon srsName="EPSG:3857"><gml:polygonMember><gml:Polygon><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates>4,12 4,11 1,11 1,13 2,13 2,12 4,12</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs></gml:Polygon></gml:polygonMember></gml:MultiPolygon></ogr:geometryProperty>
125+
<ogr:id_a>A8</ogr:id_a>
126+
</ogr:union0>
127+
</gml:featureMember>
128+
<gml:featureMember>
129+
<ogr:union0 fid="union0.19">
130+
<ogr:geometryProperty><gml:MultiPolygon srsName="EPSG:3857"><gml:polygonMember><gml:Polygon><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates>1,14 1,16 4,16 4,15 2,15 2,14 1,14</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs></gml:Polygon></gml:polygonMember></gml:MultiPolygon></ogr:geometryProperty>
131+
<ogr:id_a>A9</ogr:id_a>
132+
</ogr:union0>
133+
</gml:featureMember>
134+
<gml:featureMember>
135+
<ogr:union0 fid="union0.20">
136+
<ogr:geometryProperty><gml:MultiPolygon srsName="EPSG:3857"><gml:polygonMember><gml:Polygon><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates>4,15 5,15 5,12 4,12 4,13 4,14 4,15</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs></gml:Polygon></gml:polygonMember></gml:MultiPolygon></ogr:geometryProperty>
137+
<ogr:id_a>A10</ogr:id_a>
138+
</ogr:union0>
139+
</gml:featureMember>
140+
<gml:featureMember>
141+
<ogr:union0 fid="union0.21">
142+
<ogr:geometryProperty><gml:MultiPolygon srsName="EPSG:3857"><gml:polygonMember><gml:Polygon><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates>6,11 10,11 10,12 7,12 7,15 6,15 6,11</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs></gml:Polygon></gml:polygonMember></gml:MultiPolygon></ogr:geometryProperty>
143+
<ogr:id_a>A11</ogr:id_a>
144+
</ogr:union0>
145+
</gml:featureMember>
146+
<gml:featureMember>
147+
<ogr:union0 fid="union0.22">
148+
<ogr:geometryProperty><gml:MultiPolygon srsName="EPSG:3857"><gml:polygonMember><gml:Polygon><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates>8,13 10,13 10,15 8,15 8,13</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs></gml:Polygon></gml:polygonMember></gml:MultiPolygon></ogr:geometryProperty>
149+
<ogr:id_a>A12</ogr:id_a>
150+
</ogr:union0>
151+
</gml:featureMember>
152+
</ogr:FeatureCollection>
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<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">
3+
<xs:import namespace="http://www.opengis.net/gml" schemaLocation="http://schemas.opengis.net/gml/2.1.2/feature.xsd"/>
4+
<xs:element name="FeatureCollection" type="ogr:FeatureCollectionType" substitutionGroup="gml:_FeatureCollection"/>
5+
<xs:complexType name="FeatureCollectionType">
6+
<xs:complexContent>
7+
<xs:extension base="gml:AbstractFeatureCollectionType">
8+
<xs:attribute name="lockId" type="xs:string" use="optional"/>
9+
<xs:attribute name="scope" type="xs:string" use="optional"/>
10+
</xs:extension>
11+
</xs:complexContent>
12+
</xs:complexType>
13+
<xs:element name="union0" type="ogr:union0_Type" substitutionGroup="gml:_Feature"/>
14+
<xs:complexType name="union0_Type">
15+
<xs:complexContent>
16+
<xs:extension base="gml:AbstractFeatureType">
17+
<xs:sequence>
18+
<xs:element name="geometryProperty" type="gml:MultiPolygonPropertyType" nillable="true" minOccurs="0" maxOccurs="1"/>
19+
<xs:element name="id_a" nillable="true" minOccurs="0" maxOccurs="1">
20+
<xs:simpleType>
21+
<xs:restriction base="xs:string">
22+
<xs:maxLength value="255"/>
23+
</xs:restriction>
24+
</xs:simpleType>
25+
</xs:element>
26+
</xs:sequence>
27+
</xs:extension>
28+
</xs:complexContent>
29+
</xs:complexType>
30+
</xs:schema>

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5330,6 +5330,21 @@ tests:
53305330
fields:
53315331
fid: skip
53325332

5333+
- algorithm: native:union
5334+
name: Test Union of single layer
5335+
params:
5336+
INPUT:
5337+
name: custom/overlay0.geojson
5338+
type: vector
5339+
results:
5340+
OUTPUT:
5341+
name: expected/union0.gml
5342+
type: vector
5343+
compare:
5344+
unordered: true
5345+
fields:
5346+
fid: skip
5347+
53335348
- algorithm: native:union
53345349
name: Test Union (basic)
53355350
params:

‎python/testing/__init__.py

Lines changed: 158 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,57 @@ def checkLayersEqual(self, layer_expected, layer_result, use_asserts=False, **kw
104104
except KeyError:
105105
precision = 14
106106

107+
try:
108+
topo_equal_check = compare['geometry']['topo_equal_check']
109+
except KeyError:
110+
topo_equal_check = False
111+
112+
try:
113+
unordered = compare['unordered']
114+
except KeyError:
115+
unordered = False
116+
117+
if unordered:
118+
features_expected = [f for f in layer_expected.getFeatures(request)]
119+
for feat in layer_result.getFeatures(request):
120+
feat_expected_equal = None
121+
for feat_expected in features_expected:
122+
if self.checkGeometriesEqual(feat.geometry(), feat_expected.geometry(),
123+
feat.id(), feat_expected.id(),
124+
False, precision, topo_equal_check) and \
125+
self.checkAttributesEqual(feat, feat_expected, layer_expected.fields(), False, compare):
126+
feat_expected_equal = feat_expected
127+
break
128+
129+
if feat_expected_equal is not None:
130+
features_expected.remove(feat_expected_equal)
131+
else:
132+
if use_asserts:
133+
_TestCase.assertTrue(
134+
self, False,
135+
'Unexpected result feature: fid {}, geometry: {}, attributes: {}'.format(
136+
feat.id(),
137+
feat.geometry().constGet().asWkt(precision) if feat.geometry() else 'NULL',
138+
feat.attributes())
139+
)
140+
else:
141+
return False
142+
143+
if len(features_expected) != 0:
144+
if use_asserts:
145+
lst_missing = []
146+
for feat in features_expected:
147+
lst_missing.append('fid {}, geometry: {}, attributes: {}'.format(
148+
feat.id(),
149+
feat.geometry().constGet().asWkt(precision) if feat.geometry() else 'NULL',
150+
feat.attributes())
151+
)
152+
_TestCase.assertTrue(self, False, 'Some expected features not found in results:\n' + '\n'.join(lst_missing))
153+
else:
154+
return False
155+
156+
return True
157+
107158
def sort_by_pk_or_fid(f):
108159
if 'pk' in kwargs and kwargs['pk'] is not None:
109160
key = kwargs['pk']
@@ -118,88 +169,18 @@ def sort_by_pk_or_fid(f):
118169
result_features = sorted(layer_result.getFeatures(request), key=sort_by_pk_or_fid)
119170

120171
for feats in zip(expected_features, result_features):
121-
if feats[0].hasGeometry():
122-
geom0 = feats[0].geometry().constGet().asWkt(precision)
123-
else:
124-
geom0 = None
125-
if feats[1].hasGeometry():
126-
geom1 = feats[1].geometry().constGet().asWkt(precision)
127-
else:
128-
geom1 = None
129-
if use_asserts:
130-
_TestCase.assertEqual(
131-
self,
132-
geom0,
133-
geom1,
134-
'Features (Expected fid: {}, Result fid: {}) differ in geometry: \n\n Expected geometry:\n {}\n\n Result geometry:\n {}'.format(
135-
feats[0].id(),
136-
feats[1].id(),
137-
geom0,
138-
geom1
139-
)
140-
)
141-
elif geom0 != geom1:
142-
return False
143172

144-
for attr_expected, field_expected in zip(feats[0].attributes(), layer_expected.fields().toList()):
145-
try:
146-
cmp = compare['fields'][field_expected.name()]
147-
except KeyError:
148-
try:
149-
cmp = compare['fields']['__all__']
150-
except KeyError:
151-
cmp = {}
152-
153-
# Skip field
154-
if 'skip' in cmp:
155-
continue
156-
157-
if use_asserts:
158-
_TestCase.assertIn(
159-
self,
160-
field_expected.name().lower(),
161-
[name.lower() for name in feats[1].fields().names()])
162-
163-
attr_result = feats[1][field_expected.name()]
164-
field_result = [fld for fld in layer_expected.fields().toList() if fld.name() == field_expected.name()][0]
165-
166-
# Cast field to a given type
167-
if 'cast' in cmp:
168-
if cmp['cast'] == 'int':
169-
attr_expected = int(attr_expected) if attr_expected else None
170-
attr_result = int(attr_result) if attr_result else None
171-
if cmp['cast'] == 'float':
172-
attr_expected = float(attr_expected) if attr_expected else None
173-
attr_result = float(attr_result) if attr_result else None
174-
if cmp['cast'] == 'str':
175-
attr_expected = str(attr_expected) if attr_expected else None
176-
attr_result = str(attr_result) if attr_result else None
177-
178-
# Round field (only numeric so it works with __all__)
179-
if 'precision' in cmp and field_expected.type() in [QVariant.Int, QVariant.Double, QVariant.LongLong]:
180-
if not attr_expected == NULL:
181-
attr_expected = round(attr_expected, cmp['precision'])
182-
if not attr_result == NULL:
183-
attr_result = round(attr_result, cmp['precision'])
173+
eq = self.checkGeometriesEqual(feats[0].geometry(),
174+
feats[1].geometry(),
175+
feats[0].id(),
176+
feats[1].id(),
177+
use_asserts, precision, topo_equal_check)
178+
if not eq and not use_asserts:
179+
return False
184180

185-
if use_asserts:
186-
_TestCase.assertEqual(
187-
self,
188-
attr_expected,
189-
attr_result,
190-
'Features {}/{} differ in attributes\n\n * Field expected: {} ({})\n * result : {} ({})\n\n * Expected: {} != Result : {}'.format(
191-
feats[0].id(),
192-
feats[1].id(),
193-
field_expected.name(),
194-
field_expected.typeName(),
195-
field_result.name(),
196-
field_result.typeName(),
197-
repr(attr_expected),
198-
repr(attr_result)
199-
)
200-
)
201-
elif attr_expected != attr_result:
202-
return False
181+
eq = self.checkAttributesEqual(feats[0], feats[1], layer_expected.fields(), use_asserts, compare)
182+
if not eq and not use_asserts:
183+
return False
203184

204185
return True
205186

@@ -215,6 +196,103 @@ def assertFilesEqual(self, filepath_expected, filepath_result):
215196
diff = list(diff)
216197
self.assertEqual(0, len(diff), ''.join(diff))
217198

199+
def checkGeometriesEqual(self, geom0, geom1, geom0_id, geom1_id, use_asserts=False, precision=14, topo_equal_check=False):
200+
""" Checks whether two geometries are the same - using either a strict check of coordinates (up to given precision)
201+
or by using topological equality (where e.g. a polygon with clockwise is equal to a polygon with counter-clockwise
202+
order of vertices)
203+
.. versionadded:: 3.2
204+
"""
205+
if not geom0.isNull() and not geom1.isNull():
206+
if topo_equal_check:
207+
equal = geom0.isGeosEqual(geom1)
208+
else:
209+
equal = geom0.constGet().asWkt(precision) == geom1.constGet().asWkt(precision)
210+
elif geom0.isNull() and geom1.isNull():
211+
equal = True
212+
else:
213+
equal = False
214+
215+
if use_asserts:
216+
_TestCase.assertTrue(
217+
self,
218+
equal,
219+
'Features (Expected fid: {}, Result fid: {}) differ in geometry: \n\n Expected geometry:\n {}\n\n Result geometry:\n {}'.format(
220+
geom0_id,
221+
geom1_id,
222+
geom0.constGet().asWkt(precision) if not geom0.isNull() else 'NULL',
223+
geom1.constGet().asWkt(precision) if not geom1.isNull() else 'NULL'
224+
)
225+
)
226+
else:
227+
return equal
228+
229+
def checkAttributesEqual(self, feat0, feat1, fields_expected, use_asserts, compare):
230+
""" Checks whether attributes of two features are the same
231+
.. versionadded:: 3.2
232+
"""
233+
234+
for attr_expected, field_expected in zip(feat0.attributes(), fields_expected.toList()):
235+
try:
236+
cmp = compare['fields'][field_expected.name()]
237+
except KeyError:
238+
try:
239+
cmp = compare['fields']['__all__']
240+
except KeyError:
241+
cmp = {}
242+
243+
# Skip field
244+
if 'skip' in cmp:
245+
continue
246+
247+
if use_asserts:
248+
_TestCase.assertIn(
249+
self,
250+
field_expected.name().lower(),
251+
[name.lower() for name in feat1.fields().names()])
252+
253+
attr_result = feat1[field_expected.name()]
254+
field_result = [fld for fld in fields_expected.toList() if fld.name() == field_expected.name()][0]
255+
256+
# Cast field to a given type
257+
if 'cast' in cmp:
258+
if cmp['cast'] == 'int':
259+
attr_expected = int(attr_expected) if attr_expected else None
260+
attr_result = int(attr_result) if attr_result else None
261+
if cmp['cast'] == 'float':
262+
attr_expected = float(attr_expected) if attr_expected else None
263+
attr_result = float(attr_result) if attr_result else None
264+
if cmp['cast'] == 'str':
265+
attr_expected = str(attr_expected) if attr_expected else None
266+
attr_result = str(attr_result) if attr_result else None
267+
268+
# Round field (only numeric so it works with __all__)
269+
if 'precision' in cmp and field_expected.type() in [QVariant.Int, QVariant.Double, QVariant.LongLong]:
270+
if not attr_expected == NULL:
271+
attr_expected = round(attr_expected, cmp['precision'])
272+
if not attr_result == NULL:
273+
attr_result = round(attr_result, cmp['precision'])
274+
275+
if use_asserts:
276+
_TestCase.assertEqual(
277+
self,
278+
attr_expected,
279+
attr_result,
280+
'Features {}/{} differ in attributes\n\n * Field expected: {} ({})\n * result : {} ({})\n\n * Expected: {} != Result : {}'.format(
281+
feat0.id(),
282+
feat1.id(),
283+
field_expected.name(),
284+
field_expected.typeName(),
285+
field_result.name(),
286+
field_result.typeName(),
287+
repr(attr_expected),
288+
repr(attr_result)
289+
)
290+
)
291+
elif attr_expected != attr_result:
292+
return False
293+
294+
return True
295+
218296

219297
class _UnexpectedSuccess(Exception):
220298

‎src/analysis/processing/qgsalgorithmunion.cpp

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,25 +53,24 @@ QgsProcessingAlgorithm *QgsUnionAlgorithm::createInstance() const
5353
void QgsUnionAlgorithm::initAlgorithm( const QVariantMap & )
5454
{
5555
addParameter( new QgsProcessingParameterFeatureSource( QStringLiteral( "INPUT" ), QObject::tr( "Input layer" ) ) );
56-
addParameter( new QgsProcessingParameterFeatureSource( QStringLiteral( "OVERLAY" ), QObject::tr( "Union layer" ) ) );
56+
addParameter( new QgsProcessingParameterFeatureSource( QStringLiteral( "OVERLAY" ), QObject::tr( "Union layer" ), QList< int >(), QVariant(), true ) );
5757

5858
addParameter( new QgsProcessingParameterFeatureSink( QStringLiteral( "OUTPUT" ), QObject::tr( "Union" ) ) );
5959
}
6060

61-
6261
QVariantMap QgsUnionAlgorithm::processAlgorithm( const QVariantMap &parameters, QgsProcessingContext &context, QgsProcessingFeedback *feedback )
6362
{
6463
std::unique_ptr< QgsFeatureSource > sourceA( parameterAsSource( parameters, QStringLiteral( "INPUT" ), context ) );
6564
if ( !sourceA )
6665
throw QgsProcessingException( invalidSourceError( parameters, QStringLiteral( "INPUT" ) ) );
6766

6867
std::unique_ptr< QgsFeatureSource > sourceB( parameterAsSource( parameters, QStringLiteral( "OVERLAY" ), context ) );
69-
if ( !sourceB )
68+
if ( parameters.value( QStringLiteral( "OVERLAY" ) ).isValid() && !sourceB )
7069
throw QgsProcessingException( invalidSourceError( parameters, QStringLiteral( "OVERLAY" ) ) );
7170

7271
QgsWkbTypes::Type geomType = QgsWkbTypes::multiType( sourceA->wkbType() );
7372

74-
QgsFields fields = QgsProcessingUtils::combineFields( sourceA->fields(), sourceB->fields() );
73+
QgsFields fields = sourceB ? QgsProcessingUtils::combineFields( sourceA->fields(), sourceB->fields() ) : sourceA->fields();
7574

7675
QString dest;
7776
std::unique_ptr< QgsFeatureSink > sink( parameterAsSink( parameters, QStringLiteral( "OUTPUT" ), context, dest, fields, geomType, sourceA->sourceCrs() ) );
@@ -81,6 +80,13 @@ QVariantMap QgsUnionAlgorithm::processAlgorithm( const QVariantMap &parameters,
8180
QVariantMap outputs;
8281
outputs.insert( QStringLiteral( "OUTPUT" ), dest );
8382

83+
if ( !sourceB )
84+
{
85+
// we are doing single layer union
86+
QgsOverlayUtils::resolveOverlaps( *sourceA.get(), *sink.get(), feedback );
87+
return outputs;
88+
}
89+
8490
QList<int> fieldIndicesA = QgsProcessingUtils::fieldNamesToIndices( QStringList(), sourceA->fields() );
8591
QList<int> fieldIndicesB = QgsProcessingUtils::fieldNamesToIndices( QStringList(), sourceB->fields() );
8692

‎src/analysis/processing/qgsoverlayutils.cpp

Lines changed: 250 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,61 @@
2020

2121
///@cond PRIVATE
2222

23+
//! Makes sure that what came out from intersection of two geometries is good to be used in the output
24+
static bool sanitizeIntersectionResult( QgsGeometry &geom, QgsWkbTypes::GeometryType geometryType )
25+
{
26+
if ( geom.isNull() )
27+
{
28+
// TODO: not sure if this ever happens - if it does, that means GEOS failed badly - would be good to have a test for such situation
29+
throw QgsProcessingException( QStringLiteral( "%1\n\n%2" ).arg( QObject::tr( "GEOS geoprocessing error: intersection failed." ), geom.lastError() ) );
30+
}
31+
32+
// Intersection of geometries may give use also geometries we do not want in our results.
33+
// For example, two square polygons touching at the corner have a point as the intersection, but no area.
34+
// In other cases we may get a mixture of geometries in the output - we want to keep only the expected types.
35+
if ( QgsWkbTypes::flatType( geom.wkbType() ) == QgsWkbTypes::GeometryCollection )
36+
{
37+
// try to filter out irrelevant parts with different geometry type than what we want
38+
geom.convertGeometryCollectionToSubclass( geometryType );
39+
if ( geom.isEmpty() )
40+
return false;
41+
}
42+
43+
if ( QgsWkbTypes::geometryType( geom.wkbType() ) != geometryType )
44+
{
45+
// we can't make use of this resulting geometry
46+
return false;
47+
}
48+
49+
// some data providers are picky about the geometries we pass to them: we can't add single-part geometries
50+
// when we promised multi-part geometries, so ensure we have the right type
51+
geom.convertToMultiType();
52+
53+
return true;
54+
}
55+
56+
57+
//! Makes sure that what came out from difference of two geometries is good to be used in the output
58+
static bool sanitizeDifferenceResult( QgsGeometry &geom )
59+
{
60+
if ( geom.isNull() )
61+
{
62+
// TODO: not sure if this ever happens - if it does, that means GEOS failed badly - would be good to have a test for such situation
63+
throw QgsProcessingException( QStringLiteral( "%1\n\n%2" ).arg( QObject::tr( "GEOS geoprocessing error: difference failed." ), geom.lastError() ) );
64+
}
65+
66+
// if geomB covers the whole source geometry, we get an empty geometry collection
67+
if ( geom.isEmpty() )
68+
return false;
69+
70+
// some data providers are picky about the geometries we pass to them: we can't add single-part geometries
71+
// when we promised multi-part geometries, so ensure we have the right type
72+
geom.convertToMultiType();
73+
74+
return true;
75+
}
76+
77+
2378
void QgsOverlayUtils::difference( const QgsFeatureSource &sourceA, const QgsFeatureSource &sourceB, QgsFeatureSink &sink, QgsProcessingContext &context, QgsProcessingFeedback *feedback, int &count, int totalCount, QgsOverlayUtils::DifferenceOutput outputAttrs )
2479
{
2580
QgsFeatureRequest requestB;
@@ -83,8 +138,7 @@ void QgsOverlayUtils::difference( const QgsFeatureSource &sourceA, const QgsFeat
83138
geom = geom.difference( geomB );
84139
}
85140

86-
// if geomB covers the whole source geometry, we get an empty geometry collection
87-
if ( geom.isEmpty() )
141+
if ( !sanitizeDifferenceResult( geom ) )
88142
continue;
89143

90144
const QgsAttributes attrsA( featA.attributes() );
@@ -104,7 +158,6 @@ void QgsOverlayUtils::difference( const QgsFeatureSource &sourceA, const QgsFeat
104158
}
105159

106160
QgsFeature outFeat;
107-
geom.convertToMultiType();
108161
outFeat.setGeometry( geom );
109162
outFeat.setAttributes( attrs );
110163
sink.addFeature( outFeat, QgsFeatureSink::FastInsert );
@@ -179,43 +232,215 @@ void QgsOverlayUtils::intersection( const QgsFeatureSource &sourceA, const QgsFe
179232
continue;
180233

181234
QgsGeometry intGeom = geom.intersection( tmpGeom );
235+
if ( !sanitizeIntersectionResult( intGeom, geometryType ) )
236+
continue;
237+
238+
const QgsAttributes attrsB( featB.attributes() );
239+
for ( int i = 0; i < fieldIndicesB.count(); ++i )
240+
outAttributes[fieldIndicesA.count() + i] = attrsB[fieldIndicesB[i]];
241+
242+
outFeat.setGeometry( intGeom );
243+
outFeat.setAttributes( outAttributes );
244+
sink.addFeature( outFeat, QgsFeatureSink::FastInsert );
245+
}
246+
247+
++count;
248+
feedback->setProgress( count / ( double ) totalCount * 100. );
249+
}
250+
}
251+
252+
void QgsOverlayUtils::resolveOverlaps( const QgsFeatureSource &source, QgsFeatureSink &sink, QgsProcessingFeedback *feedback )
253+
{
254+
int count = 0;
255+
int totalCount = source.featureCount();
256+
if ( totalCount == 0 )
257+
return; // nothing to do here
258+
259+
QgsFeatureId newFid = -1;
260+
261+
QgsWkbTypes::GeometryType geometryType = QgsWkbTypes::geometryType( QgsWkbTypes::multiType( source.wkbType() ) );
262+
263+
QgsFeatureRequest requestOnlyGeoms;
264+
requestOnlyGeoms.setSubsetOfAttributes( QgsAttributeList() );
265+
266+
QgsFeatureRequest requestOnlyAttrs;
267+
requestOnlyAttrs.setFlags( QgsFeatureRequest::NoGeometry );
268+
269+
QgsFeatureRequest requestOnlyIds;
270+
requestOnlyIds.setFlags( QgsFeatureRequest::NoGeometry );
271+
requestOnlyIds.setSubsetOfAttributes( QgsAttributeList() );
272+
273+
// make a set of used feature IDs so that we do not try to reuse them for newly added features
274+
QgsFeature f;
275+
QSet<QgsFeatureId> fids;
276+
QgsFeatureIterator it = source.getFeatures( requestOnlyIds );
277+
while ( it.nextFeature( f ) )
278+
{
279+
if ( feedback->isCanceled() )
280+
return;
281+
282+
fids.insert( f.id() );
283+
}
284+
285+
QHash<QgsFeatureId, QgsGeometry> geometries;
286+
QgsSpatialIndex index;
287+
QHash<QgsFeatureId, QList<QgsFeatureId> > intersectingIds; // which features overlap a particular area
288+
289+
// resolve intersections
290+
291+
it = source.getFeatures( requestOnlyGeoms );
292+
while ( it.nextFeature( f ) )
293+
{
294+
if ( feedback->isCanceled() )
295+
return;
296+
297+
QgsFeatureId fid1 = f.id();
298+
QgsGeometry g1 = f.geometry();
299+
std::unique_ptr< QgsGeometryEngine > g1engine;
182300

183-
if ( intGeom.isNull() )
301+
geometries.insert( fid1, g1 );
302+
index.insertFeature( f );
303+
304+
QgsRectangle bbox( f.geometry().boundingBox() );
305+
const QList<QgsFeatureId> ids = index.intersects( bbox );
306+
for ( QgsFeatureId fid2 : ids )
307+
{
308+
if ( fid1 == fid2 )
309+
continue;
310+
311+
if ( !g1engine )
184312
{
185-
// TODO: not sure if this ever happens - if it does, that means GEOS failed badly - would be good to have a test for such situation
186-
throw QgsProcessingException( QStringLiteral( "%1\n\n%2" ).arg( QObject::tr( "GEOS geoprocessing error: intersection failed." ), intGeom.lastError() ) );
313+
// use prepared geometries for faster intersection tests
314+
g1engine.reset( QgsGeometry::createGeometryEngine( g1.constGet() ) );
315+
g1engine->prepareGeometry();
187316
}
188317

189-
// Intersection of geometries may give use also geometries we do not want in our results.
190-
// For example, two square polygons touching at the corner have a point as the intersection, but no area.
191-
// In other cases we may get a mixture of geometries in the output - we want to keep only the expected types.
192-
if ( QgsWkbTypes::flatType( intGeom.wkbType() ) == QgsWkbTypes::GeometryCollection )
318+
QgsGeometry g2 = geometries.value( fid2 );
319+
if ( !g1engine->intersects( g2.constGet() ) )
320+
continue;
321+
322+
QgsGeometry geomIntersection = g1.intersection( g2 );
323+
if ( !sanitizeIntersectionResult( geomIntersection, geometryType ) )
324+
continue;
325+
326+
//
327+
// add intersection geometry
328+
//
329+
330+
// figure out new fid
331+
while ( fids.contains( newFid ) )
332+
--newFid;
333+
fids.insert( newFid );
334+
335+
geometries.insert( newFid, geomIntersection );
336+
QgsFeature fx( newFid );
337+
fx.setGeometry( geomIntersection );
338+
339+
index.insertFeature( fx );
340+
341+
// figure out which feature IDs belong to this intersection. Some of the IDs can be of the newly
342+
// created geometries - in such case we need to retrieve original IDs
343+
QList<QgsFeatureId> lst;
344+
if ( intersectingIds.contains( fid1 ) )
345+
lst << intersectingIds.value( fid1 );
346+
else
347+
lst << fid1;
348+
if ( intersectingIds.contains( fid2 ) )
349+
lst << intersectingIds.value( fid2 );
350+
else
351+
lst << fid2;
352+
intersectingIds.insert( newFid, lst );
353+
354+
//
355+
// update f1
356+
//
357+
358+
QgsGeometry g12 = g1.difference( g2 );
359+
360+
index.deleteFeature( f );
361+
geometries.remove( fid1 );
362+
363+
if ( sanitizeDifferenceResult( g12 ) )
193364
{
194-
// try to filter out irrelevant parts with different geometry type than what we want
195-
intGeom.convertGeometryCollectionToSubclass( geometryType );
196-
if ( intGeom.isEmpty() )
197-
continue;
365+
geometries.insert( fid1, g12 );
366+
367+
QgsFeature f1x( fid1 );
368+
f1x.setGeometry( g12 );
369+
index.insertFeature( f1x );
198370
}
199371

200-
if ( QgsWkbTypes::geometryType( intGeom.wkbType() ) != geometryType )
372+
//
373+
// update f2
374+
//
375+
376+
QgsGeometry g21 = g2.difference( g1 );
377+
378+
QgsFeature f2old( fid2 );
379+
f2old.setGeometry( g2 );
380+
index.deleteFeature( f2old );
381+
382+
geometries.remove( fid2 );
383+
384+
if ( sanitizeDifferenceResult( g21 ) )
201385
{
202-
// we can't make use of this resulting geometry
203-
continue;
204-
}
386+
geometries.insert( fid2, g21 );
205387

206-
const QgsAttributes attrsB( featB.attributes() );
207-
for ( int i = 0; i < fieldIndicesB.count(); ++i )
208-
outAttributes[fieldIndicesA.count() + i] = attrsB[fieldIndicesB[i]];
388+
QgsFeature f2x( fid2 );
389+
f2x.setGeometry( g21 );
390+
index.insertFeature( f2x );
391+
}
209392

210-
intGeom.convertToMultiType();
211-
outFeat.setGeometry( intGeom );
212-
outFeat.setAttributes( outAttributes );
213-
sink.addFeature( outFeat, QgsFeatureSink::FastInsert );
393+
// update our temporary copy of the geometry to what is left from it
394+
g1 = g12;
395+
g1engine.reset();
214396
}
215397

216398
++count;
217399
feedback->setProgress( count / ( double ) totalCount * 100. );
218400
}
401+
402+
// release some memory of structures we don't need anymore
403+
404+
fids.clear();
405+
index = QgsSpatialIndex();
406+
407+
// load attributes
408+
409+
QHash<QgsFeatureId, QgsAttributes> attributesHash;
410+
it = source.getFeatures( requestOnlyAttrs );
411+
while ( it.nextFeature( f ) )
412+
{
413+
if ( feedback->isCanceled() )
414+
return;
415+
416+
attributesHash.insert( f.id(), f.attributes() );
417+
}
418+
419+
// store stuff in the sink
420+
421+
for ( auto i = geometries.constBegin(); i != geometries.constEnd(); ++i )
422+
{
423+
if ( feedback->isCanceled() )
424+
return;
425+
426+
QgsFeature outFeature( i.key() );
427+
outFeature.setGeometry( i.value() );
428+
429+
if ( intersectingIds.contains( i.key() ) )
430+
{
431+
const QList<QgsFeatureId> ids = intersectingIds.value( i.key() );
432+
for ( QgsFeatureId id : ids )
433+
{
434+
outFeature.setAttributes( attributesHash.value( id ) );
435+
sink.addFeature( outFeature, QgsFeatureSink::FastInsert );
436+
}
437+
}
438+
else
439+
{
440+
outFeature.setAttributes( attributesHash.value( i.key() ) );
441+
sink.addFeature( outFeature, QgsFeatureSink::FastInsert );
442+
}
443+
}
219444
}
220445

221446
///@endcond PRIVATE

‎src/analysis/processing/qgsoverlayutils.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,16 @@ namespace QgsOverlayUtils
4343

4444
void intersection( const QgsFeatureSource &sourceA, const QgsFeatureSource &sourceB, QgsFeatureSink &sink, QgsProcessingContext &context, QgsProcessingFeedback *feedback, int &count, int totalCount, const QList<int> &fieldIndicesA, const QList<int> &fieldIndicesB );
4545

46+
/**
47+
* Copies features from the source to the sink and resolves overlaps: for each pair of overlapping features A and B
48+
* it will produce:
49+
* 1. a feature with geometry A - B with A's attributes
50+
* 2. a feature with geometry B - A with B's attributes
51+
* 3. two features with geometry intersection(A, B) - one with A's attributes, one with B's attributes.
52+
*
53+
* As a result, for all pairs of features in the output, a pair either has no common interior or their interior is the same.
54+
*/
55+
void resolveOverlaps( const QgsFeatureSource &source, QgsFeatureSink &sink, QgsProcessingFeedback *feedback );
4656
}
4757

4858
///@endcond PRIVATE

0 commit comments

Comments
 (0)
Please sign in to comment.