Merge pull request #61515 from rouault/wfs_no_gmlas_if_transaction

[WFS Provider] Add a UI and URI parameter featureMode=default/SimpleFeatures/ComplexFeatures
This commit is contained in:
Even Rouault 2025-04-17 12:54:24 +02:00 committed by GitHub
commit e68e87ad6c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 164 additions and 52 deletions

View File

@ -147,6 +147,7 @@ following parameters (note that parameter keys are case sensitive):
- IgnoreAxisOrientation=1: to ignore EPSG axis order for WFS 1.1 or 2.0
- InvertAxisOrientation=1: to invert axis order
- hideDownloadProgressDialog=1: to hide the download progress dialog
- featureMode=default/simpleFeatures/complexFeatures (QGIS >= 3.44)
The filter key value can either be a QGIS expression or an OGC XML
filter. If the value is set to a QGIS expression the driver will turn it

View File

@ -147,6 +147,7 @@ following parameters (note that parameter keys are case sensitive):
- IgnoreAxisOrientation=1: to ignore EPSG axis order for WFS 1.1 or 2.0
- InvertAxisOrientation=1: to invert axis order
- hideDownloadProgressDialog=1: to hide the download progress dialog
- featureMode=default/simpleFeatures/complexFeatures (QGIS >= 3.44)
The filter key value can either be a QGIS expression or an OGC XML
filter. If the value is set to a QGIS expression the driver will turn it

View File

@ -69,6 +69,7 @@ const QgsSettingsEntryEnumFlag<Qgis::TilePixelRatio> *QgsOwsConnection::settings
const QgsSettingsEntryString *QgsOwsConnection::settingsMaxNumFeatures = new QgsSettingsEntryString( QStringLiteral( "max-num-features" ), sTreeOwsConnections ) ;
const QgsSettingsEntryString *QgsOwsConnection::settingsPagesize = new QgsSettingsEntryString( QStringLiteral( "page-size" ), sTreeOwsConnections ) ;
const QgsSettingsEntryString *QgsOwsConnection::settingsPagingEnabled = new QgsSettingsEntryString( QStringLiteral( "paging-enabled" ), sTreeOwsConnections, QString( "default" ) ) ;
const QgsSettingsEntryString *QgsOwsConnection::settingsWfsFeatureMode = new QgsSettingsEntryString( QStringLiteral( "feature-mode" ), sTreeOwsConnections, QString( "default" ) ) ;
const QgsSettingsEntryBool *QgsOwsConnection::settingsPreferCoordinatesForWfsT11 = new QgsSettingsEntryBool( QStringLiteral( "prefer-coordinates-for-wfs-T11" ), sTreeOwsConnections, false ) ;
const QgsSettingsEntryBool *QgsOwsConnection::settingsIgnoreAxisOrientation = new QgsSettingsEntryBool( QStringLiteral( "ignore-axis-orientation" ), sTreeOwsConnections, false ) ;
const QgsSettingsEntryBool *QgsOwsConnection::settingsInvertAxisOrientation = new QgsSettingsEntryBool( QStringLiteral( "invert-axis-orientation" ), sTreeOwsConnections, false ) ;

View File

@ -109,6 +109,7 @@ class CORE_EXPORT QgsOwsConnection : public QObject
static const QgsSettingsEntryString *settingsMaxNumFeatures;
static const QgsSettingsEntryString *settingsPagesize;
static const QgsSettingsEntryString *settingsPagingEnabled;
static const QgsSettingsEntryString *settingsWfsFeatureMode;
static const QgsSettingsEntryBool *settingsPreferCoordinatesForWfsT11;
static const QgsSettingsEntryBool *settingsIgnoreAxisOrientation;
static const QgsSettingsEntryBool *settingsInvertAxisOrientation;

View File

@ -203,6 +203,7 @@ typedef QSet<int> QgsAttributeIds;
* - IgnoreAxisOrientation=1: to ignore EPSG axis order for WFS 1.1 or 2.0
* - InvertAxisOrientation=1: to invert axis order
* - hideDownloadProgressDialog=1: to hide the download progress dialog
* - featureMode=default/simpleFeatures/complexFeatures (QGIS >= 3.44)
*
* The filter key value can either be a QGIS expression
* or an OGC XML filter. If the value is set to a QGIS expression the driver will

View File

