[ogr] Read field domains from datasets and auto translate to value map

editor config or range config

Requires GDAL 3.3+
This commit is contained in:
Nyall Dawson 2021-04-29 09:53:31 +10:00
parent cad5707a62
commit 58c3665f23
5 changed files with 224 additions and 0 deletions

View File

@ -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++;
}

View File

@ -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;

View File

@ -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

View File

@ -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()

BIN
tests/testdata/domains.gpkg vendored Normal file

Binary file not shown.