From 58c3665f2393a1b9eb9989ef4925a5c0f6838c9a Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Thu, 29 Apr 2021 09:53:31 +1000 Subject: [PATCH] [ogr] Read field domains from datasets and auto translate to value map editor config or range config Requires GDAL 3.3+ --- src/core/providers/ogr/qgsogrprovider.cpp | 89 ++++++++++++++++++++++ src/core/qgsogrutils.cpp | 83 ++++++++++++++++++++ src/core/qgsogrutils.h | 6 ++ tests/src/python/test_provider_ogr.py | 46 +++++++++++ tests/testdata/domains.gpkg | Bin 0 -> 122880 bytes 5 files changed, 224 insertions(+) create mode 100644 tests/testdata/domains.gpkg diff --git a/src/core/providers/ogr/qgsogrprovider.cpp b/src/core/providers/ogr/qgsogrprovider.cpp index ef7fae12796..a756654d818 100644 --- a/src/core/providers/ogr/qgsogrprovider.cpp +++ b/src/core/providers/ogr/qgsogrprovider.cpp @@ -1215,6 +1215,17 @@ void QgsOgrProvider::loadFields() mPrimaryKeyAttrs << 0; } +#if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,3,0) + // needed for field domain retrieval on GDAL 3.3+ +#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) + QMutex *datasetMutex = nullptr; +#else + QRecursiveMutex *datasetMutex = nullptr; +#endif + GDALDatasetH ds = mOgrLayer->getDatasetHandleAndMutex( datasetMutex ); + QMutexLocker locker( datasetMutex ); +#endif + for ( int i = 0; i < fdef.GetFieldCount(); ++i ) { OGRFieldDefnH fldDef = fdef.GetFieldDefn( i ); @@ -1371,6 +1382,84 @@ void QgsOgrProvider::loadFields() mDefaultValues.insert( createdFields, defaultValue ); } +#if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,3,0) + if ( const char *domainName = OGR_Fld_GetDomainName( fldDef ) ) + { + // dataset retains ownership of domain! + if ( OGRFieldDomainH domain = GDALDatasetGetFieldDomain( ds, domainName ) ) + { + switch ( OGR_FldDomain_GetDomainType( domain ) ) + { + case OFDT_CODED: + { + QVariantList valueConfig; + const OGRCodedValue *codedValue = OGR_CodedFldDomain_GetEnumeration( domain ); + while ( codedValue && codedValue->pszCode ) + { + const QString code( codedValue->pszCode ); + const QString value( codedValue->pszValue ); + + QVariantMap config; + config[ value ] = code; + valueConfig.append( config ); + + codedValue++; + } + + QVariantMap editorConfig; + editorConfig.insert( QStringLiteral( "map" ), valueConfig ); + newField.setEditorWidgetSetup( QgsEditorWidgetSetup( QStringLiteral( "ValueMap" ), editorConfig ) ); + break; + } + + case OFDT_RANGE: + if ( newField.isNumeric() ) + { + // QGIS doesn't support the inclusive option yet! + bool isInclusive = false; + + QVariantMap editorConfig; + editorConfig.insert( QStringLiteral( "Step" ), 1 ); + editorConfig.insert( QStringLiteral( "Style" ), QStringLiteral( "SpinBox" ) ); + editorConfig.insert( QStringLiteral( "AllowNull" ), nullable ); + editorConfig.insert( QStringLiteral( "Precision" ), newField.precision() ); + + OGRFieldType domainFieldType = OGR_FldDomain_GetFieldType( domain ); + bool hasMinOrMax = false; + if ( const OGRField *min = OGR_RangeFldDomain_GetMin( domain, &isInclusive ) ) + { + const QVariant minValue = QgsOgrUtils::OGRFieldtoVariant( min, domainFieldType ); + if ( minValue.isValid() ) + { + editorConfig.insert( QStringLiteral( "Min" ), minValue ); + hasMinOrMax = true; + } + } + if ( const OGRField *max = OGR_RangeFldDomain_GetMax( domain, &isInclusive ) ) + { + const QVariant maxValue = QgsOgrUtils::OGRFieldtoVariant( max, domainFieldType ); + if ( maxValue.isValid() ) + { + editorConfig.insert( QStringLiteral( "Max" ), maxValue ); + hasMinOrMax = true; + } + } + + if ( hasMinOrMax ) + newField.setEditorWidgetSetup( QgsEditorWidgetSetup( QStringLiteral( "Range" ), editorConfig ) ); + } + // GDAL also supports range domains for fields types like date/datetimes, but the QGIS corresponding field + // config doesn't support this yet! + break; + + case OFDT_GLOB: + // not supported by QGIS yet + break; + } + } + } +#endif + mAttributeFields.append( newField ); createdFields++; } diff --git a/src/core/qgsogrutils.cpp b/src/core/qgsogrutils.cpp index 8b532147a69..d620574c7ad 100644 --- a/src/core/qgsogrutils.cpp +++ b/src/core/qgsogrutils.cpp @@ -98,6 +98,89 @@ void gdal::GDALWarpOptionsDeleter::operator()( GDALWarpOptions *options ) GDALDestroyWarpOptions( options ); } +QVariant QgsOgrUtils::OGRFieldtoVariant( const OGRField *value, OGRFieldType type ) +{ + if ( !value ) + return QVariant(); + + switch ( type ) + { + case OFTInteger: + return value->Integer; + + case OFTInteger64: + return value->Integer64; + + case OFTReal: + return value->Real; + + case OFTString: + case OFTWideString: + return QString::fromUtf8( value->String ); + + case OFTDate: + return QDate( value->Date.Year, value->Date.Month, value->Date.Day ); + + case OFTTime: + { + float secondsPart = 0; + float millisecondPart = std::modf( value->Date.Second, &secondsPart ); + return QTime( value->Date.Hour, value->Date.Minute, static_cast< int >( secondsPart ), static_cast< int >( 1000 * millisecondPart ) ); + } + + case OFTDateTime: + { + float secondsPart = 0; + float millisecondPart = std::modf( value->Date.Second, &secondsPart ); + return QDateTime( QDate( value->Date.Year, value->Date.Month, value->Date.Day ), + QTime( value->Date.Hour, value->Date.Minute, static_cast< int >( secondsPart ), static_cast< int >( 1000 * millisecondPart ) ) ); + } + + case OFTBinary: + // not supported! + Q_ASSERT_X( false, "QgsOgrUtils::OGRFieldtoVariant", "OFTBinary type not supported" ); + return QVariant(); + + case OFTIntegerList: + { + QVariantList res; + res.reserve( value->IntegerList.nCount ); + for ( int i = 0; i < value->IntegerList.nCount; ++i ) + res << value->IntegerList.paList[ i ]; + return res; + } + + case OFTInteger64List: + { + QVariantList res; + res.reserve( value->Integer64List.nCount ); + for ( int i = 0; i < value->Integer64List.nCount; ++i ) + res << value->Integer64List.paList[ i ]; + return res; + } + + case OFTRealList: + { + QVariantList res; + res.reserve( value->RealList.nCount ); + for ( int i = 0; i < value->RealList.nCount; ++i ) + res << value->RealList.paList[ i ]; + return res; + } + + case OFTStringList: + case OFTWideStringList: + { + QVariantList res; + res.reserve( value->StringList.nCount ); + for ( int i = 0; i < value->StringList.nCount; ++i ) + res << QString::fromUtf8( value->StringList.paList[ i ] ); + return res; + } + } + return QVariant(); +} + QgsFeature QgsOgrUtils::readOgrFeature( OGRFeatureH ogrFet, const QgsFields &fields, QTextCodec *encoding ) { QgsFeature feature; diff --git a/src/core/qgsogrutils.h b/src/core/qgsogrutils.h index 3810364151f..c8f93e4ac84 100644 --- a/src/core/qgsogrutils.h +++ b/src/core/qgsogrutils.h @@ -165,6 +165,12 @@ class CORE_EXPORT QgsOgrUtils { public: + /** + * Converts an OGRField \a value of the specified \a type into a QVariant. + * \since QGIS 3.20 + */ + static QVariant OGRFieldtoVariant( const OGRField *value, OGRFieldType type ); + /** * Reads an OGR feature and converts it to a QgsFeature. * \param ogrFet OGR feature handle diff --git a/tests/src/python/test_provider_ogr.py b/tests/src/python/test_provider_ogr.py index d684b48cee3..6561d26f568 100644 --- a/tests/src/python/test_provider_ogr.py +++ b/tests/src/python/test_provider_ogr.py @@ -1231,6 +1231,52 @@ class PyQgsOGRProvider(unittest.TestCase): encodedUri = QgsProviderRegistry.instance().encodeUri('ogr', parts) self.assertEqual(encodedUri, uri) + @unittest.skipIf(int(gdal.VersionInfo('VERSION_NUM')) < GDAL_COMPUTE_VERSION(3, 3, 0), "GDAL 3.3 required") + def testFieldDomains(self): + """ + Test that field domains are translated from OGR where available (requires GDAL 3.3 or later) + """ + datasource = os.path.join(unitTestDataPath(), 'domains.gpkg') + vl = QgsVectorLayer(datasource, 'test', 'ogr') + self.assertTrue(vl.isValid()) + + fields = vl.fields() + + range_int_field = fields[fields.lookupField('with_range_domain_int')] + range_int_setup = range_int_field.editorWidgetSetup() + self.assertEqual(range_int_setup.type(), 'Range') + self.assertTrue(range_int_setup.config()['AllowNull']) + self.assertEqual(range_int_setup.config()['Max'], 2) + self.assertEqual(range_int_setup.config()['Min'], 1) + self.assertEqual(range_int_setup.config()['Precision'], 0) + self.assertEqual(range_int_setup.config()['Step'], 1) + self.assertEqual(range_int_setup.config()['Style'], 'SpinBox') + + range_int64_field = fields[fields.lookupField('with_range_domain_int64')] + range_int64_setup = range_int64_field.editorWidgetSetup() + self.assertEqual(range_int64_setup.type(), 'Range') + self.assertTrue(range_int64_setup.config()['AllowNull']) + self.assertEqual(range_int64_setup.config()['Max'], 1234567890123) + self.assertEqual(range_int64_setup.config()['Min'], -1234567890123) + self.assertEqual(range_int64_setup.config()['Precision'], 0) + self.assertEqual(range_int64_setup.config()['Step'], 1) + self.assertEqual(range_int64_setup.config()['Style'], 'SpinBox') + + range_real_field = fields[fields.lookupField('with_range_domain_real')] + range_real_setup = range_real_field.editorWidgetSetup() + self.assertEqual(range_real_setup.type(), 'Range') + self.assertTrue(range_real_setup.config()['AllowNull']) + self.assertEqual(range_real_setup.config()['Max'], 2.5) + self.assertEqual(range_real_setup.config()['Min'], 1.5) + self.assertEqual(range_real_setup.config()['Precision'], 0) + self.assertEqual(range_real_setup.config()['Step'], 1) + self.assertEqual(range_real_setup.config()['Style'], 'SpinBox') + + enum_field = fields[fields.lookupField('with_enum_domain')] + enum_setup = enum_field.editorWidgetSetup() + self.assertEqual(enum_setup.type(), 'ValueMap') + self.assertTrue(enum_setup.config()['map'], [{'one': '1'}, {'': '2'}]) + if __name__ == '__main__': unittest.main() diff --git a/tests/testdata/domains.gpkg b/tests/testdata/domains.gpkg new file mode 100644 index 0000000000000000000000000000000000000000..20e8221ecb5596c78145495d5157d7aa6bfdf6b8 GIT binary patch literal 122880 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 v_rGcn!Z8Sd00