Skip to content

Commit

Permalink
[FEATURE][ogr] Add string list field type support
Browse files Browse the repository at this point in the history
  • Loading branch information
nirvn committed Apr 10, 2019
1 parent 28b3323 commit 9ce1093
Show file tree
Hide file tree
Showing 11 changed files with 370 additions and 1 deletion.
2 changes: 1 addition & 1 deletion python/plugins/processing/algs/qgis/ui/AggregatesPanel.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ def newField(self, field=None):
default_aggregate = 'sum'
if field.type() == QVariant.DateTime:
default_aggregate = ''
if field.type() == QVariant.String:
if field.type() == QVariant.String or (field.type() == QVariant.List and field.subType() == QVariant.String):
default_aggregate = 'concatenate'

return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ class FieldsMappingModel(QAbstractTableModel):
(QVariant.Int, "Integer"),
(QVariant.LongLong, "Integer64"),
(QVariant.String, "String"),
(QVariant.List, "List"),
(QVariant.Bool, "Boolean")])

def __init__(self, parent=None):
Expand Down
Binary file not shown.
28 changes: 28 additions & 0 deletions src/core/qgsogrutils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,34 @@ QVariant QgsOgrUtils::getOgrFeatureAttribute( OGRFeatureH ogrFet, const QgsField
break;
}

case QVariant::List:
{
if ( fields.at( attIndex ).subType() == QVariant::String )
{
QStringList list;
char **lst = OGR_F_GetFieldAsStringList( ogrFet, attIndex );
const int count = CSLCount( lst );
if ( count > 0 )
{
for ( int i = 0; i < count; i++ )
{
if ( encoding )
list << encoding->toUnicode( lst[i] );
else
list << QString::fromUtf8( lst[i] );
}
}
value = list;
}
else
{
Q_ASSERT_X( false, "QgsOgrUtils::getOgrFeatureAttribute", "unsupported field type" );
if ( ok )
*ok = false;
}
break;
}

