Skip to content

Commit

Permalink
Fix multiline wkt paste from clipboard
Browse files Browse the repository at this point in the history
Fixes #44989
  • Loading branch information
elpaso committed Sep 18, 2021
1 parent 5ea353b commit 303a6f2
Show file tree
Hide file tree
Showing 2 changed files with 97 additions and 32 deletions.
105 changes: 76 additions & 29 deletions src/app/qgsclipboard.cpp
Expand Up @@ -206,48 +206,95 @@ QgsFeatureList QgsClipboard::stringToFeatureList( const QString &string, const Q
return features;

// otherwise try to read in as WKT
const QStringList values = string.split( '\n' );
if ( values.isEmpty() || string.isEmpty() )
if ( string.isEmpty() || string.split( '\n' ).count() == 0 )
return features;

const QgsFields sourceFields = retrieveFields();

const auto constValues = values;
for ( const QString &row : constValues )
// Poor man's csv parser
bool isInsideQuotes {false};
QgsAttributes attrs;
QgsGeometry geom;
QString attrVal;
bool isFirstLine {string.startsWith( QStringLiteral( "wkt_geom" ) )};
// it seems there is no other way to check for header
const bool hasHeader{string.startsWith( QStringLiteral( "wkt_geom" ) )};
QgsGeometry geometry;
bool setFields {fields.isEmpty()};
QgsFields fieldsFromClipboard;

auto parseFunc = [ & ]( const QChar & c )
{
// Assume that it's just WKT for now. because GeoJSON is managed by
// previous QgsOgrUtils::stringToFeatureList call
// Get the first value of a \t separated list. WKT clipboard pasted
// feature has first element the WKT geom.
// This split is to fix the following issue: https://github.com/qgis/QGIS/issues/24769
// Value separators are set in generateClipboardText
QStringList fieldValues = row.split( '\t' );
if ( fieldValues.isEmpty() )
continue;

QgsFeature feature;
feature.setFields( sourceFields );
feature.initAttributes( fieldValues.size() - 1 );

//skip header line
if ( fieldValues.at( 0 ) == QLatin1String( "wkt_geom" ) )

// parse geom only if it wasn't successfully set before
if ( geometry.isNull() )
{
continue;
geometry = QgsGeometry::fromWkt( attrVal );
}

for ( int i = 1; i < fieldValues.size(); ++i )
if ( isFirstLine ) // ... name
{
feature.setAttribute( i - 1, fieldValues.at( i ) );
if ( attrVal != QStringLiteral( "wkt_geom" ) ) // ignore this one
{
fieldsFromClipboard.append( QgsField{attrVal, QVariant::String } );
}
}
else // ... or value
{
attrs.append( attrVal );
}

const QgsGeometry geometry = QgsGeometry::fromWkt( fieldValues[0] );
if ( !geometry.isNull() )
// end of record, create a new feature if it's not the header
if ( c == QChar( '\n' ) )
{
feature.setGeometry( geometry );
if ( isFirstLine )
{
isFirstLine = false;
}
else
{
QgsFeature feature{setFields ? fieldsFromClipboard : fields};
feature.setGeometry( geometry );
if ( hasHeader || !geometry.isNull() )
{
attrs.pop_front();
}
feature.setAttributes( attrs );
features.append( feature );
geometry = QgsGeometry();
attrs.clear();
}
}
attrVal.clear();
};

features.append( feature );
for ( auto c = string.constBegin(); c < string.constEnd(); ++c )
{
if ( *c == QChar( '\n' ) || *c == QChar( '\t' ) )
{
if ( isInsideQuotes )
{
attrVal.append( *c );
}
else
{
parseFunc( *c );
}
}
else if ( *c == QChar( '\"' ) )
{
isInsideQuotes = !isInsideQuotes;
}
else
{
attrVal.append( *c );
}
}

// handle missing newline
if ( !string.endsWith( QChar( '\n' ) ) )
{
parseFunc( QChar( '\n' ) );
}

return features;
}

Expand Down
24 changes: 21 additions & 3 deletions tests/src/app/testqgisappclipboard.cpp
Expand Up @@ -121,6 +121,7 @@ void TestQgisAppClipboard::copyPaste()

