Skip to content
Permalink
Browse files
[ogr] Read field domains from datasets and auto translate to value map
editor config or range config

Requires GDAL 3.3+
  • Loading branch information
nyalldawson committed Apr 30, 2021
1 parent cad5707 commit 58c3665f2393a1b9eb9989ef4925a5c0f6838c9a
@@ -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++;
}
@@ -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;
@@ -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
@@ -1231,6 +1231,52 @@ def testDecodeEncodeUriVsizip(self):
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()
Binary file not shown.

0 comments on commit 58c3665

Please sign in to comment.