case QVariant::Map:
{
//it has to be JSON
Expand Down
55 changes: 55 additions & 0 deletions src/core/qgsvectorfilewriter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,28 @@ void QgsVectorFileWriter::init( QString vectorFileName,
ogrType = OFTBinary;
break;

#if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(2,4,0)
case QVariant::List:
// only string list supported at the moment, fall through to default for other types
if ( attrField.subType() == QVariant::String )
{
const char *pszDataTypes = GDALGetMetadataItem( poDriver, GDAL_DMD_CREATIONFIELDDATATYPES, nullptr );
if ( pszDataTypes && strstr( pszDataTypes, "StringList" ) )
{
ogrType = OFTStringList;
supportsStringList = true;
}
else
{
ogrType = OFTString;
ogrWidth = 255;
}
break;
}
//intentional fall-through
FALLTHROUGH
#endif

default:
//assert(0 && "invalid variant type!");
mErrorMessage = QObject::tr( "Unsupported type for field %1" )
Expand Down Expand Up @@ -2253,6 +2275,39 @@ gdal::ogr_feature_unique_ptr QgsVectorFileWriter::createFeature( const QgsFeatur

case QVariant::Invalid:
break;

#if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(2,4,0)
case QVariant::List:
// only string list supported at the moment, fall through to default for other types
if ( field.subType() == QVariant::String )
{
QStringList list = attrValue.toStringList();
if ( supportsStringList )
{
int count = list.count();
char **lst = new char *[count + 1];
if ( count > 0 )
{
int pos = 0;
for ( QString string : list )
{
lst[pos] = mCodec->fromUnicode( string ).data();
pos++;
}
}
lst[count] = nullptr;
OGR_F_SetFieldStringList( poFeature.get(), ogrField, lst );
}
else
{
OGR_F_SetFieldString( poFeature.get(), ogrField, mCodec->fromUnicode( list.join( ',' ) ).constData() );
}
break;
}
//intentional fall-through
FALLTHROUGH
#endif

default:
mErrorMessage = QObject::tr( "Invalid variant type for field %1[%2]: received %3 with type %4" )
.arg( mFields.at( fldIdx ).name() )
Expand Down
1 change: 1 addition & 0 deletions src/core/qgsvectorfilewriter.h
Original file line number Diff line number Diff line change
Expand Up @@ -888,6 +888,7 @@ class CORE_EXPORT QgsVectorFileWriter : public QgsFeatureSink
QgsRenderContext mRenderContext;

bool mUsingTransaction = false;
bool supportsStringList = false;

void createSymbolLayerTable( QgsVectorLayer *vl, const QgsCoordinateTransform &ct, OGRDataSourceH ds );
gdal::ogr_feature_unique_ptr createFeature( const QgsFeature &feature );
Expand Down
84 changes: 84 additions & 0 deletions src/providers/ogr/qgsogrprovider.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,20 @@ bool QgsOgrProvider::convertField( QgsField &field, const QTextCodec &encoding )
ogrType = OFTDateTime;
break;
#if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(2,4,0)
case QVariant::List:
if ( field.subType() == QVariant::String )
{
ogrType = OFTStringList;
}
else
{
// only string lists are supported at this moment
return false;
}
break;
#endif
#if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(2,4,0)
case QVariant::Map:
ogrType = OFTString;
Expand Down Expand Up @@ -487,6 +501,7 @@ QgsOgrProvider::QgsOgrProvider( QString const &uri, const ProviderOptions &optio
bool supportsTime = mGDALDriverName != QLatin1String( "ESRI Shapefile" ) && mGDALDriverName != QLatin1String( "GPKG" );
bool supportsDateTime = mGDALDriverName != QLatin1String( "ESRI Shapefile" );
bool supportsBinary = false;
bool supportsStringList = false;
const char *pszDataTypes = nullptr;
if ( mOgrOrigLayer )
{
Expand All @@ -501,6 +516,9 @@ QgsOgrProvider::QgsOgrProvider( QString const &uri, const ProviderOptions &optio
supportsTime = CSLFindString( papszTokens, "Time" ) >= 0;
supportsDateTime = CSLFindString( papszTokens, "DateTime" ) >= 0;
supportsBinary = CSLFindString( papszTokens, "Binary" ) >= 0;
#if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(2,4,0)
supportsStringList = CSLFindString( papszTokens, "StringList" ) >= 0;
#endif
CSLDestroy( papszTokens );
}

Expand All @@ -524,6 +542,11 @@ QgsOgrProvider::QgsOgrProvider( QString const &uri, const ProviderOptions &optio
nativeTypes
<< QgsVectorDataProvider::NativeType( tr( "Binary object (BLOB)" ), QStringLiteral( "binary" ), QVariant::ByteArray );
}
if ( supportsStringList )
{
nativeTypes
<< QgsVectorDataProvider::NativeType( tr( "String List" ), QStringLiteral( "stringlist" ), QVariant::List, 0, 0, 0, 0, QVariant::String );
}

bool supportsBoolean = false;

Expand Down Expand Up @@ -1086,6 +1109,14 @@ void QgsOgrProvider::loadFields()
}
break;
#endif

#if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(2,4,0)
case OFTStringList:
varType = QVariant::List;
varSubType = QVariant::String;
break;
#endif

default:
varType = QVariant::String; // other unsupported, leave it as a string
}
Expand Down Expand Up @@ -1592,6 +1623,27 @@ bool QgsOgrProvider::addFeaturePrivate( QgsFeature &f, Flags flags )
break;
}

#if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(2,4,0)
case OFTStringList:
{
QStringList list = attrVal.toStringList();
int count = list.count();
char **lst = new char *[count + 1];
if ( count > 0 )
{
int pos = 0;
for ( QString string : list )
{
lst[pos] = textEncoding()->fromUnicode( string ).data();
pos++;
}
}
lst[count] = nullptr;
OGR_F_SetFieldStringList( feature.get(), ogrAttId, lst );
break;
}
#endif

default:
QgsMessageLog::logMessage( tr( "type %1 for attribute %2 not found" ).arg( type ).arg( qgisAttId ), tr( "OGR" ) );
break;
Expand Down Expand Up @@ -1705,6 +1757,17 @@ bool QgsOgrProvider::addAttributeOGRLevel( const QgsField &field, bool &ignoreEr
case QVariant::Map:
type = OFTString;
break;
#if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(2,4,0)
case QVariant::List:
// only string list supported at the moment, fall through to default for other types
if ( field.subType() == QVariant::String )
{
type = OFTStringList;
break;
}
//intentional fall-through
FALLTHROUGH
#endif
default:
pushError( tr( "type %1 for field %2 not found" ).arg( field.typeName(), field.name() ) );
ignoreErrorOut = true;
Expand Down Expand Up @@ -2177,6 +2240,27 @@ bool QgsOgrProvider::changeAttributeValues( const QgsChangedAttributesMap &attr_
break;
}

#if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(2,4,0)
case OFTStringList:
{
QStringList list = it2->toStringList();
int count = list.count();
char **lst = new char *[count + 1];
if ( count > 0 )
{
int pos = 0;
for ( QString string : list )
{
lst[pos] = textEncoding()->fromUnicode( string ).data();
pos++;
}
}
lst[count] = nullptr;
OGR_F_SetFieldStringList( of.get(), f, lst );
break;
}
#endif

default:
pushError( tr( "Type %1 of attribute %2 of feature %3 unknown." ).arg( type ).arg( fid ).arg( f ) );
break;
Expand Down
34 changes: 34 additions & 0 deletions tests/src/python/test_provider_ogr.py
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,40 @@ def testBinaryField(self):
self.assertIsInstance(features[2]['DATA'], QByteArray)
self.assertEqual(hashlib.md5(features[2]['DATA'].data()).hexdigest(), '4b952b80e4288ca5111be2f6dd5d6809')

@unittest.skip(int(gdal.VersionInfo('VERSION_NUM')) < GDAL_COMPUTE_VERSION(2, 4, 0))
def testStringListField(self):
source = os.path.join(TEST_DATA_DIR, 'stringlist.gml')
vl = QgsVectorLayer(source)
self.assertTrue(vl.isValid())

fields = vl.fields()
descriptive_group_field = fields[fields.lookupField('descriptiveGroup')]
self.assertEqual(descriptive_group_field.type(), QVariant.List)
self.assertEqual(descriptive_group_field.typeName(), 'StringList')
self.assertEqual(descriptive_group_field.subType(), QVariant.String)

feature = vl.getFeature(1000002717654)
self.assertEqual(feature['descriptiveGroup'], ['Building'])
self.assertEqual(feature['reasonForChange'], ['Reclassified', 'Attributes'])

tmpfile = os.path.join(self.basetestpath, 'newstringlistfield.gml')
ds = ogr.GetDriverByName('GML').CreateDataSource(tmpfile)
lyr = ds.CreateLayer('test', geom_type=ogr.wkbPoint)
lyr.CreateField(ogr.FieldDefn('strfield', ogr.OFTString))
lyr.CreateField(ogr.FieldDefn('intfield', ogr.OFTInteger))
lyr.CreateField(ogr.FieldDefn('strlistfield', ogr.OFTStringList))
ds = None

vl = QgsVectorLayer(tmpfile)
self.assertTrue(vl.isValid())

dp = vl.dataProvider()
fields = dp.fields()
list_field = fields[fields.lookupField('strlistfield')]
self.assertEqual(list_field.type(), QVariant.List)
self.assertEqual(list_field.typeName(), 'StringList')
self.assertEqual(list_field.subType(), QVariant.String)

def testBlobCreation(self):
"""
Test creating binary blob field in existing table
Expand Down
40 changes: 40 additions & 0 deletions tests/src/python/test_qgsvectorfilewriter.py
Original file line number Diff line number Diff line change
Expand Up @@ -1044,6 +1044,46 @@ def testDropZ(self):
f = next(created_layer.getFeatures(QgsFeatureRequest()))
self.assertEqual(f.geometry().asWkt(), 'Point (10 10)')

@unittest.skip(int(gdal.VersionInfo('VERSION_NUM')) < GDAL_COMPUTE_VERSION(2, 4, 0))
def testWriteWithStringListField(self):
"""
Test writing with a string list field
:return:
"""
tmpfile = os.path.join(self.basetestpath, 'newstringlistfield.gml')
ds = ogr.GetDriverByName('GML').CreateDataSource(tmpfile)
lyr = ds.CreateLayer('test', geom_type=ogr.wkbPoint)
lyr.CreateField(ogr.FieldDefn('strfield', ogr.OFTString))
lyr.CreateField(ogr.FieldDefn('intfield', ogr.OFTInteger))
lyr.CreateField(ogr.FieldDefn('strlistfield', ogr.OFTStringList))
ds = None

vl = QgsVectorLayer(tmpfile)
self.assertTrue(vl.isValid())

# write a gml dataset with a string list field
filename = os.path.join(str(QDir.tempPath()), 'with_stringlist_field.gml')
rc, errmsg = QgsVectorFileWriter.writeAsVectorFormat(vl,
filename,
'utf-8',
vl.crs(),
'GML')

self.assertEqual(rc, QgsVectorFileWriter.NoError)

# open the resulting gml
vl = QgsVectorLayer(filename, '', 'ogr')
self.assertTrue(vl.isValid())
fields = vl.fields()

# test type of converted field
idx = fields.indexFromName('strlistfield')
self.assertEqual(fields.at(idx).type(), QVariant.List)
self.assertEqual(fields.at(idx).subType(), QVariant.String)

del vl
os.unlink(filename)

def testWriteWithBinaryField(self):
"""
Test writing with a binary field
Expand Down
32 changes: 32 additions & 0 deletions tests/testdata/stringlist.gml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8" ?>
<ogr:FeatureCollection
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://ogr.maptools.org/ stringlist.xsd"
xmlns:ogr="http://ogr.maptools.org/"
xmlns:gml="http://www.opengis.net/gml">
<gml:boundedBy>
<gml:Box>
<gml:coord><gml:X>511558.15</gml:X><gml:Y>197206.2</gml:Y></gml:coord>
<gml:coord><gml:X>511566.3</gml:X><gml:Y>197217.9</gml:Y></gml:coord>
</gml:Box>
</gml:boundedBy>

<gml:featureMember>
<ogr:stringlist fid="osgb1000002717654">
<ogr:geometryProperty><gml:Polygon srsName="EPSG:27700"><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates>511564.95,197209.45 511566.15,197215.8 511566.3,197216.7 511559.85,197217.9 511558.15,197208.45 511563.7,197207.4 511563.5,197206.35 511564.35,197206.2 511564.95,197209.45</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs></gml:Polygon></ogr:geometryProperty>
<ogr:featureCode>10021</ogr:featureCode>
<ogr:version>3</ogr:version>
<ogr:versionDate>2003-12-29</ogr:versionDate>
<ogr:theme>Buildings</ogr:theme>
<ogr:calculatedAreaValue>63.757504</ogr:calculatedAreaValue>
<ogr:changeDate>2003-12-08</ogr:changeDate>
<ogr:changeDate>2003-12-08</ogr:changeDate>
<ogr:reasonForChange>Reclassified</ogr:reasonForChange>
<ogr:reasonForChange>Attributes</ogr:reasonForChange>
<ogr:descriptiveGroup>Building</ogr:descriptiveGroup>
<ogr:descriptiveTerm xsi:nil="true"/>
<ogr:make>Manmade</ogr:make>
<ogr:physicalLevel>50</ogr:physicalLevel>
</ogr:stringlist>
</gml:featureMember>
</ogr:FeatureCollection>
Loading

0 comments on commit 9ce1093

Please sign in to comment.