1 /***************************************************************************
2     qgsgeometrygapcheck.cpp
3     ---------------------
4     begin                : September 2015
5     copyright            : (C) 2014 by Sandro Mani / Sourcepole AG
6     email                : smani at sourcepole dot ch
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 "qgsgeometrycheckcontext.h"
17 #include "qgsgeometryengine.h"
18 #include "qgsgeometrygapcheck.h"
19 #include "qgsgeometrycollection.h"
20 #include "qgsfeaturepool.h"
21 #include "qgsvectorlayer.h"
22 #include "qgsvectorlayerutils.h"
23 #include "qgsfeedback.h"
24 #include "qgsapplication.h"
25 #include "qgsproject.h"
26 #include "qgsexpressioncontextutils.h"
27 #include "qgspolygon.h"
28 #include "qgscurve.h"
29 #include "qgssnappingutils.h"
30 
31 #include "geos_c.h"
32 
QgsGeometryGapCheck(const QgsGeometryCheckContext * context,const QVariantMap & configuration)33 QgsGeometryGapCheck::QgsGeometryGapCheck( const QgsGeometryCheckContext *context, const QVariantMap &configuration )
34   : QgsGeometryCheck( context, configuration )
35   ,  mGapThresholdMapUnits( configuration.value( QStringLiteral( "gapThreshold" ) ).toDouble() )
36 {
37 }
38 
prepare(const QgsGeometryCheckContext * context,const QVariantMap & configuration)39 void QgsGeometryGapCheck::prepare( const QgsGeometryCheckContext *context, const QVariantMap &configuration )
40 {
41   if ( configuration.value( QStringLiteral( "allowedGapsEnabled" ) ).toBool() )
42   {
43     QgsVectorLayer *layer = context->project()->mapLayer<QgsVectorLayer *>( configuration.value( "allowedGapsLayer" ).toString() );
44     if ( layer )
45     {
46       mAllowedGapsLayer = layer;
47       mAllowedGapsSource = qgis::make_unique<QgsVectorLayerFeatureSource>( layer );
48 
49       mAllowedGapsBuffer = configuration.value( QStringLiteral( "allowedGapsBuffer" ) ).toDouble();
50     }
51   }
52   else
53   {
54     mAllowedGapsSource.reset();
55   }
56 }
57 
collectErrors(const QMap<QString,QgsFeaturePool * > & featurePools,QList<QgsGeometryCheckError * > & errors,QStringList & messages,QgsFeedback * feedback,const LayerFeatureIds & ids) const58 void QgsGeometryGapCheck::collectErrors( const QMap<QString, QgsFeaturePool *> &featurePools, QList<QgsGeometryCheckError *> &errors, QStringList &messages, QgsFeedback *feedback, const LayerFeatureIds &ids ) const
59 {
60   if ( feedback )
61     feedback->setProgress( feedback->progress() + 1.0 );
62 
63   std::unique_ptr<QgsAbstractGeometry> allowedGapsGeom;
64   std::unique_ptr<QgsGeometryEngine> allowedGapsGeomEngine;
65 
66   if ( mAllowedGapsSource )
67   {
68     QVector<QgsGeometry> allowedGaps;
69     QgsFeatureRequest request;
70     request.setSubsetOfAttributes( QgsAttributeList() );
71     QgsFeatureIterator iterator = mAllowedGapsSource->getFeatures( request );
72     QgsFeature feature;
73 
74     while ( iterator.nextFeature( feature ) )
75     {
76       QgsGeometry geom = feature.geometry();
77       QgsGeometry gg = geom.buffer( mAllowedGapsBuffer, 20 );
78       allowedGaps.append( gg );
79     }
80 
81     std::unique_ptr< QgsGeometryEngine > allowedGapsEngine = QgsGeometryCheckerUtils::createGeomEngine( nullptr, mContext->tolerance );
82 
83     // Create union of allowed gaps
84     QString errMsg;
85     allowedGapsGeom.reset( allowedGapsEngine->combine( allowedGaps, &errMsg ) );
86     allowedGapsGeomEngine = QgsGeometryCheckerUtils::createGeomEngine( allowedGapsGeom.get(), mContext->tolerance );
87     allowedGapsGeomEngine->prepareGeometry();
88   }
89 
90   QVector<QgsGeometry> geomList;
91   QMap<QString, QgsFeatureIds> featureIds = ids.isEmpty() ? allLayerFeatureIds( featurePools ) : ids.toMap();
92   const QgsGeometryCheckerUtils::LayerFeatures layerFeatures( featurePools, featureIds, compatibleGeometryTypes(), nullptr, mContext, true );
93   for ( const QgsGeometryCheckerUtils::LayerFeature &layerFeature : layerFeatures )
94   {
95     geomList.append( layerFeature.geometry() );
96 
97     if ( feedback && feedback->isCanceled() )
98     {
99       geomList.clear();
100       break;
101     }
102   }
103 
104   if ( geomList.isEmpty() )
105   {
106     return;
107   }
108 
109   std::unique_ptr< QgsGeometryEngine > geomEngine = QgsGeometryCheckerUtils::createGeomEngine( nullptr, mContext->tolerance );
110 
111   // Create union of geometry
112   QString errMsg;
113   std::unique_ptr<QgsAbstractGeometry> unionGeom( geomEngine->combine( geomList, &errMsg ) );
114   if ( !unionGeom )
115   {
116     messages.append( tr( "Gap check: %1" ).arg( errMsg ) );
117     return;
118   }
119 
120   // Get envelope of union
121   geomEngine = QgsGeometryCheckerUtils::createGeomEngine( unionGeom.get(), mContext->tolerance );
122   geomEngine->prepareGeometry();
123   std::unique_ptr<QgsAbstractGeometry> envelope( geomEngine->envelope( &errMsg ) );
124   if ( !envelope )
125   {
126     messages.append( tr( "Gap check: %1" ).arg( errMsg ) );
127     return;
128   }
129 
130   // Buffer envelope
131   geomEngine = QgsGeometryCheckerUtils::createGeomEngine( envelope.get(), mContext->tolerance );
132   geomEngine->prepareGeometry();
133   QgsAbstractGeometry *bufEnvelope = geomEngine->buffer( 2, 0, GEOSBUF_CAP_SQUARE, GEOSBUF_JOIN_MITRE, 4. );  //#spellok  //#spellok
134   envelope.reset( bufEnvelope );
135 
136   // Compute difference between envelope and union to obtain gap polygons
137   geomEngine = QgsGeometryCheckerUtils::createGeomEngine( envelope.get(), mContext->tolerance );
138   geomEngine->prepareGeometry();
139   std::unique_ptr<QgsAbstractGeometry> diffGeom( geomEngine->difference( unionGeom.get(), &errMsg ) );
140   if ( !diffGeom )
141   {
142     messages.append( tr( "Gap check: %1" ).arg( errMsg ) );
143     return;
144   }
145 
146   // For each gap polygon which does not lie on the boundary, get neighboring polygons and add error
147   QgsGeometryPartIterator parts = diffGeom->parts();
148   while ( parts.hasNext() )
149   {
150     const QgsAbstractGeometry *gapGeom = parts.next();
151     // Skip the gap between features and boundingbox
152     const double spacing = context()->tolerance;
153     if ( gapGeom->boundingBox().snappedToGrid( spacing ) == envelope->boundingBox().snappedToGrid( spacing ) )
154     {
155       continue;
156     }
157 
158     // Skip gaps above threshold
159     if ( ( mGapThresholdMapUnits > 0 && gapGeom->area() > mGapThresholdMapUnits ) || gapGeom->area() < mContext->reducedTolerance )
160     {
161       continue;
162     }
163 
164     QgsRectangle gapAreaBBox = gapGeom->boundingBox();
165 
166     // Get neighboring polygons
167     QMap<QString, QgsFeatureIds> neighboringIds;
168     const QgsGeometryCheckerUtils::LayerFeatures layerFeatures( featurePools, featureIds.keys(), gapAreaBBox, compatibleGeometryTypes(), mContext );
169     std::unique_ptr< QgsGeometryEngine > gapGeomEngine = QgsGeometryCheckerUtils::createGeomEngine( gapGeom, mContext->tolerance );
170     gapGeomEngine->prepareGeometry();
171     for ( const QgsGeometryCheckerUtils::LayerFeature &layerFeature : layerFeatures )
172     {
173       const QgsGeometry geom = layerFeature.geometry();
174       if ( gapGeomEngine->distance( geom.constGet() ) < mContext->tolerance )
175       {
176         neighboringIds[layerFeature.layer()->id()].insert( layerFeature.feature().id() );
177         gapAreaBBox.combineExtentWith( geom.boundingBox() );
178       }
179     }
180 
181     if ( neighboringIds.isEmpty() )
182     {
183       continue;
184     }
185 
186     if ( allowedGapsGeomEngine && allowedGapsGeomEngine->contains( gapGeom ) )
187     {
188       continue;
189     }
190 
191     // Add error
192     double area = gapGeom->area();
193     QgsRectangle gapBbox = gapGeom->boundingBox();
194     errors.append( new QgsGeometryGapCheckError( this, QString(), QgsGeometry( gapGeom->clone() ), neighboringIds, area, gapBbox, gapAreaBBox ) );
195   }
196 }
197 
fixError(const QMap<QString,QgsFeaturePool * > & featurePools,QgsGeometryCheckError * error,int method,const QMap<QString,int> &,Changes & changes) const198 void QgsGeometryGapCheck::fixError( const QMap<QString, QgsFeaturePool *> &featurePools, QgsGeometryCheckError *error, int method, const QMap<QString, int> & /*mergeAttributeIndices*/, Changes &changes ) const
199 {
200   QMetaEnum metaEnum = QMetaEnum::fromType<QgsGeometryGapCheck::ResolutionMethod>();
201   if ( !metaEnum.isValid() || !metaEnum.valueToKey( method ) )
202   {
203     error->setFixFailed( tr( "Unknown method" ) );
204   }
205   else
206   {
207     ResolutionMethod methodValue = static_cast<ResolutionMethod>( method );
208     switch ( methodValue )
209     {
210       case NoChange:
211         error->setFixed( method );
212         break;
213 
214       case MergeLongestEdge:
215       {
216         QString errMsg;
217         if ( mergeWithNeighbor( featurePools, static_cast<QgsGeometryGapCheckError *>( error ), changes, errMsg, LongestSharedEdge ) )
218         {
219           error->setFixed( method );
220         }
221         else
222         {
223           error->setFixFailed( tr( "Failed to merge with neighbor: %1" ).arg( errMsg ) );
224         }
225         break;
226       }
227 
228       case AddToAllowedGaps:
229       {
230         QgsVectorLayer *layer = qobject_cast<QgsVectorLayer *>( mAllowedGapsLayer.data() );
231         if ( layer )
232         {
233           if ( !layer->isEditable() && !layer->startEditing() )
234           {
235             error->setFixFailed( tr( "Could not start editing layer %1" ).arg( layer->name() ) );
236           }
237           else
238           {
239             QgsFeature feature = QgsVectorLayerUtils::createFeature( layer, error->geometry() );
240             QgsFeatureList features = QgsVectorLayerUtils::makeFeatureCompatible( feature, layer );
241             if ( !layer->addFeatures( features ) )
242             {
243               error->setFixFailed( tr( "Could not add feature to layer %1" ).arg( layer->name() ) );
244             }
245             else
246             {
247               error->setFixed( method );
248             }
249           }
250         }
251         else
252         {
253           error->setFixFailed( tr( "Allowed gaps layer could not be resolved" ) );
254         }
255         break;
256       }
257 
258       case CreateNewFeature:
259       {
260         QgsGeometryGapCheckError *gapCheckError = static_cast<QgsGeometryGapCheckError *>( error );
261         QgsProject *project = QgsProject::instance();
262         QgsVectorLayer *layer = qobject_cast<QgsVectorLayer *>( project->mapLayer( gapCheckError->neighbors().keys().first() ) );
263         if ( layer )
264         {
265           const QgsGeometry geometry = error->geometry();
266           QgsExpressionContext context( QgsExpressionContextUtils::globalProjectLayerScopes( layer ) );
267           QgsFeature feature = QgsVectorLayerUtils::createFeature( layer, geometry, QgsAttributeMap(), &context );
268           if ( !layer->addFeature( feature ) )
269           {
270             error->setFixFailed( tr( "Could not add feature" ) );
271           }
272           else
273           {
274             error->setFixed( method );
275           }
276         }
277         else
278         {
279           error->setFixFailed( tr( "Could not resolve target layer %1 to add feature" ).arg( error->layerId() ) );
280         }
281         break;
282       }
283 
284       case MergeLargestArea:
285       {
286         QString errMsg;
287         if ( mergeWithNeighbor( featurePools, static_cast<QgsGeometryGapCheckError *>( error ), changes, errMsg, LargestArea ) )
288         {
289           error->setFixed( method );
290         }
291         else
292         {
293           error->setFixFailed( tr( "Failed to merge with neighbor: %1" ).arg( errMsg ) );
294         }
295         break;
296       }
297     }
298   }
299 }
300 
mergeWithNeighbor(const QMap<QString,QgsFeaturePool * > & featurePools,QgsGeometryGapCheckError * err,Changes & changes,QString & errMsg,Condition condition) const301 bool QgsGeometryGapCheck::mergeWithNeighbor( const QMap<QString, QgsFeaturePool *> &featurePools, QgsGeometryGapCheckError *err, Changes &changes, QString &errMsg, Condition condition ) const
302 {
303   double maxVal = 0.;
304   QString mergeLayerId;
305   QgsFeature mergeFeature;
306   int mergePartIdx = -1;
307 
308   const QgsGeometry geometry = err->geometry();
309   const QgsAbstractGeometry *errGeometry = QgsGeometryCheckerUtils::getGeomPart( geometry.constGet(), 0 );
310 
311   const auto layerIds = err->neighbors().keys();
312   QList<QgsFeature> neighbours;
313 
314   // Search for touching neighboring geometries
315   for ( const QString &layerId : layerIds )
316   {
317     QgsFeaturePool *featurePool = featurePools.value( layerId );
318     std::unique_ptr<QgsAbstractGeometry> errLayerGeom( errGeometry->clone() );
319     QgsCoordinateTransform ct( featurePool->crs(), mContext->mapCrs, mContext->transformContext );
320     errLayerGeom->transform( ct, QgsCoordinateTransform::ReverseTransform );
321 
322     const auto featureIds = err->neighbors().value( layerId );
323 
324     for ( QgsFeatureId testId : featureIds )
325     {
326       QgsFeature feature;
327       if ( !featurePool->getFeature( testId, feature ) )
328       {
329         continue;
330       }
331 
332       QgsGeometry transformedGeometry = feature.geometry();
333       transformedGeometry.transform( ct );
334       feature.setGeometry( transformedGeometry );
335       neighbours.append( feature );
336     }
337 
338     for ( const QgsFeature &testFeature : neighbours )
339     {
340       const QgsGeometry featureGeom = testFeature.geometry();
341       const QgsAbstractGeometry *testGeom = featureGeom.constGet();
342       for ( int iPart = 0, nParts = testGeom->partCount(); iPart < nParts; ++iPart )
343       {
344         double val = 0;
345         switch ( condition )
346         {
347           case LongestSharedEdge:
348             val = QgsGeometryCheckerUtils::sharedEdgeLength( errLayerGeom.get(), QgsGeometryCheckerUtils::getGeomPart( testGeom, iPart ), mContext->reducedTolerance );
349             break;
350 
351           case LargestArea:
352             // We might get a neighbour where we touch only a corner
353             if ( QgsGeometryCheckerUtils::sharedEdgeLength( errLayerGeom.get(), QgsGeometryCheckerUtils::getGeomPart( testGeom, iPart ), mContext->reducedTolerance ) > 0 )
354               val = QgsGeometryCheckerUtils::getGeomPart( testGeom, iPart )->area();
355             break;
356         }
357 
358         if ( val > maxVal )
359         {
360           maxVal = val;
361           mergeFeature = testFeature;
362           mergePartIdx = iPart;
363           mergeLayerId = layerId;
364         }
365       }
366     }
367   }
368 
369   if ( maxVal == 0. )
370   {
371     return false;
372   }
373 
374   // Create an index of all neighbouring vertices
375   QgsSpatialIndex neighbourVerticesIndex( QgsSpatialIndex::Flag::FlagStoreFeatureGeometries );
376   int id = 0;
377   for ( const QgsFeature &neighbour : neighbours )
378   {
379     QgsVertexIterator vit = neighbour.geometry().vertices();
380     while ( vit.hasNext() )
381     {
382       QgsPoint pt = vit.next();
383       QgsFeature f;
384       f.setId( id ); // required for SpatialIndex to return the correct result
385       f.setGeometry( QgsGeometry( pt.clone() ) );
386       neighbourVerticesIndex.addFeature( f );
387       id++;
388     }
389   }
390 
391   // Snap to the closest vertex
392   QgsPolyline snappedRing;
393   QgsVertexIterator iterator = errGeometry->vertices();
394   while ( iterator.hasNext() )
395   {
396     QgsPoint pt = iterator.next();
397     QgsGeometry closestGeom = neighbourVerticesIndex.geometry( neighbourVerticesIndex.nearestNeighbor( QgsPointXY( pt ) ).first() );
398     if ( !closestGeom.isEmpty() )
399     {
400       snappedRing.append( QgsPoint( closestGeom.vertexAt( 0 ) ) );
401     }
402   }
403 
404   std::unique_ptr<QgsPolygon> snappedErrGeom = qgis::make_unique<QgsPolygon>();
405   snappedErrGeom->setExteriorRing( new QgsLineString( snappedRing ) );
406 
407   // Merge geometries
408   QgsFeaturePool *featurePool = featurePools[ mergeLayerId ];
409   std::unique_ptr<QgsAbstractGeometry> errLayerGeom( snappedErrGeom->clone() );
410   QgsCoordinateTransform ct( featurePool->crs(), mContext->mapCrs, mContext->transformContext );
411   errLayerGeom->transform( ct, QgsCoordinateTransform::ReverseTransform );
412   const QgsGeometry mergeFeatureGeom = mergeFeature.geometry();
413   const QgsAbstractGeometry *mergeGeom = mergeFeatureGeom.constGet();
414   std::unique_ptr< QgsGeometryEngine > geomEngine = QgsGeometryCheckerUtils::createGeomEngine( errLayerGeom.get(), 0 );
415   std::unique_ptr<QgsAbstractGeometry> combinedGeom( geomEngine->combine( QgsGeometryCheckerUtils::getGeomPart( mergeGeom, mergePartIdx ), &errMsg ) );
416   if ( !combinedGeom || combinedGeom->isEmpty() || !QgsWkbTypes::isSingleType( combinedGeom->wkbType() ) )
417   {
418     return false;
419   }
420 
421   // Add merged polygon to destination geometry
422   replaceFeatureGeometryPart( featurePools, mergeLayerId, mergeFeature, mergePartIdx, combinedGeom.release(), changes );
423 
424   return true;
425 }
426 
427 
resolutionMethods() const428 QStringList QgsGeometryGapCheck::resolutionMethods() const
429 {
430   QStringList methods = QStringList()
431                         << tr( "Add gap area to neighboring polygon with longest shared edge" )
432                         << tr( "No action" );
433   if ( mAllowedGapsSource )
434     methods << tr( "Add gap to allowed exceptions" );
435 
436   return methods;
437 }
438 
availableResolutionMethods() const439 QList<QgsGeometryCheckResolutionMethod> QgsGeometryGapCheck::availableResolutionMethods() const
440 {
441   QList<QgsGeometryCheckResolutionMethod> fixes
442   {
443     QgsGeometryCheckResolutionMethod( MergeLongestEdge, tr( "Add to longest shared edge" ), tr( "Add the gap area to the neighbouring polygon with the longest shared edge." ), false ),
444     QgsGeometryCheckResolutionMethod( CreateNewFeature, tr( "Create new feature" ), tr( "Create a new feature from the gap area." ), false ),
445     QgsGeometryCheckResolutionMethod( MergeLargestArea, tr( "Add to largest neighbouring area" ), tr( "Add the gap area to the neighbouring polygon with the largest area." ), false )
446   };
447 
448   if ( mAllowedGapsSource )
449     fixes << QgsGeometryCheckResolutionMethod( AddToAllowedGaps, tr( "Add Gap to Allowed Exceptions" ), tr( "Create a new feature from the gap geometry on the allowed exceptions layer." ), true );
450 
451   fixes << QgsGeometryCheckResolutionMethod( NoChange, tr( "No action" ), tr( "Do not perform any action and mark this error as fixed." ), false );
452 
453   return fixes;
454 }
455 
description() const456 QString QgsGeometryGapCheck::description() const
457 {
458   return factoryDescription();
459 }
460 
id() const461 QString QgsGeometryGapCheck::id() const
462 {
463   return factoryId();
464 }
465 
flags() const466 QgsGeometryCheck::Flags QgsGeometryGapCheck::flags() const
467 {
468   return factoryFlags();
469 }
470 
471 ///@cond private
factoryDescription()472 QString QgsGeometryGapCheck::factoryDescription()
473 {
474   return tr( "Gap" );
475 }
476 
factoryId()477 QString QgsGeometryGapCheck::factoryId()
478 {
479   return QStringLiteral( "QgsGeometryGapCheck" );
480 }
481 
factoryFlags()482 QgsGeometryCheck::Flags QgsGeometryGapCheck::factoryFlags()
483 {
484   return QgsGeometryCheck::AvailableInValidation;
485 }
486 
factoryCompatibleGeometryTypes()487 QList<QgsWkbTypes::GeometryType> QgsGeometryGapCheck::factoryCompatibleGeometryTypes()
488 {
489   return {QgsWkbTypes::PolygonGeometry};
490 }
491 
factoryIsCompatible(QgsVectorLayer * layer)492 bool QgsGeometryGapCheck::factoryIsCompatible( QgsVectorLayer *layer ) SIP_SKIP
493 {
494   return factoryCompatibleGeometryTypes().contains( layer->geometryType() );
495 }
496 
factoryCheckType()497 QgsGeometryCheck::CheckType QgsGeometryGapCheck::factoryCheckType()
498 {
499   return QgsGeometryCheck::LayerCheck;
500 }
501 ///@endcond private
502 
contextBoundingBox() const503 QgsRectangle QgsGeometryGapCheckError::contextBoundingBox() const
504 {
505   return mContextBoundingBox;
506 }
507 
isEqual(QgsGeometryCheckError * other) const508 bool QgsGeometryGapCheckError::isEqual( QgsGeometryCheckError *other ) const
509 {
510   QgsGeometryGapCheckError *err = dynamic_cast<QgsGeometryGapCheckError *>( other );
511   return err && QgsGeometryCheckerUtils::pointsFuzzyEqual( err->location(), location(), mCheck->context()->reducedTolerance ) && err->neighbors() == neighbors();
512 }
513 
closeMatch(QgsGeometryCheckError * other) const514 bool QgsGeometryGapCheckError::closeMatch( QgsGeometryCheckError *other ) const
515 {
516   QgsGeometryGapCheckError *err = dynamic_cast<QgsGeometryGapCheckError *>( other );
517   return err && err->layerId() == layerId() && err->neighbors() == neighbors();
518 }
519 
update(const QgsGeometryCheckError * other)520 void QgsGeometryGapCheckError::update( const QgsGeometryCheckError *other )
521 {
522   QgsGeometryCheckError::update( other );
523   // Static cast since this should only get called if isEqual == true
524   const QgsGeometryGapCheckError *err = static_cast<const QgsGeometryGapCheckError *>( other );
525   mNeighbors = err->mNeighbors;
526   mGapAreaBBox = err->mGapAreaBBox;
527 }
528 
handleChanges(const QgsGeometryCheck::Changes &)529 bool QgsGeometryGapCheckError::handleChanges( const QgsGeometryCheck::Changes & )
530 {
531   return true;
532 }
533 
affectedAreaBBox() const534 QgsRectangle QgsGeometryGapCheckError::affectedAreaBBox() const
535 {
536   return mGapAreaBBox;
537 }
538 
involvedFeatures() const539 QMap<QString, QgsFeatureIds> QgsGeometryGapCheckError::involvedFeatures() const
540 {
541   return mNeighbors;
542 }
543 
icon() const544 QIcon QgsGeometryGapCheckError::icon() const
545 {
546 
547   if ( status() == QgsGeometryCheckError::StatusFixed )
548     return QgsApplication::getThemeIcon( QStringLiteral( "/algorithms/mAlgorithmCheckGeometry.svg" ) );
549   else
550     return QgsApplication::getThemeIcon( QStringLiteral( "/checks/SliverOrGap.svg" ) );
551 }
552