@ -528,6 +528,7 @@ QDomDocument QgsManageConnectionsDialog::saveWfsConnections( const QStringList &
el.setAttribute( QStringLiteral( "username" ), QgsOwsConnection::settingsUsername->value( { QStringLiteral( "wfs" ), connections[i] } ) );
el.setAttribute( QStringLiteral( "password" ), QgsOwsConnection::settingsPassword->value( { QStringLiteral( "wfs" ), connections[i] } ) );
el.setAttribute( QStringLiteral( "httpMethod" ), QgsOwsConnection::settingsPreferredHttpMethod->value( { QStringLiteral( "wfs" ), connections[i] } ) == Qgis::HttpMethod::Post ? QStringLiteral( "post" ) : QStringLiteral( "get" ) );
el.setAttribute( QStringLiteral( "featureMode" ), QgsOwsConnection::settingsWfsFeatureMode->value( { QStringLiteral( "wfs" ), connections[i] } ) );
root.appendChild( el );
}
@ -1082,6 +1083,7 @@ void QgsManageConnectionsDialog::loadWfsConnections( const QDomDocument &doc, co
QgsOwsConnection::settingsIgnoreAxisOrientation->setValue( child.attribute( QStringLiteral( "ignoreAxisOrientation" ) ).toInt(), { QStringLiteral( "wfs" ), connectionName } );
QgsOwsConnection::settingsInvertAxisOrientation->setValue( child.attribute( QStringLiteral( "invertAxisOrientation" ) ).toInt(), { QStringLiteral( "wfs" ), connectionName } );
QgsOwsConnection::settingsPreferredHttpMethod->setValue( child.attribute( QStringLiteral( "httpMethod" ) ).compare( QLatin1String( "post" ), Qt::CaseInsensitive ) == 0 ? Qgis::HttpMethod::Post : Qgis::HttpMethod::Get, { QStringLiteral( "wfs" ), connectionName } );
QgsOwsConnection::settingsWfsFeatureMode->setValue( child.attribute( QStringLiteral( "featureMode" ) ), { QStringLiteral( "wfs" ), connectionName } );
if ( !child.attribute( QStringLiteral( "username" ) ).isEmpty() )
{

View File

@ -89,6 +89,11 @@ QgsNewHttpConnection::QgsNewHttpConnection( QWidget *parent, ConnectionTypes typ
cmbVersion->addItem( tr( "OGC API - Features" ) );
connect( cmbVersion, static_cast<void ( QComboBox::* )( int )>( &QComboBox::currentIndexChanged ), this, &QgsNewHttpConnection::wfsVersionCurrentIndexChanged );
mComboWfsFeatureMode->clear();
mComboWfsFeatureMode->addItem( tr( "Default" ), QStringLiteral( "default" ) );
mComboWfsFeatureMode->addItem( tr( "Simple Features" ), QStringLiteral( "simpleFeatures" ) );
mComboWfsFeatureMode->addItem( tr( "Complex Features" ), QStringLiteral( "complexFeatures" ) );
mComboHttpMethod->addItem( QStringLiteral( "GET" ), QVariant::fromValue( Qgis::HttpMethod::Get ) );
mComboHttpMethod->addItem( QStringLiteral( "POST" ), QVariant::fromValue( Qgis::HttpMethod::Post ) );
mComboHttpMethod->setCurrentIndex( mComboHttpMethod->findData( QVariant::fromValue( Qgis::HttpMethod::Get ) ) );
@ -356,6 +361,9 @@ void QgsNewHttpConnection::updateServiceSpecificSettings()
else
cmbFeaturePaging->setCurrentIndex( static_cast<int>( QgsNewHttpConnection::WfsFeaturePagingIndex::DEFAULT ) );
const QString wfsFeatureMode = QgsOwsConnection::settingsWfsFeatureMode->value( detailsParameters );
mComboWfsFeatureMode->setCurrentIndex( std::max( mComboWfsFeatureMode->findData( wfsFeatureMode ), 0 ) );
mComboHttpMethod->setCurrentIndex( mComboHttpMethod->findData( QVariant::fromValue( QgsOwsConnection::settingsPreferredHttpMethod->value( detailsParameters ) ) ) );
txtPageSize->setText( QgsOwsConnection::settingsPagesize->value( detailsParameters ) );
}
@ -475,6 +483,9 @@ void QgsNewHttpConnection::accept()
break;
}
QgsOwsConnection::settingsPagingEnabled->setValue( pagingEnabled, detailsParameters );
const QString featureMode = mComboWfsFeatureMode->currentData().toString();
QgsOwsConnection::settingsWfsFeatureMode->setValue( featureMode, detailsParameters );
}
QStringList credentialsParameters = { mServiceName.toLower(), newConnectionName };

