1 /***************************************************************************
2 qgsoverlayutils.cpp
3 ---------------------
4 Date : April 2018
5 Copyright : (C) 2018 by Martin Dobias
6 Email : wonder dot sk at gmail dot com
7 ***************************************************************************
8 * *
9 * This program is free software; you can redistribute it and/or modify *
10 * it under the terms of the GNU General Public License as published by *
11 * the Free Software Foundation; either version 2 of the License, or *
12 * (at your option) any later version. *
13 * *
14 ***************************************************************************/
15
16 #include "qgsoverlayutils.h"
17
18 #include "qgsgeometryengine.h"
19 #include "qgsprocessingalgorithm.h"
20
21 ///@cond PRIVATE
22
sanitizeIntersectionResult(QgsGeometry & geom,QgsWkbTypes::GeometryType geometryType)23 bool QgsOverlayUtils::sanitizeIntersectionResult( QgsGeometry &geom, QgsWkbTypes::GeometryType geometryType )
24 {
25 if ( geom.isNull() )
26 {
27 // 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
28 throw QgsProcessingException( QStringLiteral( "%1\n\n%2" ).arg( QObject::tr( "GEOS geoprocessing error: intersection failed." ), geom.lastError() ) );
29 }
30
31 // Intersection of geometries may give use also geometries we do not want in our results.
32 // For example, two square polygons touching at the corner have a point as the intersection, but no area.
33 // In other cases we may get a mixture of geometries in the output - we want to keep only the expected types.
34 if ( QgsWkbTypes::flatType( geom.wkbType() ) == QgsWkbTypes::GeometryCollection )
35 {
36 // try to filter out irrelevant parts with different geometry type than what we want
37 geom.convertGeometryCollectionToSubclass( geometryType );
38 if ( geom.isEmpty() )
39 return false;
40 }
41
42 if ( QgsWkbTypes::geometryType( geom.wkbType() ) != geometryType )
43 {
44 // we can't make use of this resulting geometry
45 return false;
46 }
47
48 // some data providers are picky about the geometries we pass to them: we can't add single-part geometries
49 // when we promised multi-part geometries, so ensure we have the right type
50 geom.convertToMultiType();
51
52 return true;
53 }
54
55
56 //! Makes sure that what came out from difference of two geometries is good to be used in the output
sanitizeDifferenceResult(QgsGeometry & geom,QgsWkbTypes::GeometryType geometryType)57 static bool sanitizeDifferenceResult( QgsGeometry &geom, QgsWkbTypes::GeometryType geometryType )
58 {
59 if ( geom.isNull() )
60 {
61 // 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
62 throw QgsProcessingException( QStringLiteral( "%1\n\n%2" ).arg( QObject::tr( "GEOS geoprocessing error: difference failed." ), geom.lastError() ) );
63 }
64
65 //fix geometry collections
66 if ( QgsWkbTypes::flatType( geom.wkbType() ) == QgsWkbTypes::GeometryCollection )
67 {
68 // try to filter out irrelevant parts with different geometry type than what we want
69 geom.convertGeometryCollectionToSubclass( geometryType );
70 }
71
72 // if geomB covers the whole source geometry, we get an empty geometry collection
73 if ( geom.isEmpty() )
74 return false;
75
76 // some data providers are picky about the geometries we pass to them: we can't add single-part geometries
77 // when we promised multi-part geometries, so ensure we have the right type
78 geom.convertToMultiType();
79
80 return true;
81 }
82
83
difference(const QgsFeatureSource & sourceA,const QgsFeatureSource & sourceB,QgsFeatureSink & sink,QgsProcessingContext & context,QgsProcessingFeedback * feedback,long & count,long totalCount,QgsOverlayUtils::DifferenceOutput outputAttrs)84 void QgsOverlayUtils::difference( const QgsFeatureSource &sourceA, const QgsFeatureSource &sourceB, QgsFeatureSink &sink, QgsProcessingContext &context, QgsProcessingFeedback *feedback, long &count, long totalCount, QgsOverlayUtils::DifferenceOutput outputAttrs )
85 {
86 QgsWkbTypes::GeometryType geometryType = QgsWkbTypes::geometryType( QgsWkbTypes::multiType( sourceA.wkbType() ) );
87 QgsFeatureRequest requestB;
88 requestB.setNoAttributes();
89 if ( outputAttrs != OutputBA )
90 requestB.setDestinationCrs( sourceA.sourceCrs(), context.transformContext() );
91 QgsSpatialIndex indexB( sourceB.getFeatures( requestB ), feedback );
92 if ( feedback->isCanceled() )
93 return;
94
95 int fieldsCountA = sourceA.fields().count();
96 int fieldsCountB = sourceB.fields().count();
97 QgsAttributes attrs;
98 attrs.resize( outputAttrs == OutputA ? fieldsCountA : ( fieldsCountA + fieldsCountB ) );
99
100 if ( totalCount == 0 )
101 totalCount = 1; // avoid division by zero
102
103 QgsFeature featA;
104 QgsFeatureRequest requestA;
105 requestA.setInvalidGeometryCheck( context.invalidGeometryCheck() );
106 if ( outputAttrs == OutputBA )
107 requestA.setDestinationCrs( sourceB.sourceCrs(), context.transformContext() );
108 QgsFeatureIterator fitA = sourceA.getFeatures( requestA );
109 while ( fitA.nextFeature( featA ) )
110 {
111 if ( feedback->isCanceled() )
112 break;
113
114 if ( featA.hasGeometry() )
115 {
116 QgsGeometry geom( featA.geometry() );
117 QgsFeatureIds intersects = qgis::listToSet( indexB.intersects( geom.boundingBox() ) );
118
119 QgsFeatureRequest request;
120 request.setFilterFids( intersects );
121 request.setNoAttributes();
122 if ( outputAttrs != OutputBA )
123 request.setDestinationCrs( sourceA.sourceCrs(), context.transformContext() );
124
125 std::unique_ptr< QgsGeometryEngine > engine;
126 if ( !intersects.isEmpty() )
127 {
128 // use prepared geometries for faster intersection tests
129 engine.reset( QgsGeometry::createGeometryEngine( geom.constGet() ) );
130 engine->prepareGeometry();
131 }
132
133 QVector<QgsGeometry> geometriesB;
134 QgsFeature featB;
135 QgsFeatureIterator fitB = sourceB.getFeatures( request );
136 while ( fitB.nextFeature( featB ) )
137 {
138 if ( feedback->isCanceled() )
139 break;
140
141 if ( engine->intersects( featB.geometry().constGet() ) )
142 geometriesB << featB.geometry();
143 }
144
145 if ( !geometriesB.isEmpty() )
146 {
147 QgsGeometry geomB = QgsGeometry::unaryUnion( geometriesB );
148 if ( !geomB.lastError().isEmpty() )
149 {
150 // This may happen if input geometries from a layer do not line up well (for example polygons
151 // that are nearly touching each other, but there is a very tiny overlap or gap at one of the edges).
152 // It is possible to get rid of this issue in two steps:
153 // 1. snap geometries with a small tolerance (e.g. 1cm) using QgsGeometrySnapperSingleSource
154 // 2. fix geometries (removes polygons collapsed to lines etc.) using MakeValid
155 throw QgsProcessingException( QStringLiteral( "%1\n\n%2" ).arg( QObject::tr( "GEOS geoprocessing error: unary union failed." ), geomB.lastError() ) );
156 }
157 geom = geom.difference( geomB );
158 }
159
160 if ( !sanitizeDifferenceResult( geom, geometryType ) )
161 continue;
162
163 const QgsAttributes attrsA( featA.attributes() );
164 switch ( outputAttrs )
165 {
166 case OutputA:
167 attrs = attrsA;
168 break;
169 case OutputAB:
170 for ( int i = 0; i < fieldsCountA; ++i )
171 attrs[i] = attrsA[i];
172 break;
173 case OutputBA:
174 for ( int i = 0; i < fieldsCountA; ++i )
175 attrs[i + fieldsCountB] = attrsA[i];
176 break;
177 }
178
179 QgsFeature outFeat;
180 outFeat.setGeometry( geom );
181 outFeat.setAttributes( attrs );
182 sink.addFeature( outFeat, QgsFeatureSink::FastInsert );
183 }
184 else
185 {
186 // TODO: should we write out features that do not have geometry?
187 sink.addFeature( featA, QgsFeatureSink::FastInsert );
188 }
189
190 ++count;
191 feedback->setProgress( count / static_cast< double >( totalCount ) * 100. );
192 }
193 }
194
195
intersection(const QgsFeatureSource & sourceA,const QgsFeatureSource & sourceB,QgsFeatureSink & sink,QgsProcessingContext & context,QgsProcessingFeedback * feedback,long & count,long totalCount,const QList<int> & fieldIndicesA,const QList<int> & fieldIndicesB)196 void QgsOverlayUtils::intersection( const QgsFeatureSource &sourceA, const QgsFeatureSource &sourceB, QgsFeatureSink &sink, QgsProcessingContext &context, QgsProcessingFeedback *feedback, long &count, long totalCount, const QList<int> &fieldIndicesA, const QList<int> &fieldIndicesB )
197 {
198 QgsWkbTypes::GeometryType geometryType = QgsWkbTypes::geometryType( QgsWkbTypes::multiType( sourceA.wkbType() ) );
199 int attrCount = fieldIndicesA.count() + fieldIndicesB.count();
200
201 QgsFeatureRequest request;
202 request.setNoAttributes();
203 request.setDestinationCrs( sourceA.sourceCrs(), context.transformContext() );
204
205 QgsFeature outFeat;
206 QgsSpatialIndex indexB( sourceB.getFeatures( request ), feedback );
207 if ( feedback->isCanceled() )
208 return;
209
210 if ( totalCount == 0 )
211 totalCount = 1; // avoid division by zero
212
213 QgsFeature featA;
214 QgsFeatureIterator fitA = sourceA.getFeatures( QgsFeatureRequest().setSubsetOfAttributes( fieldIndicesA ) );
215 while ( fitA.nextFeature( featA ) )
216 {
217 if ( feedback->isCanceled() )
218 break;
219
220 if ( !featA.hasGeometry() )
221 continue;
222
223 QgsGeometry geom( featA.geometry() );
224 QgsFeatureIds intersects = qgis::listToSet( indexB.intersects( geom.boundingBox() ) );
225
226 QgsFeatureRequest request;
227 request.setFilterFids( intersects );
228 request.setDestinationCrs( sourceA.sourceCrs(), context.transformContext() );
229 request.setSubsetOfAttributes( fieldIndicesB );
230
231 std::unique_ptr< QgsGeometryEngine > engine;
232 if ( !intersects.isEmpty() )
233 {
234 // use prepared geometries for faster intersection tests
235 engine.reset( QgsGeometry::createGeometryEngine( geom.constGet() ) );
236 engine->prepareGeometry();
237 }
238
239 QgsAttributes outAttributes( attrCount );
240 const QgsAttributes attrsA( featA.attributes() );
241 for ( int i = 0; i < fieldIndicesA.count(); ++i )
242 outAttributes[i] = attrsA[fieldIndicesA[i]];
243
244 QgsFeature featB;
245 QgsFeatureIterator fitB = sourceB.getFeatures( request );
246 while ( fitB.nextFeature( featB ) )
247 {
248 if ( feedback->isCanceled() )
249 break;
250
251 QgsGeometry tmpGeom( featB.geometry() );
252 if ( !engine->intersects( tmpGeom.constGet() ) )
253 continue;
254
255 QgsGeometry intGeom = geom.intersection( tmpGeom );
256 if ( !sanitizeIntersectionResult( intGeom, geometryType ) )
257 continue;
258
259 const QgsAttributes attrsB( featB.attributes() );
260 for ( int i = 0; i < fieldIndicesB.count(); ++i )
261 outAttributes[fieldIndicesA.count() + i] = attrsB[fieldIndicesB[i]];
262
263 outFeat.setGeometry( intGeom );
264 outFeat.setAttributes( outAttributes );
265 sink.addFeature( outFeat, QgsFeatureSink::FastInsert );
266 }
267
268 ++count;
269 feedback->setProgress( count / static_cast<double >( totalCount ) * 100. );
270 }
271 }
272
resolveOverlaps(const QgsFeatureSource & source,QgsFeatureSink & sink,QgsProcessingFeedback * feedback)273 void QgsOverlayUtils::resolveOverlaps( const QgsFeatureSource &source, QgsFeatureSink &sink, QgsProcessingFeedback *feedback )
274 {
275 long count = 0;
276 const long totalCount = source.featureCount();
277 if ( totalCount == 0 )
278 return; // nothing to do here
279
280 QgsFeatureId newFid = -1;
281
282 QgsWkbTypes::GeometryType geometryType = QgsWkbTypes::geometryType( QgsWkbTypes::multiType( source.wkbType() ) );
283
284 QgsFeatureRequest requestOnlyGeoms;
285 requestOnlyGeoms.setNoAttributes();
286
287 QgsFeatureRequest requestOnlyAttrs;
288 requestOnlyAttrs.setFlags( QgsFeatureRequest::NoGeometry );
289
290 QgsFeatureRequest requestOnlyIds;
291 requestOnlyIds.setFlags( QgsFeatureRequest::NoGeometry );
292 requestOnlyIds.setNoAttributes();
293
294 // make a set of used feature IDs so that we do not try to reuse them for newly added features
295 QgsFeature f;
296 QSet<QgsFeatureId> fids;
297 QgsFeatureIterator it = source.getFeatures( requestOnlyIds );
298 while ( it.nextFeature( f ) )
299 {
300 if ( feedback->isCanceled() )
301 return;
302
303 fids.insert( f.id() );
304 }
305
306 QHash<QgsFeatureId, QgsGeometry> geometries;
307 QgsSpatialIndex index;
308 QHash<QgsFeatureId, QList<QgsFeatureId> > intersectingIds; // which features overlap a particular area
309
310 // resolve intersections
311
312 it = source.getFeatures( requestOnlyGeoms );
313 while ( it.nextFeature( f ) )
314 {
315 if ( feedback->isCanceled() )
316 return;
317
318 QgsFeatureId fid1 = f.id();
319 QgsGeometry g1 = f.geometry();
320 std::unique_ptr< QgsGeometryEngine > g1engine;
321
322 geometries.insert( fid1, g1 );
323 index.addFeature( f );
324
325 QgsRectangle bbox( f.geometry().boundingBox() );
326 const QList<QgsFeatureId> ids = index.intersects( bbox );
327 for ( QgsFeatureId fid2 : ids )
328 {
329 if ( fid1 == fid2 )
330 continue;
331
332 if ( !g1engine )
333 {
334 // use prepared geometries for faster intersection tests
335 g1engine.reset( QgsGeometry::createGeometryEngine( g1.constGet() ) );
336 g1engine->prepareGeometry();
337 }
338
339 QgsGeometry g2 = geometries.value( fid2 );
340 if ( !g1engine->intersects( g2.constGet() ) )
341 continue;
342
343 QgsGeometry geomIntersection = g1.intersection( g2 );
344 if ( !sanitizeIntersectionResult( geomIntersection, geometryType ) )
345 continue;
346
347 //
348 // add intersection geometry
349 //
350
351 // figure out new fid
352 while ( fids.contains( newFid ) )
353 --newFid;
354 fids.insert( newFid );
355
356 geometries.insert( newFid, geomIntersection );
357 QgsFeature fx( newFid );
358 fx.setGeometry( geomIntersection );
359
360 index.addFeature( fx );
361
362 // figure out which feature IDs belong to this intersection. Some of the IDs can be of the newly
363 // created geometries - in such case we need to retrieve original IDs
364 QList<QgsFeatureId> lst;
365 if ( intersectingIds.contains( fid1 ) )
366 lst << intersectingIds.value( fid1 );
367 else
368 lst << fid1;
369 if ( intersectingIds.contains( fid2 ) )
370 lst << intersectingIds.value( fid2 );
371 else
372 lst << fid2;
373 intersectingIds.insert( newFid, lst );
374
375 //
376 // update f1
377 //
378
379 QgsGeometry g12 = g1.difference( g2 );
380
381 index.deleteFeature( f );
382 geometries.remove( fid1 );
383
384 if ( sanitizeDifferenceResult( g12, geometryType ) )
385 {
386 geometries.insert( fid1, g12 );
387
388 QgsFeature f1x( fid1 );
389 f1x.setGeometry( g12 );
390 index.addFeature( f1x );
391 }
392
393 //
394 // update f2
395 //
396
397 QgsGeometry g21 = g2.difference( g1 );
398
399 QgsFeature f2old( fid2 );
400 f2old.setGeometry( g2 );
401 index.deleteFeature( f2old );
402
403 geometries.remove( fid2 );
404
405 if ( sanitizeDifferenceResult( g21, geometryType ) )
406 {
407 geometries.insert( fid2, g21 );
408
409 QgsFeature f2x( fid2 );
410 f2x.setGeometry( g21 );
411 index.addFeature( f2x );
412 }
413
414 // update our temporary copy of the geometry to what is left from it
415 g1 = g12;
416 g1engine.reset();
417 }
418
419 ++count;
420 feedback->setProgress( count / static_cast< double >( totalCount ) * 100. );
421 }
422 if ( feedback->isCanceled() )
423 return;
424
425 // release some memory of structures we don't need anymore
426
427 fids.clear();
428 index = QgsSpatialIndex();
429
430 // load attributes
431
432 QHash<QgsFeatureId, QgsAttributes> attributesHash;
433 it = source.getFeatures( requestOnlyAttrs );
434 while ( it.nextFeature( f ) )
435 {
436 if ( feedback->isCanceled() )
437 return;
438
439 attributesHash.insert( f.id(), f.attributes() );
440 }
441
442 // store stuff in the sink
443
444 for ( auto i = geometries.constBegin(); i != geometries.constEnd(); ++i )
445 {
446 if ( feedback->isCanceled() )
447 return;
448
449 QgsFeature outFeature( i.key() );
450 outFeature.setGeometry( i.value() );
451
452 if ( intersectingIds.contains( i.key() ) )
453 {
454 const QList<QgsFeatureId> ids = intersectingIds.value( i.key() );
455 for ( QgsFeatureId id : ids )
456 {
457 outFeature.setAttributes( attributesHash.value( id ) );
458 sink.addFeature( outFeature, QgsFeatureSink::FastInsert );
459 }
460 }
461 else
462 {
463 outFeature.setAttributes( attributesHash.value( i.key() ) );
464 sink.addFeature( outFeature, QgsFeatureSink::FastInsert );
465 }
466 }
467 }
468
469 ///@endcond PRIVATE
470