Skip to content

Commit 64b8c72

Browse files
committed
[FEATURE] Union algorithm for single layer
Resolves all overlapping geometries just like GRASS or Arc do. So now we have two variants of union: - union(A) - does union within geometries of one layer - union(A,B) - does union between geometries of two layers For union(A,B) algorithm if there are overlaps among geometries of layer A or among geometries of layer B, these are not resolved: one needs to do union(union(A,B)) to resolve all overlaps, i.e. run single layer union(X) on the produced result X=union(A,B) This should also address issues raised in #17131
1 parent c738bcf commit 64b8c72

File tree

3 files changed

+262
-31
lines changed

3 files changed

+262
-31
lines changed

src/analysis/processing/qgsalgorithmunion.cpp

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,25 +53,23 @@ 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 )
70-
throw QgsProcessingException( invalidSourceError( parameters, QStringLiteral( "OVERLAY" ) ) );
68+
// sourceB is optional so we do not throw an error if it is not a valid source
7169

7270
QgsWkbTypes::Type geomType = QgsWkbTypes::multiType( sourceA->wkbType() );
7371

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

7674
QString dest;
7775
std::unique_ptr< QgsFeatureSink > sink( parameterAsSink( parameters, QStringLiteral( "OUTPUT" ), context, dest, fields, geomType, sourceA->sourceCrs() ) );
@@ -81,6 +79,13 @@ QVariantMap QgsUnionAlgorithm::processAlgorithm( const QVariantMap &parameters,
8179
QVariantMap outputs;
8280
outputs.insert( QStringLiteral( "OUTPUT" ), dest );
8381

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

src/analysis/processing/qgsoverlayutils.cpp

Lines changed: 242 additions & 26 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,35 +232,13 @@ void QgsOverlayUtils::intersection( const QgsFeatureSource &sourceA, const QgsFe
179232
continue;
180233

181234
QgsGeometry intGeom = geom.intersection( tmpGeom );
182-
183-
if ( intGeom.isNull() )
184-
{
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() ) );
187-
}
188-
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 )
193-
{
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;
198-
}
199-
200-
if ( QgsWkbTypes::geometryType( intGeom.wkbType() ) != geometryType )
201-
{
202-
// we can't make use of this resulting geometry
235+
if ( !sanitizeIntersectionResult( intGeom, geometryType ) )
203236
continue;
204-
}
205237

206238
const QgsAttributes attrsB( featB.attributes() );
207239
for ( int i = 0; i < fieldIndicesB.count(); ++i )
208240
outAttributes[fieldIndicesA.count() + i] = attrsB[fieldIndicesB[i]];
209241

210-
intGeom.convertToMultiType();
211242
outFeat.setGeometry( intGeom );
212243
outFeat.setAttributes( outAttributes );
213244
sink.addFeature( outFeat, QgsFeatureSink::FastInsert );
@@ -218,4 +249,189 @@ void QgsOverlayUtils::intersection( const QgsFeatureSource &sourceA, const QgsFe
218249
}
219250
}
220251

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 they 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+
300+
geometries.insert( fid1, g1 );
301+
index.insertFeature( f );
302+
303+
QgsRectangle bbox( f.geometry().boundingBox() );
304+
const QList<QgsFeatureId> ids = index.intersects( bbox );
305+
for ( QgsFeatureId fid2 : ids )
306+
{
307+
if ( fid1 == fid2 )
308+
continue;
309+
310+
QgsGeometry g2 = geometries.value( fid2 );
311+
if ( !g1.intersects( g2 ) )
312+
continue;
313+
314+
QgsGeometry geomIntersection = g1.intersection( g2 );
315+
if ( !sanitizeIntersectionResult( geomIntersection, geometryType ) )
316+
continue;
317+
318+
//
319+
// add intersection geometry
320+
//
321+
322+
// figure out new fid
323+
while ( fids.contains( newFid ) )
324+
--newFid;
325+
fids.insert( newFid );
326+
327+
geometries.insert( newFid, geomIntersection );
328+
QgsFeature fx( newFid );
329+
fx.setGeometry( geomIntersection );
330+
331+
index.insertFeature( fx );
332+
333+
// figure out which feature IDs belong to this intersection. Some of the IDs can be of the newly
334+
// created geometries - in such case we need to retrieve original IDs
335+
QList<QgsFeatureId> lst;
336+
if ( intersectingIds.contains( fid1 ) )
337+
lst << intersectingIds.value( fid1 );
338+
else
339+
lst << fid1;
340+
if ( intersectingIds.contains( fid2 ) )
341+
lst << intersectingIds.value( fid2 );
342+
else
343+
lst << fid2;
344+
intersectingIds.insert( newFid, lst );
345+
346+
//
347+
// update f1
348+
//
349+
350+
QgsGeometry g12 = g1.difference( g2 );
351+
352+
index.deleteFeature( f );
353+
geometries.remove( fid1 );
354+
355+
if ( sanitizeDifferenceResult( g12 ) )
356+
{
357+
geometries.insert( fid1, g12 );
358+
359+
QgsFeature f1x( fid1 );
360+
f1x.setGeometry( g12 );
361+
index.insertFeature( f1x );
362+
}
363+
364+
//
365+
// update f2
366+
//
367+
368+
QgsGeometry g21 = g2.difference( g1 );
369+
370+
QgsFeature f2old( fid2 );
371+
f2old.setGeometry( g2 );
372+
index.deleteFeature( f2old );
373+
374+
geometries.remove( fid2 );
375+
376+
if ( sanitizeDifferenceResult( g21 ) )
377+
{
378+
geometries.insert( fid2, g21 );
379+
380+
QgsFeature f2x( fid2 );
381+
f2x.setGeometry( g21 );
382+
index.insertFeature( f2x );
383+
}
384+
385+
// update our temporary copy of the geometry to what is left from it
386+
g1 = g12;
387+
}
388+
389+
++count;
390+
feedback->setProgress( count / ( double ) totalCount * 100. );
391+
}
392+
393+
// release some memory of structures we don't need anymore
394+
395+
fids.clear();
396+
index = QgsSpatialIndex();
397+
398+
// load attributes
399+
400+
QHash<QgsFeatureId, QgsAttributes> attributesHash;
401+
it = source.getFeatures( requestOnlyAttrs );
402+
while ( it.nextFeature( f ) )
403+
{
404+
if ( feedback->isCanceled() )
405+
return;
406+
407+
attributesHash.insert( f.id(), f.attributes() );
408+
}
409+
410+
// store stuff in the sink
411+
412+
for ( auto i = geometries.constBegin(); i != geometries.constEnd(); ++i )
413+
{
414+
if ( feedback->isCanceled() )
415+
return;
416+
417+
QgsFeature outFeature( i.key() );
418+
outFeature.setGeometry( i.value() );
419+
420+
if ( intersectingIds.contains( i.key() ) )
421+
{
422+
const QList<QgsFeatureId> ids = intersectingIds.value( i.key() );
423+
for ( QgsFeatureId id : ids )
424+
{
425+
outFeature.setAttributes( attributesHash.value( id ) );
426+
sink.addFeature( outFeature, QgsFeatureSink::FastInsert );
427+
}
428+
}
429+
else
430+
{
431+
outFeature.setAttributes( attributesHash.value( i.key() ) );
432+
sink.addFeature( outFeature, QgsFeatureSink::FastInsert );
433+
}
434+
}
435+
}
436+
221437
///@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 havs 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)