View File

@ -81,6 +81,16 @@ QgsWfsConnection::QgsWfsConnection( const QString &connName )
}
}
if ( settingsWfsFeatureMode->exists( detailsParameters ) )
{
mUri.removeParam( QgsWFSConstants::URI_PARAM_FEATURE_MODE ); // setParam allow for duplicates!
const QString featureMode = settingsWfsFeatureMode->value( detailsParameters );
if ( featureMode != QLatin1String( "default" ) )
{
mUri.setParam( QgsWFSConstants::URI_PARAM_FEATURE_MODE, featureMode );
}
}
QgsDebugMsgLevel( QStringLiteral( "WFS full uri: '%1'." ).arg( QString( mUri.uri() ) ), 4 );
}

View File

@ -45,5 +45,6 @@ const QString QgsWFSConstants::URI_PARAM_SKIP_INITIAL_GET_FEATURE( QStringLitera
const QString QgsWFSConstants::URI_PARAM_GEOMETRY_TYPE_FILTER( QStringLiteral( "geometryTypeFilter" ) );
const QString QgsWFSConstants::URI_PARAM_SQL( QStringLiteral( "sql" ) );
const QString QgsWFSConstants::URI_PARAM_HTTPMETHOD( QStringLiteral( "httpMethod" ) );
const QString QgsWFSConstants::URI_PARAM_FEATURE_MODE( QStringLiteral( "featureMode" ) );
const QString QgsWFSConstants::VERSION_AUTO( QStringLiteral( "auto" ) );

View File

@ -53,6 +53,7 @@ struct QgsWFSConstants
static const QString URI_PARAM_GEOMETRY_TYPE_FILTER;
static const QString URI_PARAM_SQL;
static const QString URI_PARAM_HTTPMETHOD;
static const QString URI_PARAM_FEATURE_MODE;
//
static const QString VERSION_AUTO;

View File

@ -126,6 +126,8 @@ void QgsWfsDataItemGuiProvider::duplicateConnection( QgsDataItem *item )
QgsOwsConnection::settingsHeaders->setValue( QgsOwsConnection::settingsHeaders->value( detailsParameters ), newDetailsParameters );
QgsOwsConnection::settingsWfsFeatureMode->setValue( QgsOwsConnection::settingsWfsFeatureMode->value( detailsParameters ), newDetailsParameters );
item->parent()->refreshConnections();
}

View File