void TestQgisAppClipboard::copyToText()
{

//set clipboard to some QgsFeatures
QgsFields fields;
fields.append( QgsField( QStringLiteral( "int_field" ), QVariant::Int ) );
Expand Down Expand Up @@ -221,13 +222,28 @@ void TestQgisAppClipboard::copyToText()
settings.setEnumValue( QStringLiteral( "/qgis/copyFeatureFormat" ), QgsClipboard::AttributesWithWKT );
mQgisApp->clipboard()->generateClipboardText( result, resultHtml );
QCOMPARE( result, QString( "wkt_geom\tint_field\tstring_field\nPoint (5 6)\t1\tSingle line text\nPoint (7 8)\t2\t\"Unix Multiline \nText\"\nPoint (9 10)\t3\t\"Windows Multiline \r\nText\"" ) );

}

void TestQgisAppClipboard::pasteWkt()
{

// test issue GH #44989
QgsFeatureList features = mQgisApp->clipboard()->stringToFeatureList( QStringLiteral( "wkt_geom\tint_field\tstring_field\nPoint (5 6)\t1\tSingle line text\nPoint (7 8)\t2\t\"Unix Multiline \nText\"\nPoint (9 10)\t3\t\"Windows Multiline \r\nText\"" ), QgsFields() );
QCOMPARE( features.length(), 3 );
QVERIFY( features.at( 0 ).hasGeometry() && !features.at( 0 ).geometry().isNull() );
QVERIFY( features.at( 1 ).hasGeometry() && !features.at( 1 ).geometry().isNull() );
QVERIFY( features.at( 2 ).hasGeometry() && !features.at( 2 ).geometry().isNull() );
QCOMPARE( features.at( 0 ).fields().count(), 2 );
QCOMPARE( features.at( 0 ).attributeCount(), 2 );
QCOMPARE( features.at( 1 ).fields().count(), 2 );
QCOMPARE( features.at( 1 ).attributeCount(), 2 );
QCOMPARE( features.at( 2 ).fields().count(), 2 );
QCOMPARE( features.at( 2 ).attributeCount(), 2 );

mQgisApp->clipboard()->setText( QStringLiteral( "POINT (125 10)\nPOINT (111 30)" ) );

QgsFeatureList features = mQgisApp->clipboard()->copyOf();
features = mQgisApp->clipboard()->copyOf();
QCOMPARE( features.length(), 2 );
QVERIFY( features.at( 0 ).hasGeometry() && !features.at( 0 ).geometry().isNull() );
QCOMPARE( features.at( 0 ).geometry().constGet()->wkbType(), QgsWkbTypes::Point );
Expand All @@ -248,7 +264,8 @@ void TestQgisAppClipboard::pasteWkt()
features = mQgisApp->clipboard()->copyOf();
QCOMPARE( features.length(), 2 );

QVERIFY( features.at( 0 ).hasGeometry() && !features.at( 0 ).geometry().isNull() );
QVERIFY( features.at( 0 ).hasGeometry() );
QVERIFY( !features.at( 0 ).geometry().isNull() );
QCOMPARE( features.at( 0 ).geometry().constGet()->wkbType(), QgsWkbTypes::Point );
featureGeom = features.at( 0 ).geometry();
point = dynamic_cast< const QgsPoint * >( featureGeom.constGet() );
Expand All @@ -262,7 +279,7 @@ void TestQgisAppClipboard::pasteWkt()
QCOMPARE( point->y(), 10.0 );

//clipboard should support features without geometry
mQgisApp->clipboard()->setText( QStringLiteral( "\tMNL\t11\t282\tkm\t\t\t\n\tMNL\t11\t347.80000000000001\tkm\t\t\t" ) );
mQgisApp->clipboard()->setText( QStringLiteral( "MNL\t11\t282\tkm\t\t\t\nMNL\t11\t347.80000000000001\tkm\t\t\t" ) );
features = mQgisApp->clipboard()->copyOf();
QCOMPARE( features.length(), 2 );
QVERIFY( !features.at( 0 ).hasGeometry() );
Expand Down Expand Up @@ -375,6 +392,7 @@ void TestQgisAppClipboard::pasteGeoJson()
mQgisApp->clipboard()->setText( QStringLiteral( "{\n\"type\": \"Feature\",\"geometry\": {\"type\": \"Point\",\"coordinates\": [125, 10]},\"properties\": {\"name\": \"Dinagat Islands\"}}" ) );

const QgsFeatureList features = mQgisApp->clipboard()->copyOf( fields );

QCOMPARE( features.length(), 1 );
QVERIFY( features.at( 0 ).hasGeometry() && !features.at( 0 ).geometry().isNull() );
QCOMPARE( features.at( 0 ).geometry().constGet()->wkbType(), QgsWkbTypes::Point );
Expand Down

0 comments on commit 303a6f2

Please sign in to comment.