|
| 1 | +/*************************************************************************** |
| 2 | + testqgsgeometrychecks.cpp |
| 3 | + -------------------------------------- |
| 4 | + Date : September 2017 |
| 5 | + Copyright : (C) 2017 Sandro Mani |
| 6 | + Email : manisandro 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 "qgstest.h" |
| 17 | +#include "qgscrscache.h" |
| 18 | +#include "qgsfeature.h" |
| 19 | +#include "qgsfeaturepool.h" |
| 20 | +#include "qgsvectorlayer.h" |
| 21 | + |
| 22 | +#include "qgsgeometryanglecheck.h" |
| 23 | +#include "qgsgeometryareacheck.h" |
| 24 | +#include "qgsgeometrycontainedcheck.h" |
| 25 | +#include "qgsgeometrydanglecheck.h" |
| 26 | +#include "qgsgeometrydegeneratepolygoncheck.h" |
| 27 | +#include "qgsgeometryduplicatecheck.h" |
| 28 | +#include "qgsgeometryduplicatenodescheck.h" |
| 29 | + |
| 30 | +#include "qgsgeometrygapcheck.h" |
| 31 | +#include "qgsgeometryholecheck.h" |
| 32 | +#include "qgsgeometrylineintersectioncheck.h" |
| 33 | +#include "qgsgeometrylinelayerintersectioncheck.h" |
| 34 | +#include "qgsgeometrymultipartcheck.h" |
| 35 | +#include "qgsgeometryoverlapcheck.h" |
| 36 | +#include "qgsgeometrypointcoveredbylinecheck.h" |
| 37 | +#include "qgsgeometrypointinpolygoncheck.h" |
| 38 | +#include "qgsgeometrysegmentlengthcheck.h" |
| 39 | +#include "qgsgeometryselfcontactcheck.h" |
| 40 | +#include "qgsgeometryselfintersectioncheck.h" |
| 41 | +#include "qgsgeometrysliverpolygoncheck.h" |
| 42 | +#include "qgsgeometrytypecheck.h" |
| 43 | + |
| 44 | + |
| 45 | +class TestQgsGeometryChecks: public QObject |
| 46 | +{ |
| 47 | + Q_OBJECT |
| 48 | + private: |
| 49 | + double layerToMapUnits( const QgsMapLayer *layer, const QString &mapCrs ) const; |
| 50 | + QgsFeaturePool *createFeaturePool( QgsVectorLayer *layer, const QString &mapCrs, bool selectedOnly = false ) const; |
| 51 | + QgsGeometryCheckerContext *createTestContext( QMap<QString, QString> &layers, const QString &mapCrs = "EPSG:4326", double prec = 8 ) const; |
| 52 | + void cleanupTestContext( QgsGeometryCheckerContext *ctx ) const; |
| 53 | + void listErrors( const QList<QgsGeometryCheckError *> &checkErrors, const QStringList &messages ) const; |
| 54 | + int searchCheckError( const QList<QgsGeometryCheckError *> &checkErrors, const QString &layerId, const QgsFeatureId &featureId = -1, const QgsPointXY &pos = QgsPointXY(), const QgsVertexId &vid = QgsVertexId(), const QVariant &value = QVariant(), double tol = 1E-4 ) const; |
| 55 | + |
| 56 | + QMap<QString, QString> mLayers; |
| 57 | + QgsGeometryCheckerContext *mContext; |
| 58 | + |
| 59 | + private slots: |
| 60 | + // will be called before the first testfunction is executed. |
| 61 | + void initTestCase(); |
| 62 | + // will be called after the last testfunction is executed. |
| 63 | + void cleanupTestCase(); |
| 64 | + |
| 65 | + void testAngleCheck(); |
| 66 | + void testAreaCheck(); |
| 67 | + void testContainedCheck(); |
| 68 | + void testDangleCheck(); |
| 69 | + void testDegeneratePolygonCheck(); |
| 70 | + void testDuplicateCheck(); |
| 71 | + void testDuplicateNodesCheck(); |
| 72 | +}; |
| 73 | + |
| 74 | +void TestQgsGeometryChecks::initTestCase() |
| 75 | +{ |
| 76 | + QgsApplication::initQgis(); |
| 77 | + |
| 78 | + mLayers.insert( "point_layer.shp", "" ); |
| 79 | + mLayers.insert( "line_layer.shp", "" ); |
| 80 | + mLayers.insert( "polygon_layer.shp", "" ); |
| 81 | + mContext = createTestContext( mLayers ); |
| 82 | +} |
| 83 | + |
| 84 | +void TestQgsGeometryChecks::cleanupTestCase() |
| 85 | +{ |
| 86 | + cleanupTestContext( mContext ); |
| 87 | + QgsApplication::exitQgis(); |
| 88 | +} |
| 89 | + |
| 90 | +/////////////////////////////////////////////////////////////////////////////// |
| 91 | + |
| 92 | +void TestQgsGeometryChecks::testAngleCheck() |
| 93 | +{ |
| 94 | + QList<QgsGeometryCheckError *> checkErrors; |
| 95 | + QStringList messages; |
| 96 | + |
| 97 | + QgsGeometryAngleCheck( mContext, 15 ).collectErrors( checkErrors, messages ); |
| 98 | + listErrors( checkErrors, messages ); |
| 99 | + |
| 100 | + QCOMPARE( checkErrors.size(), 7 ); |
| 101 | + QVERIFY( searchCheckError( checkErrors, mLayers["point_layer.shp"] ) == 0 ); |
| 102 | + QVERIFY( searchCheckError( checkErrors, mLayers["line_layer.shp"], 0, QgsPointXY( -0.2225, 0.5526 ), QgsVertexId( 0, 0, 3 ), 10.5865 ) == 1 ); |
| 103 | + QVERIFY( searchCheckError( checkErrors, mLayers["line_layer.shp"], 0, QgsPointXY( -0.94996, 0.99967 ), QgsVertexId( 1, 0, 1 ), 8.3161 ) == 1 ); |
| 104 | + QVERIFY( searchCheckError( checkErrors, mLayers["line_layer.shp"], 2, QgsPointXY( -0.4547, -0.3059 ), QgsVertexId( 0, 0, 1 ), 5.4165 ) == 1 ); |
| 105 | + QVERIFY( searchCheckError( checkErrors, mLayers["line_layer.shp"], 2, QgsPointXY( -0.7594, -0.1971 ), QgsVertexId( 0, 0, 2 ), 12.5288 ) == 1 ); |
| 106 | + QVERIFY( searchCheckError( checkErrors, mLayers["polygon_layer.shp"], 0, QgsPointXY( 0.2402, 1.0786 ), QgsVertexId( 0, 0, 1 ), 13.5140 ) == 1 ); |
| 107 | + QVERIFY( searchCheckError( checkErrors, mLayers["polygon_layer.shp"], 1, QgsPointXY( 0.6960, 0.5908 ), QgsVertexId( 0, 0, 0 ), 7.0556 ) == 1 ); |
| 108 | + QVERIFY( searchCheckError( checkErrors, mLayers["polygon_layer.shp"], 1, QgsPointXY( 0.98690, 0.55699 ), QgsVertexId( 1, 0, 5 ), 7.7351 ) == 1 ); |
| 109 | +} |
| 110 | + |
| 111 | +void TestQgsGeometryChecks::testAreaCheck() |
| 112 | +{ |
| 113 | + QList<QgsGeometryCheckError *> checkErrors; |
| 114 | + QStringList messages; |
| 115 | + |
| 116 | + QgsGeometryAreaCheck( mContext, 0.04 ).collectErrors( checkErrors, messages ); |
| 117 | + listErrors( checkErrors, messages ); |
| 118 | + |
| 119 | + QCOMPARE( checkErrors.size(), 3 ); |
| 120 | + QVERIFY( searchCheckError( checkErrors, mLayers["point_layer.shp"] ) == 0 ); |
| 121 | + QVERIFY( searchCheckError( checkErrors, mLayers["line_layer.shp"] ) == 0 ); |
| 122 | + QVERIFY( searchCheckError( checkErrors, mLayers["polygon_layer.shp"], 1, QgsPointXY( 1.0068, 0.3635 ), QgsVertexId(), 0.0105 ) == 1 ); |
| 123 | + QVERIFY( searchCheckError( checkErrors, mLayers["polygon_layer.shp"], 2, QgsPointXY( 0.9739, 1.0983 ), QgsVertexId(), 0.0141 ) == 1 ); |
| 124 | + QVERIFY( searchCheckError( checkErrors, mLayers["polygon_layer.shp"], 6, QgsPointXY( 0.9968, 1.7584 ), QgsVertexId(), 0 ) == 1 ); |
| 125 | +} |
| 126 | + |
| 127 | +void TestQgsGeometryChecks::testContainedCheck() |
| 128 | +{ |
| 129 | + QList<QgsGeometryCheckError *> checkErrors; |
| 130 | + QStringList messages; |
| 131 | + |
| 132 | + QgsGeometryContainedCheck( mContext ).collectErrors( checkErrors, messages ); |
| 133 | + listErrors( checkErrors, messages ); |
| 134 | + |
| 135 | + QCOMPARE( checkErrors.size(), 3 ); |
| 136 | + QVERIFY( searchCheckError( checkErrors, mLayers["point_layer.shp"], 5, QgsPointXY(), QgsVertexId(), QVariant( "polygon_layer.shp:0" ) ) == 1 ); |
| 137 | + QVERIFY( searchCheckError( checkErrors, mLayers["line_layer.shp"], 3, QgsPointXY(), QgsVertexId(), QVariant( "polygon_layer.shp:0" ) ) == 1 ); |
| 138 | + QVERIFY( searchCheckError( checkErrors, mLayers["polygon_layer.shp"], 3, QgsPointXY(), QgsVertexId(), QVariant( "polygon_layer.shp:0" ) ) == 1 ); |
| 139 | + QVERIFY( messages.contains( "Contained check failed for (polygon_layer.shp:1): the geometry is invalid" ) ); |
| 140 | +} |
| 141 | + |
| 142 | +void TestQgsGeometryChecks::testDangleCheck() |
| 143 | +{ |
| 144 | + QList<QgsGeometryCheckError *> checkErrors; |
| 145 | + QStringList messages; |
| 146 | + |
| 147 | + QgsGeometryDangleCheck( mContext ).collectErrors( checkErrors, messages ); |
| 148 | + listErrors( checkErrors, messages ); |
| 149 | + |
| 150 | + QCOMPARE( checkErrors.size(), 4 ); |
| 151 | + QVERIFY( searchCheckError( checkErrors, mLayers["point_layer.shp"] ) == 0 ); |
| 152 | + QVERIFY( searchCheckError( checkErrors, mLayers["polygon_layer.shp"] ) == 0 ); |
| 153 | + QVERIFY( searchCheckError( checkErrors, mLayers["line_layer.shp"], 0, QgsPointXY( -0.7558, 0.7648 ), QgsVertexId( 1, 0, 0 ) ) == 1 ); |
| 154 | + QVERIFY( searchCheckError( checkErrors, mLayers["line_layer.shp"], 2, QgsPointXY( -0.7787, -0.2237 ), QgsVertexId( 0, 0, 0 ) ) == 1 ); |
| 155 | + QVERIFY( searchCheckError( checkErrors, mLayers["line_layer.shp"], 3, QgsPointXY( -0.2326, 0.9537 ), QgsVertexId( 0, 0, 0 ) ) == 1 ); |
| 156 | + QVERIFY( searchCheckError( checkErrors, mLayers["line_layer.shp"], 3, QgsPointXY( 0.07913, 0.8012 ), QgsVertexId( 0, 0, 2 ) ) == 1 ); |
| 157 | +} |
| 158 | + |
| 159 | +void TestQgsGeometryChecks::testDegeneratePolygonCheck() |
| 160 | +{ |
| 161 | + QList<QgsGeometryCheckError *> checkErrors; |
| 162 | + QStringList messages; |
| 163 | + |
| 164 | + QgsGeometryDegeneratePolygonCheck( mContext ).collectErrors( checkErrors, messages ); |
| 165 | + listErrors( checkErrors, messages ); |
| 166 | + |
| 167 | + QCOMPARE( checkErrors.size(), 1 ); |
| 168 | + QVERIFY( searchCheckError( checkErrors, mLayers["point_layer.shp"] ) == 0 ); |
| 169 | + QVERIFY( searchCheckError( checkErrors, mLayers["line_layer.shp"] ) == 0 ); |
| 170 | + QVERIFY( searchCheckError( checkErrors, mLayers["polygon_layer.shp"], 6 ) == 1 ); |
| 171 | +} |
| 172 | + |
| 173 | +void TestQgsGeometryChecks::testDuplicateCheck() |
| 174 | +{ |
| 175 | + QList<QgsGeometryCheckError *> checkErrors; |
| 176 | + QStringList messages; |
| 177 | + |
| 178 | + QgsGeometryDuplicateCheck( mContext ).collectErrors( checkErrors, messages ); |
| 179 | + listErrors( checkErrors, messages ); |
| 180 | + |
| 181 | + QCOMPARE( checkErrors.size(), 3 ); |
| 182 | + QVERIFY( |
| 183 | + searchCheckError( checkErrors, mLayers["point_layer.shp"], 6, QgsPoint(), QgsVertexId(), QVariant( "point_layer.shp:2" ) ) == 1 |
| 184 | + || searchCheckError( checkErrors, mLayers["point_layer.shp"], 2, QgsPoint(), QgsVertexId(), QVariant( "point_layer.shp:6" ) ) == 1 ); |
| 185 | + QVERIFY( |
| 186 | + searchCheckError( checkErrors, mLayers["line_layer.shp"], 4, QgsPoint(), QgsVertexId(), QVariant( "line_layer.shp:7" ) ) == 1 |
| 187 | + || searchCheckError( checkErrors, mLayers["line_layer.shp"], 7, QgsPoint(), QgsVertexId(), QVariant( "line_layer.shp:4" ) ) == 1 ); |
| 188 | + QVERIFY( |
| 189 | + searchCheckError( checkErrors, mLayers["polygon_layer.shp"], 8, QgsPoint(), QgsVertexId(), QVariant( "polygon_layer.shp:7" ) ) == 1 |
| 190 | + || searchCheckError( checkErrors, mLayers["polygon_layer.shp"], 7, QgsPoint(), QgsVertexId(), QVariant( "polygon_layer.shp:8" ) ) == 1 ); |
| 191 | +} |
| 192 | + |
| 193 | +void TestQgsGeometryChecks::testDuplicateNodesCheck() |
| 194 | +{ |
| 195 | + QList<QgsGeometryCheckError *> checkErrors; |
| 196 | + QStringList messages; |
| 197 | + |
| 198 | + QgsGeometryDuplicateNodesCheck( mContext ).collectErrors( checkErrors, messages ); |
| 199 | + listErrors( checkErrors, messages ); |
| 200 | + |
| 201 | + QCOMPARE( checkErrors.size(), 4 ); |
| 202 | + QVERIFY( searchCheckError( checkErrors, mLayers["point_layer.shp"] ) == 0 ); |
| 203 | + QVERIFY( searchCheckError( checkErrors, mLayers["line_layer.shp"], 0, QgsPointXY( -0.6360, 0.6203 ), QgsVertexId( 0, 0, 5 ) ) == 1 ); |
| 204 | + QVERIFY( searchCheckError( checkErrors, mLayers["line_layer.shp"], 6, QgsPointXY( 0.2473, 2.0821 ), QgsVertexId( 0, 0, 1 ) ) == 1 ); |
| 205 | + QVERIFY( searchCheckError( checkErrors, mLayers["line_layer.shp"], 6, QgsPointXY( 0.5158, 2.0930 ), QgsVertexId( 0, 0, 3 ) ) == 1 ); |
| 206 | + QVERIFY( searchCheckError( checkErrors, mLayers["polygon_layer.shp"], 4, QgsPointXY( 1.6319, 0.5642 ), QgsVertexId( 0, 0, 1 ) ) == 1 ); |
| 207 | +} |
| 208 | + |
| 209 | +/////////////////////////////////////////////////////////////////////////////// |
| 210 | + |
| 211 | +double TestQgsGeometryChecks::layerToMapUnits( const QgsMapLayer *layer, const QString &mapCrs ) const |
| 212 | +{ |
| 213 | + QgsCoordinateTransform crst = QgsCoordinateTransformCache::instance()->transform( layer->crs().authid(), mapCrs ); |
| 214 | + QgsRectangle extent = layer->extent(); |
| 215 | + QgsPointXY l1( extent.xMinimum(), extent.yMinimum() ); |
| 216 | + QgsPointXY l2( extent.xMaximum(), extent.yMaximum() ); |
| 217 | + double distLayerUnits = std::sqrt( l1.sqrDist( l2 ) ); |
| 218 | + QgsPointXY m1 = crst.transform( l1 ); |
| 219 | + QgsPointXY m2 = crst.transform( l2 ); |
| 220 | + double distMapUnits = std::sqrt( m1.sqrDist( m2 ) ); |
| 221 | + return distMapUnits / distLayerUnits; |
| 222 | +} |
| 223 | + |
| 224 | +QgsFeaturePool *TestQgsGeometryChecks::createFeaturePool( QgsVectorLayer *layer, const QString &mapCrs, bool selectedOnly ) const |
| 225 | +{ |
| 226 | + double layerToMapUntis = layerToMapUnits( layer, mapCrs ); |
| 227 | + QgsCoordinateTransform layerToMapTransform = QgsCoordinateTransformCache::instance()->transform( layer->crs().authid(), mapCrs ); |
| 228 | + return new QgsFeaturePool( layer, layerToMapUntis, layerToMapTransform, selectedOnly ); |
| 229 | +} |
| 230 | + |
| 231 | +QgsGeometryCheckerContext *TestQgsGeometryChecks::createTestContext( QMap<QString, QString> &layers, const QString &mapCrs, double prec ) const |
| 232 | +{ |
| 233 | + QDir testDataDir( QDir( TEST_DATA_DIR ).absoluteFilePath( "geometry_checker" ) ); |
| 234 | + |
| 235 | + QMap<QString, QgsFeaturePool *> featurePools; |
| 236 | + for ( const QString &layerFile : layers.keys() ) |
| 237 | + { |
| 238 | + QgsVectorLayer *layer = new QgsVectorLayer( testDataDir.absoluteFilePath( layerFile ), layerFile ); |
| 239 | + Q_ASSERT( layer && layer->isValid() ); |
| 240 | + layers[layerFile] = layer->id(); |
| 241 | + featurePools.insert( layer->id(), createFeaturePool( layer, mapCrs ) ); |
| 242 | + } |
| 243 | + return new QgsGeometryCheckerContext( prec, mapCrs, featurePools ); |
| 244 | +} |
| 245 | + |
| 246 | +void TestQgsGeometryChecks::cleanupTestContext( QgsGeometryCheckerContext *ctx ) const |
| 247 | +{ |
| 248 | + for ( const QgsFeaturePool *pool : ctx->featurePools ) |
| 249 | + { |
| 250 | + delete pool->getLayer(); |
| 251 | + } |
| 252 | + qDeleteAll( ctx->featurePools ); |
| 253 | + delete ctx; |
| 254 | +} |
| 255 | + |
| 256 | +void TestQgsGeometryChecks::listErrors( const QList<QgsGeometryCheckError *> &checkErrors, const QStringList &messages ) const |
| 257 | +{ |
| 258 | + QTextStream( stdout ) << " - Check result:" << endl; |
| 259 | + for ( const QgsGeometryCheckError *error : checkErrors ) |
| 260 | + { |
| 261 | + QTextStream( stdout ) << " * " << error->layerId() << ":" << error->featureId() << " @[" << error->vidx().part << ", " << error->vidx().ring << ", " << error->vidx().vertex << "](" << error->location().x() << ", " << error->location().y() << ") = " << error->value().toString() << endl; |
| 262 | + } |
| 263 | + if ( !messages.isEmpty() ) |
| 264 | + { |
| 265 | + QTextStream( stdout ) << " - Check messages:" << endl << " * " << messages.join( "\n * " ) << endl; |
| 266 | + } |
| 267 | +} |
| 268 | + |
| 269 | +int TestQgsGeometryChecks::searchCheckError( const QList<QgsGeometryCheckError *> &checkErrors, const QString &layerId, const QgsFeatureId &featureId, const QgsPointXY &pos, const QgsVertexId &vid, const QVariant &value, double tol ) const |
| 270 | +{ |
| 271 | + int matching = 0; |
| 272 | + for ( const QgsGeometryCheckError *error : checkErrors ) |
| 273 | + { |
| 274 | + if ( error->layerId() != layerId ) |
| 275 | + { |
| 276 | + continue; |
| 277 | + } |
| 278 | + if ( featureId != -1 && error->featureId() != featureId ) |
| 279 | + { |
| 280 | + continue; |
| 281 | + } |
| 282 | + if ( pos != QgsPointXY() && ( !qgsDoubleNear( error->location().x(), pos.x(), tol ) || !qgsDoubleNear( error->location().y(), pos.y(), tol ) ) ) |
| 283 | + { |
| 284 | + continue; |
| 285 | + } |
| 286 | + if ( vid.isValid() && vid != error->vidx() ) |
| 287 | + { |
| 288 | + continue; |
| 289 | + } |
| 290 | + if ( !value.isNull() ) |
| 291 | + { |
| 292 | + if ( value.type() == QVariant::Double ) |
| 293 | + { |
| 294 | + if ( !qgsDoubleNear( value.toDouble(), error->value().toDouble(), tol ) ) |
| 295 | + { |
| 296 | + continue; |
| 297 | + } |
| 298 | + } |
| 299 | + else if ( value != error->value() ) |
| 300 | + { |
| 301 | + continue; |
| 302 | + } |
| 303 | + } |
| 304 | + ++matching; |
| 305 | + } |
| 306 | + return matching; |
| 307 | +} |
| 308 | + |
| 309 | +QGSTEST_MAIN( TestQgsGeometryChecks ) |
| 310 | +#include "testqgsgeometrychecks.moc" |
0 commit comments