@ -15,6 +15,7 @@
#include "QtGlobal"
#include "qgsmessagelog.h"
#include "qgswfsconstants.h"
#include "qgswfsdatasourceuri.h"
@ -192,7 +193,8 @@ QSet<QString> QgsWFSDataSourceURI::unknownParamKeys() const
QgsWFSConstants::URI_PARAM_SKIP_INITIAL_GET_FEATURE,
QgsWFSConstants::URI_PARAM_GEOMETRY_TYPE_FILTER,
QgsWFSConstants::URI_PARAM_SQL,
QgsWFSConstants::URI_PARAM_HTTPMETHOD
QgsWFSConstants::URI_PARAM_HTTPMETHOD,
QgsWFSConstants::URI_PARAM_FEATURE_MODE,
};
QSet<QString> l_unknownParamKeys;
@ -497,6 +499,24 @@ bool QgsWFSDataSourceURI::skipInitialGetFeature() const
return mURI.param( QgsWFSConstants::URI_PARAM_SKIP_INITIAL_GET_FEATURE ).toUpper() == QLatin1String( "TRUE" );
}
QgsWFSDataSourceURI::FeatureMode QgsWFSDataSourceURI::featureMode() const
{
if ( !mURI.hasParam( QgsWFSConstants::URI_PARAM_FEATURE_MODE ) )
return FeatureMode::Default;
const QString val = mURI.param( QgsWFSConstants::URI_PARAM_FEATURE_MODE );
if ( val == QLatin1String( "default" ) )
return FeatureMode::Default;
else if ( val == QLatin1String( "simpleFeatures" ) )
return FeatureMode::SimpleFeatures;
else if ( val == QLatin1String( "complexFeatures" ) )
return FeatureMode::ComplexFeatures;
else
{
QgsMessageLog::logMessage( QObject::tr( "Unknown value for featureMode URI parameter '%1'" ).arg( val ), QObject::tr( "WFS" ) );
return FeatureMode::Default;
}
}
QString QgsWFSDataSourceURI::build( const QString &baseUri, const QString &typeName, const QString &crsString, const QString &sql, const QString &filter, bool restrictToCurrentViewExtent )
{
QgsWFSDataSourceURI uri( baseUri );

View File

@ -135,6 +135,17 @@ class QgsWFSDataSourceURI
//! Returns authorization parameters
const QgsAuthorizationSettings &auth() const { return mAuth; }
//! How to analyze DescribeFeatureType response
enum class FeatureMode
{
Default, //! If the server supports transaction, same as SIMPLE_FEATURE. Otherwise COMPLEX_FEATURE
SimpleFeatures, //! Analyze DescribeFeatureType response with QGIS built-in Simple Feature analyzer
ComplexFeatures, //! Analyze DescribeFeatureType response with OGR GMLAS Complex Feature analyzer
};
//! Returns how to analyze DescribeFeatureType response.
FeatureMode featureMode() const;
//! Builds a derived uri from a base uri
static QString build( const QString &uri, const QString &typeName, const QString &crsString = QString(), const QString &sql = QString(), const QString &filter = QString(), bool restrictToCurrentViewExtent = false );

View File

@ -1532,7 +1532,11 @@ bool QgsWFSProvider::readAttributesFromSchema( QDomDocument &schemaDoc, const QB
geometryMaybeMissing = false;
bool mayTryWithGMLAS = false;
bool ret = readAttributesFromSchemaWithoutGMLAS( schemaDoc, prefixedTypename, geometryAttribute, fields, geomType, errorMsg, mayTryWithGMLAS );
if ( singleLayerContext && mayTryWithGMLAS && GDALGetDriverByName( "GMLAS" ) )
// Only consider GMLAS / ComplexFeatures mode if FeatureMode=DEFAULT and there
// is no edition capabilities, or if explicitly requested.
// Cf https://github.com/qgis/QGIS/pull/61493
if ( ( ( mShared->mURI.featureMode() == QgsWFSDataSourceURI::FeatureMode::Default && ( mCapabilities & Qgis::VectorProviderCapability::AddFeatures ) == 0 ) || mShared->mURI.featureMode() == QgsWFSDataSourceURI::FeatureMode::ComplexFeatures ) && singleLayerContext && mayTryWithGMLAS && GDALGetDriverByName( "GMLAS" ) )
{
QString geometryAttributeGMLAS;
QgsFields fieldsGMLAS;

View File

@ -32,51 +32,36 @@
<string>WFS Options</string>
</property>
<layout class="QGridLayout" name="gridLayout1">
<item row="0" column="0">
<widget class="QLabel" name="lblVersion">
<item row="5" column="0">
<widget class="QLabel" name="lblFeatureMode">
<property name="text">
<string>Version</string>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QPushButton" name="mWfsVersionDetectButton">
<property name="text">
<string>Detect</string>
</property>
</widget>
</item>
<item row="5" column="0" colspan="3">
<widget class="QCheckBox" name="cbxWfsIgnoreAxisOrientation">
<property name="text">
<string>Ignore axis orientation (WFS 1.1/WFS 2.0)</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="lblMaxNumFeatures">
<property name="text">
<string>Max. number of features</string>
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Feature mode&lt;br/&gt;(Simple vs Complex)&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="cmbVersion"/>
</item>
<item row="4" column="1" colspan="2">
<widget class="QLineEdit" name="txtPageSize">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Enter a number to limit the maximum number of features retrieved in a single GetFeature request when paging is enabled. If let to empty, server default will apply.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
<item row="6" column="0" colspan="3">
<item row="7" column="0" colspan="3">
<widget class="QCheckBox" name="cbxWfsInvertAxisOrientation">
<property name="text">
<string>Invert axis orientation</string>
</property>
</widget>
</item>
<item row="1" column="1" colspan="2">
<widget class="QComboBox" name="mComboHttpMethod"/>
</item>
<item row="8" column="0" colspan="2">
<widget class="QCheckBox" name="cbxWfsUseGml2EncodingForTransactions">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;This might be necessary on some &lt;span style=&quot; font-weight:600;&quot;&gt;broken&lt;/span&gt; ESRI map servers when using WFS-T 1.1.0.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Use GML2 encoding for transactions</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="lblPageSize">
<property name="text">
@ -84,30 +69,24 @@
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="lblFeaturePaging">
<item row="2" column="0">
<widget class="QLabel" name="lblMaxNumFeatures">
<property name="text">
<string>Feature paging</string>
<string>Max. number of features</string>
</property>
</widget>
</item>
<item row="3" column="1" colspan="2">
<widget class="QComboBox" name="cmbFeaturePaging"/>
</item>
<item row="2" column="1" colspan="2">
<widget class="QLineEdit" name="txtMaxNumFeatures">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Enter a number to limit the maximum number of features retrieved per feature request. If let to empty, no limit is set.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
<item row="0" column="0">
<widget class="QLabel" name="lblVersion">
<property name="text">
<string>Version</string>
</property>
</widget>
</item>
<item row="7" column="0" colspan="2">
<widget class="QCheckBox" name="cbxWfsUseGml2EncodingForTransactions">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;This might be necessary on some &lt;span style=&quot; font-weight:600;&quot;&gt;broken&lt;/span&gt; ESRI map servers when using WFS-T 1.1.0.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<item row="6" column="0" colspan="3">
<widget class="QCheckBox" name="cbxWfsIgnoreAxisOrientation">
<property name="text">
<string>Use GML2 encoding for transactions</string>
<string>Ignore axis orientation (WFS 1.1/WFS 2.0)</string>
</property>
</widget>
</item>
@ -118,8 +97,39 @@
</property>
</widget>
</item>
<item row="1" column="1" colspan="2">
<widget class="QComboBox" name="mComboHttpMethod"/>
<item row="2" column="1" colspan="2">
<widget class="QLineEdit" name="txtMaxNumFeatures">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Enter a number to limit the maximum number of features retrieved per feature request. If let to empty, no limit is set.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QPushButton" name="mWfsVersionDetectButton">
<property name="text">
<string>Detect</string>
</property>
</widget>
</item>
<item row="4" column="1" colspan="2">
<widget class="QLineEdit" name="txtPageSize">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Enter a number to limit the maximum number of features retrieved in a single GetFeature request when paging is enabled. If let to empty, server default will apply.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
<item row="3" column="1" colspan="2">
<widget class="QComboBox" name="cmbFeaturePaging"/>
</item>
<item row="5" column="1" colspan="2">
<widget class="QComboBox" name="mComboWfsFeatureMode"/>
</item>
<item row="3" column="0">
<widget class="QLabel" name="lblFeaturePaging">
<property name="text">
<string>Feature paging</string>
</property>
</widget>
</item>
</layout>
</widget>
@ -394,6 +404,7 @@
<tabstop>txtMaxNumFeatures</tabstop>
<tabstop>cmbFeaturePaging</tabstop>
<tabstop>txtPageSize</tabstop>
<tabstop>mComboWfsFeatureMode</tabstop>
<tabstop>cbxWfsIgnoreAxisOrientation</tabstop>
<tabstop>cbxWfsInvertAxisOrientation</tabstop>
<tabstop>cbxWfsUseGml2EncodingForTransactions</tabstop>

View File

@ -8757,6 +8757,40 @@ Can't recognize service requested.
got_f = [f for f in vl.getFeatures()]
self.assertEqual(len(got_f), 1)
# Test the various values of featureMode URI parameter
with MessageLogger("WFS") as logger:
vl = QgsVectorLayer(
"url='http://"
+ endpoint
+ "' typename='ps:ProtectedSite' version='2.0.0' skipInitialGetFeature='true' featureMode='simpleFeatures'",
"test",
"WFS",
)
self.assertFalse(vl.isValid())
self.assertEqual(len(logger.messages()), 1, logger.messages())
self.assertIn(
"It is probably a schema for Complex Features",
logger.messages()[0].decode("UTF-8"),
)
vl = QgsVectorLayer(
"url='http://"
+ endpoint
+ "' typename='ps:ProtectedSite' version='2.0.0' skipInitialGetFeature='true' featureMode='complexFeatures'",
"test",
"WFS",
)
self.assertTrue(vl.isValid())
vl = QgsVectorLayer(
"url='http://"
+ endpoint
+ "' typename='ps:ProtectedSite' version='2.0.0' skipInitialGetFeature='true' featureMode='default'",
"test",
"WFS",
)
self.assertTrue(vl.isValid())
class TestPyQgsWFSProviderPost(QgisTestCase, ProviderTestCase):
"""