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