diff --git a/src/providers/wfs/CMakeLists.txt b/src/providers/wfs/CMakeLists.txt index 045f8757be8..c553d933653 100644 --- a/src/providers/wfs/CMakeLists.txt +++ b/src/providers/wfs/CMakeLists.txt @@ -63,6 +63,7 @@ target_include_directories(provider_wfs_a PUBLIC target_link_libraries(provider_wfs_a qgis_core + GDAL::GDAL ) # require c++17 @@ -120,6 +121,7 @@ else() target_link_libraries (provider_wfs qgis_core + GDAL::GDAL ) if (WITH_GUI) diff --git a/src/providers/wfs/qgswfsfeatureiterator.cpp b/src/providers/wfs/qgswfsfeatureiterator.cpp index a88e1d7101b..a10192bb855 100644 --- a/src/providers/wfs/qgswfsfeatureiterator.cpp +++ b/src/providers/wfs/qgswfsfeatureiterator.cpp @@ -213,12 +213,24 @@ QUrl QgsWFSFeatureDownloaderImpl::buildURL( qint64 startIndex, long long maxFeat arg( minx ).arg( miny ).arg( maxx ).arg( maxy ) ); QgsExpression bboxExp( filterBbox ); QDomDocument bboxDoc; + + QMap fieldNameToXPathMap; + if ( !mShared->mFieldNameToXPathAndIsNestedContentMap.isEmpty() ) + { + for ( auto iterFieldName = mShared->mFieldNameToXPathAndIsNestedContentMap.constBegin(); iterFieldName != mShared->mFieldNameToXPathAndIsNestedContentMap.constEnd(); ++iterFieldName ) + { + const QString &fieldName = iterFieldName.key(); + const auto &value = iterFieldName.value(); + fieldNameToXPathMap[fieldName] = value.first; + } + } + QDomElement bboxElem = QgsOgcUtils::expressionToOgcFilter( bboxExp, bboxDoc, gmlVersion, filterVersion, mShared->mLayerPropertiesList.size() == 1 ? mShared->mLayerPropertiesList[0].mNamespacePrefix : QString(), mShared->mLayerPropertiesList.size() == 1 ? mShared->mLayerPropertiesList[0].mNamespaceURI : QString(), geometryAttribute, mShared->srsName(), - honourAxisOrientation, mShared->mURI.invertAxisOrientation() ); + honourAxisOrientation, mShared->mURI.invertAxisOrientation(), nullptr, fieldNameToXPathMap, mShared->mNamespacePrefixToURIMap ); bboxDoc.appendChild( bboxElem ); filters.push_back( bboxDoc.toString() ); diff --git a/src/providers/wfs/qgswfsprovider.cpp b/src/providers/wfs/qgswfsprovider.cpp index 2405a9988e5..e50d58cfe37 100644 --- a/src/providers/wfs/qgswfsprovider.cpp +++ b/src/providers/wfs/qgswfsprovider.cpp @@ -16,13 +16,17 @@ ***************************************************************************/ #include "qgis.h" +#include "qgscplhttpfetchoverrider.h" #include "qgsfeature.h" +#include "qgsfeedback.h" #include "qgsfields.h" #include "qgsgeometry.h" #include "qgscoordinatereferencesystem.h" #include "qgslogger.h" #include "qgsmessagelog.h" #include "qgsogcutils.h" +#include "qgsogrutils.h" +#include "qgssqliteutils.h" #include "qgswfsconstants.h" #include "qgswfsfeatureiterator.h" #include "qgswfsprovider.h" @@ -33,6 +37,10 @@ #include "qgswfsutils.h" #include "qgssettings.h" +#include "cpl_string.h" +#include "gdal.h" + +#include #include #include #include @@ -605,6 +613,8 @@ bool QgsWFSProvider::processSQL( const QString &sqlString, QString &errorMsg, QS QgsFields fields; Qgis::WkbType geomType; if ( !readAttributesFromSchema( describeFeatureDocument, + response, + /* singleLayerContext = */ typenameList.size() == 1, typeName, geometryAttribute, fields, geomType, errorMsg ) ) { @@ -624,7 +634,7 @@ bool QgsWFSProvider::processSQL( const QString &sqlString, QString &errorMsg, QS } } - setLayerPropertiesListFromDescribeFeature( describeFeatureDocument, typenameList, errorMsg ); + setLayerPropertiesListFromDescribeFeature( describeFeatureDocument, response, typenameList, errorMsg ); const QString &defaultTypeName = mShared->mURI.typeName(); QgsWFSProviderSQLColumnRefValidator oColumnValidator( @@ -789,7 +799,7 @@ bool QgsWFSProvider::processSQL( const QString &sqlString, QString &errorMsg, QS return true; } -bool QgsWFSProvider::setLayerPropertiesListFromDescribeFeature( QDomDocument &describeFeatureDocument, const QStringList &typenameList, QString &errorMsg ) +bool QgsWFSProvider::setLayerPropertiesListFromDescribeFeature( QDomDocument &describeFeatureDocument, const QByteArray &response, const QStringList &typenameList, QString &errorMsg ) { mShared->mLayerPropertiesList.clear(); for ( const QString &typeName : typenameList ) @@ -798,6 +808,8 @@ bool QgsWFSProvider::setLayerPropertiesListFromDescribeFeature( QDomDocument &de QgsFields fields; Qgis::WkbType geomType; if ( !readAttributesFromSchema( describeFeatureDocument, + response, + /* singleLayerContext = */ typenameList.size() == 1, typeName, geometryAttribute, fields, geomType, errorMsg ) ) { @@ -1513,6 +1525,8 @@ bool QgsWFSProvider::describeFeatureType( QString &geometryAttribute, QgsFields } if ( !readAttributesFromSchema( describeFeatureDocument, + response, + /* singleLayerContext = */ true, mShared->mURI.typeName(), geometryAttribute, fields, geomType, errorMsg ) ) { @@ -1522,18 +1536,478 @@ bool QgsWFSProvider::describeFeatureType( QString &geometryAttribute, QgsFields return false; } - setLayerPropertiesListFromDescribeFeature( describeFeatureDocument, {mShared->mURI.typeName()}, errorMsg ); + setLayerPropertiesListFromDescribeFeature( describeFeatureDocument, response, {mShared->mURI.typeName()}, errorMsg ); return true; } + bool QgsWFSProvider::readAttributesFromSchema( QDomDocument &schemaDoc, + const QByteArray &response, + bool singleLayerContext, const QString &prefixedTypename, QString &geometryAttribute, QgsFields &fields, Qgis::WkbType &geomType, QString &errorMsg ) { + bool mayTryWithGMLAS = false; + bool ret = readAttributesFromSchemaWithoutGMLAS( schemaDoc, prefixedTypename, geometryAttribute, fields, geomType, errorMsg, mayTryWithGMLAS ); + if ( singleLayerContext && + mayTryWithGMLAS && + GDALGetDriverByName( "GMLAS" ) ) + { + QgsFields fieldsGMLAS; + Qgis::WkbType geomTypeGMLAS; + QString errorMsgGMLAS; + if ( readAttributesFromSchemaWithGMLAS( response, prefixedTypename, geometryAttribute, fieldsGMLAS, geomTypeGMLAS, errorMsgGMLAS ) ) + { + fields = fieldsGMLAS; + geomType = geomTypeGMLAS; + ret = true; + } + else if ( !ret ) + { + errorMsg = errorMsgGMLAS; + } + } + return ret; +} + +static QVariant::Type getVariantTypeFromXML( const QString &xmlType ) +{ + QVariant::Type attributeType = QVariant::Invalid; + + const QString type = QString( xmlType ) + .replace( QLatin1String( "xs:" ), QString() ) + .replace( QLatin1String( "xsd:" ), QString() ); + + if ( type.compare( QLatin1String( "string" ), Qt::CaseInsensitive ) == 0 || + type.compare( QLatin1String( "token" ), Qt::CaseInsensitive ) == 0 || + type.compare( QLatin1String( "NMTOKEN" ), Qt::CaseInsensitive ) == 0 || + type.compare( QLatin1String( "NCName" ), Qt::CaseInsensitive ) == 0 || + type.compare( QLatin1String( "QName" ), Qt::CaseInsensitive ) == 0 || + type.compare( QLatin1String( "ID" ), Qt::CaseInsensitive ) == 0 || + type.compare( QLatin1String( "IDREF" ), Qt::CaseInsensitive ) == 0 || + type.compare( QLatin1String( "anyURI" ), Qt::CaseInsensitive ) == 0 || + type.compare( QLatin1String( "anySimpleType" ), Qt::CaseInsensitive ) == 0 ) + { + attributeType = QVariant::String; + } + else if ( type.compare( QLatin1String( "boolean" ), Qt::CaseInsensitive ) == 0 ) + { + attributeType = QVariant::Bool; + } + else if ( type.compare( QLatin1String( "double" ), Qt::CaseInsensitive ) == 0 || + type.compare( QLatin1String( "float" ), Qt::CaseInsensitive ) == 0 || + type.compare( QLatin1String( "decimal" ), Qt::CaseInsensitive ) == 0 ) + { + attributeType = QVariant::Double; + } + else if ( type.compare( QLatin1String( "byte" ), Qt::CaseInsensitive ) == 0 || + type.compare( QLatin1String( "unsignedByte" ), Qt::CaseInsensitive ) == 0 || + type.compare( QLatin1String( "int" ), Qt::CaseInsensitive ) == 0 || + type.compare( QLatin1String( "short" ), Qt::CaseInsensitive ) == 0 || + type.compare( QLatin1String( "unsignedShort" ), Qt::CaseInsensitive ) == 0 ) + { + attributeType = QVariant::Int; + } + else if ( type.compare( QLatin1String( "long" ), Qt::CaseInsensitive ) == 0 || + type.compare( QLatin1String( "unsignedLong" ), Qt::CaseInsensitive ) == 0 || + type.compare( QLatin1String( "integer" ), Qt::CaseInsensitive ) == 0 || + type.compare( QLatin1String( "negativeInteger" ), Qt::CaseInsensitive ) == 0 || + type.compare( QLatin1String( "nonNegativeInteger" ), Qt::CaseInsensitive ) == 0 || + type.compare( QLatin1String( "positiveInteger" ), Qt::CaseInsensitive ) == 0 ) + { + attributeType = QVariant::LongLong; + } + else if ( type.compare( QLatin1String( "date" ), Qt::CaseInsensitive ) == 0 || + type.compare( QLatin1String( "gYear" ), Qt::CaseInsensitive ) == 0 || + type.compare( QLatin1String( "gYearMonth" ), Qt::CaseInsensitive ) == 0 ) + { + attributeType = QVariant::Date; + } + else if ( type.compare( QLatin1String( "time" ), Qt::CaseInsensitive ) == 0 ) + { + attributeType = QVariant::Time; + } + else if ( type.compare( QLatin1String( "dateTime" ), Qt::CaseInsensitive ) == 0 ) + { + attributeType = QVariant::DateTime; + } + return attributeType; +} + +bool QgsWFSProvider::readAttributesFromSchemaWithGMLAS( const QByteArray &response, + const QString &prefixedTypename, + QString &geometryAttribute, + QgsFields &fields, + Qgis::WkbType &geomType, + QString &errorMsg ) +{ + QUrl url( mShared->mURI.requestUrl( QStringLiteral( "DescribeFeatureType" ) ) ); + QUrlQuery query( url ); + query.addQueryItem( QStringLiteral( "TYPENAME" ), prefixedTypename ); + url.setQuery( query ); + + // If a previous attempt with the same URL failed because of cancellation + // in the past second, do not retry. + // The main use case for that is when QgsWfsProviderMetadata::querySublayers() + // is called when adding a layer, and several QgsWFSProvider instances are + // quickly created. + static QMutex mutex; + static QUrl lastCanceledURL; + static QDateTime lastCanceledDateTime; + { + QMutexLocker lock( &mutex ); + if ( lastCanceledURL == url && lastCanceledDateTime + 1 > QDateTime::currentDateTime() ) + { + mMetadataRetrievalCanceled = true; + return false; + } + } + + // Create a unique /vsimem/ filename + constexpr int TEMP_FILENAME_SIZE = 128; + void *p = malloc( TEMP_FILENAME_SIZE ); + char *pszSchemaTempFilename = static_cast( p ); + snprintf( pszSchemaTempFilename, TEMP_FILENAME_SIZE, "/vsimem/schema_%p.xsd", p ); + + // Serialize the main schema into a temporary /vsimem/ filename + char *pszSchema = VSIStrdup( response.constData() ); + VSILFILE *fp = VSIFileFromMemBuffer( pszSchemaTempFilename, + reinterpret_cast( pszSchema ), strlen( pszSchema ), /* bTakeOwnership=*/ true ); + if ( fp ) + VSIFCloseL( fp ); + + QgsFeedback feedback; + GDALDatasetH hDS = nullptr; + + // Analyze the DescribeFeatureType response schema with the OGR GMLAS driver + // in a thread, so it can get interrupted (with GDAL 3.9: https://github.com/OSGeo/gdal/pull/9019) + const auto downloaderLambda = [pszSchemaTempFilename, &feedback, &hDS]() + { + QgsCPLHTTPFetchOverrider cplHTTPFetchOverrider( QString(), &feedback ); + QgsSetCPLHTTPFetchOverriderInitiatorClass( cplHTTPFetchOverrider, QStringLiteral( "WFSProviderDownloadSchema" ) ) + + char **papszOpenOptions = nullptr; + papszOpenOptions = CSLSetNameValue( papszOpenOptions, "XSD", pszSchemaTempFilename ); + hDS = GDALOpenEx( "GMLAS:", GDAL_OF_VECTOR, nullptr, papszOpenOptions, nullptr ); + CSLDestroy( papszOpenOptions ); + }; + + std::unique_ptr<_DownloaderThread> downloaderThread = + std::make_unique<_DownloaderThread>( downloaderLambda ); + downloaderThread->start(); + + QTimer timerForHits; + + QMessageBox *box = nullptr; + QWidget *parentWidget = nullptr; + if ( qApp->thread() == QThread::currentThread() ) + { + parentWidget = QApplication::activeWindow(); + if ( !parentWidget ) + { + const QWidgetList widgets = QgsApplication::topLevelWidgets(); + for ( QWidget *widget : widgets ) + { + if ( widget->objectName() == QLatin1String( "QgisApp" ) ) + { + parentWidget = widget; + break; + } + } + } + } + if ( parentWidget ) + { + // Display an information box if within 2 seconds, the schema has not + // been analyzed. + box = new QMessageBox( + QMessageBox::Information, tr( "Information" ), tr( "Download of schemas in progress..." ), + QMessageBox::Cancel, + parentWidget ); +#if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,9,0) + connect( box, &QDialog::rejected, &feedback, &QgsFeedback::cancel ); +#else + box->button( QMessageBox::Cancel )->setEnabled( false ); +#endif + + QgsSettings s; + const double settingDefaultValue = 2.0; + const QString settingName = QStringLiteral( "qgis/wfsDownloadSchemasPopupTimeout" ); + if ( !s.contains( settingName ) ) + { + s.setValue( settingName, settingDefaultValue ); + } + const double timeout = s.value( settingName, settingDefaultValue ).toDouble(); + if ( timeout > 0 ) + { + timerForHits.setInterval( static_cast( 1000 * timeout ) ); + timerForHits.setSingleShot( true ); + timerForHits.start(); + connect( &timerForHits, &QTimer::timeout, box, &QDialog::exec ); + } + + // Close dialog when download theread finishes. + // Will actually trigger the QDialog::rejected signal... + connect( downloaderThread.get(), &QThread::finished, box, &QDialog::accept ); + } + + // Run an event loop until download thread finishes + QEventLoop loop; + connect( downloaderThread.get(), &QThread::finished, &loop, &QEventLoop::quit ); + loop.exec( QEventLoop::ExcludeUserInputEvents ); + downloaderThread->wait(); + + VSIUnlink( pszSchemaTempFilename ); + VSIFree( pszSchemaTempFilename ); + + bool ret = hDS != nullptr; + if ( feedback.isCanceled() && !ret ) + { + QMutexLocker lock( &mutex ); + mMetadataRetrievalCanceled = true; + lastCanceledURL = url; + lastCanceledDateTime = QDateTime::currentDateTime(); + errorMsg = tr( "Schema analysis interrupted by user." ); + return false; + } + if ( !ret ) + { + errorMsg = tr( "Cannot analyze schema indicated in DescribeFeatureType response." ); + return false; + } + + gdal::dataset_unique_ptr oDSCloser( hDS ); + + // Retrieve namespace prefix and URIs + OGRLayerH hOtherMetadataLayer = GDALDatasetGetLayerByName( hDS, "_ogr_other_metadata" ); + if ( !hOtherMetadataLayer ) + { + // should not happen + QgsDebugMsgLevel( QStringLiteral( "Cannot find _ogr_other_metadata layer" ), 4 ); + return false; + } + + auto hOtherMetadataLayerDefn = OGR_L_GetLayerDefn( hOtherMetadataLayer ); + + const int keyIdx = OGR_FD_GetFieldIndex( hOtherMetadataLayerDefn, "key" ); + if ( keyIdx < 0 ) + { + // should not happen + QgsDebugMsgLevel( QStringLiteral( "Cannot find key field in _ogr_other_metadata" ), 4 ); + return false; + } + + const int valueIdx = OGR_FD_GetFieldIndex( hOtherMetadataLayerDefn, "value" ); + if ( valueIdx < 0 ) + { + // should not happen + QgsDebugMsgLevel( QStringLiteral( "Cannot find value field in _ogr_other_metadata" ), 4 ); + return false; + } + + std::map> mapPrefixIdxToPrefixAndUri; + while ( true ) + { + gdal::ogr_feature_unique_ptr hFeatureOtherMD( + OGR_L_GetNextFeature( hOtherMetadataLayer ) ); + if ( !hFeatureOtherMD ) + break; + + const QString key = QString::fromUtf8( + OGR_F_GetFieldAsString( hFeatureOtherMD.get(), keyIdx ) ); + const QString value = QString::fromUtf8( + OGR_F_GetFieldAsString( hFeatureOtherMD.get(), valueIdx ) ); + + if ( key.startsWith( QLatin1String( "namespace_prefix_" ) ) ) + { + mapPrefixIdxToPrefixAndUri[key.mid( int( strlen( "namespace_prefix_" ) ) ).toInt()].first = value; + } + else if ( key.startsWith( QLatin1String( "namespace_uri_" ) ) ) + { + mapPrefixIdxToPrefixAndUri[key.mid( int( strlen( "namespace_uri_" ) ) ).toInt()].second = value; + } + } + for ( const auto &kv : mapPrefixIdxToPrefixAndUri ) + { + if ( !kv.second.first.isEmpty() && !kv.second.second.isEmpty() ) + { + mShared->mNamespacePrefixToURIMap[kv.second.first] = kv.second.second; + QgsDebugMsgLevel( QStringLiteral( "%1 -> %2" ).arg( kv.second.first ).arg( kv.second.second ), 4 ); + } + } + + // Find the layer of interest + OGRLayerH hLayersMetadata = GDALDatasetGetLayerByName( hDS, "_ogr_layers_metadata" ); + if ( !hLayersMetadata ) + { + // should not happen + QgsDebugMsgLevel( QStringLiteral( "Cannot find _ogr_layers_metadata layer" ), 4 ); + return false; + } + OGR_L_SetAttributeFilter( hLayersMetadata, + ( "layer_xpath = " + QgsSqliteUtils::quotedString( prefixedTypename ).toStdString() ).c_str() ); + gdal::ogr_feature_unique_ptr hFeatureLayersMD( OGR_L_GetNextFeature( hLayersMetadata ) ); + if ( !hFeatureLayersMD ) + { + QgsDebugMsgLevel( + QStringLiteral( "Cannot find feature with layer_xpath = %1 in _ogr_layers_metadata" ).arg( prefixedTypename ), 4 ); + return false; + } + const int fldIdx = OGR_F_GetFieldIndex( hFeatureLayersMD.get(), "layer_name" ); + if ( fldIdx < 0 ) + { + // should not happen + QgsDebugMsgLevel( QStringLiteral( "Cannot find layer_name field in _ogr_layers_metadata" ), 4 ); + return false; + } + const QString layerName = QString::fromUtf8( + OGR_F_GetFieldAsString( hFeatureLayersMD.get(), fldIdx ) ); + + OGRLayerH hLayer = GDALDatasetGetLayerByName( + hDS, layerName.toStdString().c_str() ); + if ( !hLayer ) + { + // should not happen + QgsDebugMsgLevel( QStringLiteral( "Cannot find %& layer" ).arg( layerName ), 4 ); + return false; + } + + // Get field information + OGRLayerH hFieldsMetadata = GDALDatasetGetLayerByName( hDS, "_ogr_fields_metadata" ); + if ( !hFieldsMetadata ) + { + // should not happen + QgsDebugMsgLevel( QStringLiteral( "Cannot find _ogr_fields_metadata layer" ), 4 ); + return false; + } + OGR_L_SetAttributeFilter( hFieldsMetadata, + ( "layer_name = " + QgsSqliteUtils::quotedString( layerName ).toStdString() ).c_str() ); + + auto hFieldsMetadataDefn = OGR_L_GetLayerDefn( hFieldsMetadata ); + + const int fieldNameIdx = OGR_FD_GetFieldIndex( hFieldsMetadataDefn, "field_name" ); + if ( fieldNameIdx < 0 ) + { + // should not happen + QgsDebugMsgLevel( QStringLiteral( "Cannot find field_name field in _ogr_fields_metadata" ), 4 ); + return false; + } + + const int fieldXPathIdx = OGR_FD_GetFieldIndex( hFieldsMetadataDefn, "field_xpath" ); + if ( fieldXPathIdx < 0 ) + { + // should not happen + QgsDebugMsgLevel( QStringLiteral( "Cannot find field_xpath field in _ogr_fields_metadata" ), 4 ); + return false; + } + + const int fieldIsListIdx = OGR_FD_GetFieldIndex( hFieldsMetadataDefn, "field_is_list" ); + if ( fieldIsListIdx < 0 ) + { + // should not happen + QgsDebugMsgLevel( QStringLiteral( "Cannot find field_is_list field in _ogr_fields_metadata" ), 4 ); + return false; + } + + const int fieldTypeIdx = OGR_FD_GetFieldIndex( hFieldsMetadataDefn, "field_type" ); + if ( fieldTypeIdx < 0 ) + { + // should not happen + QgsDebugMsgLevel( QStringLiteral( "Cannot find field_type field in _ogr_fields_metadata" ), 4 ); + return false; + } + + const int fieldCategoryIdx = OGR_FD_GetFieldIndex( hFieldsMetadataDefn, "field_category" ); + if ( fieldCategoryIdx < 0 ) + { + // should not happen + QgsDebugMsgLevel( QStringLiteral( "Cannot find field_category field in _ogr_fields_metadata" ), 4 ); + return false; + } + + mShared->mFieldNameToXPathAndIsNestedContentMap.clear(); + while ( true ) + { + gdal::ogr_feature_unique_ptr hFeatureFieldsMD( OGR_L_GetNextFeature( hFieldsMetadata ) ); + if ( !hFeatureFieldsMD ) + break; + + QString fieldName = QString::fromUtf8( OGR_F_GetFieldAsString( hFeatureFieldsMD.get(), fieldNameIdx ) ); + const char *fieldXPath = OGR_F_GetFieldAsString( hFeatureFieldsMD.get(), fieldXPathIdx ); + // The xpath includes the one of the feature itself. We can strip it off + const char *slash = strchr( fieldXPath, '/' ); + if ( slash ) + fieldXPath = slash + 1; + const bool fieldIsList = OGR_F_GetFieldAsInteger( hFeatureFieldsMD.get(), fieldIsListIdx ) == 1; + const char *fieldType = OGR_F_GetFieldAsString( hFeatureFieldsMD.get(), fieldTypeIdx ); + const char *fieldCategory = OGR_F_GetFieldAsString( hFeatureFieldsMD.get(), fieldCategoryIdx ); + + // For fields that should be linked to other tables and that we will + // get as JSON, remove the "_pkid" suffix from the name created by GMLAS. + if ( EQUAL( fieldCategory, "PATH_TO_CHILD_ELEMENT_WITH_LINK" ) && + fieldName.endsWith( QLatin1String( "_pkid" ) ) ) + { + fieldName.resize( fieldName.size() - int( strlen( "_pkid" ) ) ); + } + + QgsDebugMsgLevel( + QStringLiteral( "field %1: xpath=%2 is_list=%3 type=%4 category=%5" ). + arg( fieldName ).arg( fieldXPath ).arg( fieldIsList ).arg( fieldType ).arg( fieldCategory ), 5 ); + if ( EQUAL( fieldCategory, "REGULAR" ) && EQUAL( fieldType, "geometry" ) ) + { + if ( geometryAttribute.isEmpty() ) + { + mShared->mFieldNameToXPathAndIsNestedContentMap[fieldName] = + QPair( fieldXPath, false ); + geometryAttribute = fieldName; + geomType = QgsOgrUtils::ogrGeometryTypeToQgsWkbType( + OGR_L_GetGeomType( hLayer ) ); + } + } + else if ( EQUAL( fieldCategory, "REGULAR" ) && !fieldIsList ) + { + QVariant::Type type = getVariantTypeFromXML( QString::fromUtf8( fieldType ) ); + if ( type != QVariant::Invalid ) + { + fields.append( QgsField( fieldName, type, fieldType ) ); + } + else + { + // unhandled:duration, base64Binary, hexBinary, anyType + QgsDebugMsgLevel( + QStringLiteral( "unhandled type for field %1: xpath=%2 is_list=%3 type=%4 category=%5" ). + arg( fieldName ).arg( fieldXPath ).arg( fieldIsList ).arg( fieldType ).arg( fieldCategory ), 3 ); + fields.append( QgsField( fieldName, QVariant::String, fieldType ) ); + } + mShared->mFieldNameToXPathAndIsNestedContentMap[fieldName] = + QPair( fieldXPath, false ); + } + else + { + QgsField field( fieldName, QVariant::String ); + field.setEditorWidgetSetup( QgsEditorWidgetSetup( QStringLiteral( "JsonEdit" ), QVariantMap() ) ); + fields.append( field ); + mShared->mFieldNameToXPathAndIsNestedContentMap[fieldName] = + QPair( fieldXPath, true ); + } + } + + return true; +} + +bool QgsWFSProvider::readAttributesFromSchemaWithoutGMLAS( QDomDocument &schemaDoc, + const QString &prefixedTypename, + QString &geometryAttribute, + QgsFields &fields, + Qgis::WkbType &geomType, + QString &errorMsg, bool &mayTryWithGMLAS ) +{ + mayTryWithGMLAS = false; + //get the root element QDomNodeList schemaNodeList = schemaDoc.elementsByTagNameNS( QgsWFSConstants::XMLSCHEMA_NAMESPACE, QStringLiteral( "schema" ) ); if ( schemaNodeList.length() < 1 ) @@ -1615,6 +2089,7 @@ bool QgsWFSProvider::readAttributesFromSchema( QDomDocument &schemaDoc, if ( foundImport && onlyIncludeOrImport ) { errorMsg = tr( "It is probably a schema for Complex Features." ); + mayTryWithGMLAS = true; } // e.g http://services.cuzk.cz/wfs/inspire-CP-wfs.asp?SERVICE=WFS&VERSION=2.0.0&REQUEST=DescribeFeatureType // which has a single @@ -1643,17 +2118,18 @@ bool QgsWFSProvider::readAttributesFromSchema( QDomDocument &schemaDoc, arg( schemaLocation, errorMsg ); } - return readAttributesFromSchema( describeFeatureDocument, - prefixedTypename, - geometryAttribute, - fields, - geomType, - errorMsg ); + return readAttributesFromSchemaWithoutGMLAS( describeFeatureDocument, + prefixedTypename, + geometryAttribute, + fields, + geomType, + errorMsg, mayTryWithGMLAS ); } else { errorMsg = tr( "Cannot find element '%1'" ).arg( unprefixedTypename ); + mayTryWithGMLAS = true; } return false; } @@ -1772,27 +2248,18 @@ bool QgsWFSProvider::readAttributesFromSchema( QDomDocument &schemaDoc, propertyType = propertyType.at( 0 ).toUpper() + propertyType.mid( 1 ); geomType = geomTypeFromPropertyType( geometryAttribute, propertyType ); } - else if ( !name.isEmpty() ) //todo: distinguish between numerical and non-numerical types + else if ( !name.isEmpty() ) { - QVariant::Type attributeType = QVariant::String; //string is default type - if ( type.contains( QLatin1String( "double" ), Qt::CaseInsensitive ) || type.contains( QLatin1String( "float" ), Qt::CaseInsensitive ) || type.contains( QLatin1String( "decimal" ), Qt::CaseInsensitive ) ) + const QVariant::Type attributeType = getVariantTypeFromXML( type ); + if ( attributeType != QVariant::Invalid ) { - attributeType = QVariant::Double; + fields.append( QgsField( name, attributeType, type ) ); } - else if ( type.contains( QLatin1String( "int" ), Qt::CaseInsensitive ) || - type.contains( QLatin1String( "short" ), Qt::CaseInsensitive ) ) + else { - attributeType = QVariant::Int; + mayTryWithGMLAS = true; + fields.append( QgsField( name, QVariant::String, type ) ); } - else if ( type.contains( QLatin1String( "long" ), Qt::CaseInsensitive ) ) - { - attributeType = QVariant::LongLong; - } - else if ( type.contains( QLatin1String( "dateTime" ), Qt::CaseInsensitive ) ) - { - attributeType = QVariant::DateTime; - } - fields.append( QgsField( name, attributeType, type ) ); } } if ( !foundGeometryAttribute ) diff --git a/src/providers/wfs/qgswfsprovider.h b/src/providers/wfs/qgswfsprovider.h index acc22442f24..eb698527ca4 100644 --- a/src/providers/wfs/qgswfsprovider.h +++ b/src/providers/wfs/qgswfsprovider.h @@ -138,6 +138,9 @@ class QgsWFSProvider final: public QgsVectorDataProvider //! Perform an initial GetFeature request with a 1-feature limit. void issueInitialGetFeature(); + //! Return whether metadata retrieval has been canceled (typically download of the schema) + bool metadataRetrievalCanceled() const { return mMetadataRetrievalCanceled; } + private slots: void featureReceivedAnalyzeOneFeature( QVector ); @@ -171,11 +174,24 @@ class QgsWFSProvider final: public QgsVectorDataProvider QDomElement geometryElement( const QgsGeometry &geometry, QDomDocument &transactionDoc ); //! Set mShared->mLayerPropertiesList from describeFeatureDocument - bool setLayerPropertiesListFromDescribeFeature( QDomDocument &describeFeatureDocument, const QStringList &typenameList, QString &errorMsg ); + bool setLayerPropertiesListFromDescribeFeature( QDomDocument &describeFeatureDocument, const QByteArray &response, const QStringList &typenameList, QString &errorMsg ); //! backup of mShared->mLayerPropertiesList on the feature type when there is no sql request QList< QgsOgcUtils::LayerProperties > mLayerPropertiesListWhenNoSqlRequest; + //! Set if metadata retrieval has been canceled (typically download of the schema) + bool mMetadataRetrievalCanceled = false; + + bool readAttributesFromSchemaWithoutGMLAS( QDomDocument &schemaDoc, + const QString &prefixedTypename, + QString &geometryAttribute, + QgsFields &fields, Qgis::WkbType &geomType, QString &errorMsg, bool &mayTryWithGMLAS ); + + bool readAttributesFromSchemaWithGMLAS( const QByteArray &response, + const QString &prefixedTypename, + QString &geometryAttribute, + QgsFields &fields, Qgis::WkbType &geomType, QString &errorMsg ); + protected: //! String used to define a subset of the layer @@ -206,6 +222,8 @@ class QgsWFSProvider final: public QgsVectorDataProvider * thematic attributes and their types from a dom document. Returns true in case of success. */ bool readAttributesFromSchema( QDomDocument &schemaDoc, + const QByteArray &response, + bool singleLayerContext, const QString &prefixedTypename, QString &geometryAttribute, QgsFields &fields, Qgis::WkbType &geomType, QString &errorMsg ); diff --git a/src/providers/wfs/qgswfsprovidermetadata.cpp b/src/providers/wfs/qgswfsprovidermetadata.cpp index 8edef0c520f..a4f79bc03cc 100644 --- a/src/providers/wfs/qgswfsprovidermetadata.cpp +++ b/src/providers/wfs/qgswfsprovidermetadata.cpp @@ -236,6 +236,8 @@ QList QgsWfsProviderMetadata::querySublayers( const QgsWFSProvider provider( uri + " " + QgsWFSConstants::URI_PARAM_SKIP_INITIAL_GET_FEATURE + "='true'", QgsDataProvider::ProviderOptions(), caps ); + if ( provider.metadataRetrievalCanceled() ) + return res; QgsProviderSublayerDetails details; details.setType( Qgis::LayerType::Vector ); details.setProviderKey( QgsWFSProvider::WFS_PROVIDER_KEY ); diff --git a/src/providers/wfs/qgswfsshareddata.cpp b/src/providers/wfs/qgswfsshareddata.cpp index 0cce64716cd..2c332b05642 100644 --- a/src/providers/wfs/qgswfsshareddata.cpp +++ b/src/providers/wfs/qgswfsshareddata.cpp @@ -54,6 +54,8 @@ QgsWFSSharedData *QgsWFSSharedData::clone() const copy->mGeometryAttribute = mGeometryAttribute; copy->mLayerPropertiesList = mLayerPropertiesList; copy->mMapFieldNameToSrcLayerNameFieldName = mMapFieldNameToSrcLayerNameFieldName; + copy->mFieldNameToXPathAndIsNestedContentMap = mFieldNameToXPathAndIsNestedContentMap; + copy->mNamespacePrefixToURIMap = mNamespacePrefixToURIMap; copy->mPageSize = mPageSize; copy->mCaps = mCaps; copy->mHasWarnedAboutMissingFeatureId = mHasWarnedAboutMissingFeatureId; @@ -105,8 +107,23 @@ QString QgsWFSSharedData::computedExpression( const QgsExpression &expression ) bool honourAxisOrientation = false; getVersionValues( gmlVersion, filterVersion, honourAxisOrientation ); + QMap fieldNameToXPathMap; + if ( !mFieldNameToXPathAndIsNestedContentMap.isEmpty() ) + { + for ( auto iterFieldName = mFieldNameToXPathAndIsNestedContentMap.constBegin(); iterFieldName != mFieldNameToXPathAndIsNestedContentMap.constEnd(); ++iterFieldName ) + { + const QString &fieldName = iterFieldName.key(); + const auto &value = iterFieldName.value(); + fieldNameToXPathMap[fieldName] = value.first; + } + } + QDomDocument expressionDoc; - QDomElement expressionElem = QgsOgcUtils::expressionToOgcExpression( expression, expressionDoc, gmlVersion, filterVersion, mGeometryAttribute, srsName(), honourAxisOrientation, mURI.invertAxisOrientation(), nullptr, true ); + QDomElement expressionElem = QgsOgcUtils::expressionToOgcExpression( + expression, expressionDoc, gmlVersion, filterVersion, mGeometryAttribute, + srsName(), honourAxisOrientation, mURI.invertAxisOrientation(), nullptr, + true, + fieldNameToXPathMap, mNamespacePrefixToURIMap ); if ( !expressionElem.isNull() ) { @@ -155,12 +172,23 @@ bool QgsWFSSharedData::computeFilter( QString &errorMsg ) } } + QMap fieldNameToXPathMap; + if ( !mFieldNameToXPathAndIsNestedContentMap.isEmpty() ) + { + for ( auto iterFieldName = mFieldNameToXPathAndIsNestedContentMap.constBegin(); iterFieldName != mFieldNameToXPathAndIsNestedContentMap.constEnd(); ++iterFieldName ) + { + const QString &fieldName = iterFieldName.key(); + const auto &value = iterFieldName.value(); + fieldNameToXPathMap[fieldName] = value.first; + } + } + QDomDocument filterDoc; const QDomElement filterElem = QgsOgcUtils::SQLStatementToOgcFilter( sql, filterDoc, gmlVersion, filterVersion, mLayerPropertiesList, honourAxisOrientation, mURI.invertAxisOrientation(), mCaps.mapUnprefixedTypenameToPrefixedTypename, - &errorMsg ); + &errorMsg, fieldNameToXPathMap, mNamespacePrefixToURIMap ); if ( !errorMsg.isEmpty() ) { errorMsg = tr( "SQL statement to OGC Filter error: " ) + errorMsg; @@ -188,13 +216,24 @@ bool QgsWFSSharedData::computeFilter( QString &errorMsg ) //if not, if must be a QGIS expression const QgsExpression filterExpression( filter ); + QMap fieldNameToXPathMap; + if ( !mFieldNameToXPathAndIsNestedContentMap.isEmpty() ) + { + for ( auto iterFieldName = mFieldNameToXPathAndIsNestedContentMap.constBegin(); iterFieldName != mFieldNameToXPathAndIsNestedContentMap.constEnd(); ++iterFieldName ) + { + const QString &fieldName = iterFieldName.key(); + const auto &value = iterFieldName.value(); + fieldNameToXPathMap[fieldName] = value.first; + } + } + const QDomElement filterElem = QgsOgcUtils::expressionToOgcFilter( filterExpression, filterDoc, gmlVersion, filterVersion, mLayerPropertiesList.size() == 1 ? mLayerPropertiesList[0].mNamespacePrefix : QString(), mLayerPropertiesList.size() == 1 ? mLayerPropertiesList[0].mNamespaceURI : QString(), mGeometryAttribute, srsName(), honourAxisOrientation, mURI.invertAxisOrientation(), - &errorMsg ); + &errorMsg, fieldNameToXPathMap, mNamespacePrefixToURIMap ); if ( !errorMsg.isEmpty() ) { @@ -261,11 +300,16 @@ QgsGmlStreamingParser *QgsWFSSharedData::createParser() const } else { - return new QgsGmlStreamingParser( mURI.typeName(), - mGeometryAttribute, - mFields, - axisOrientationLogic, - mURI.invertAxisOrientation() ); + auto parser = new QgsGmlStreamingParser( mURI.typeName(), + mGeometryAttribute, + mFields, + axisOrientationLogic, + mURI.invertAxisOrientation() ); + if ( !mFieldNameToXPathAndIsNestedContentMap.isEmpty() ) + { + parser->setFieldsXPath( mFieldNameToXPathAndIsNestedContentMap, mNamespacePrefixToURIMap ); + } + return parser; } } @@ -329,6 +373,29 @@ QString QgsWFSSharedData::combineWFSFilters( const std::vector &filters } envelopeFilterDoc.firstChildElement().appendChild( andElem ); + QSet setNamespaceURI; + for ( auto iterFieldName = mFieldNameToXPathAndIsNestedContentMap.constBegin(); iterFieldName != mFieldNameToXPathAndIsNestedContentMap.constEnd(); ++iterFieldName ) + { + const auto &value = iterFieldName.value(); + const QStringList parts = value.first.split( '/' ); + for ( const QString &part : parts ) + { + const QStringList subparts = part.split( ':' ); + if ( subparts.size() == 2 && subparts[0] != QLatin1String( "gml" ) ) + { + const auto iter = mNamespacePrefixToURIMap.constFind( subparts[0] ); + if ( iter != mNamespacePrefixToURIMap.constEnd() && + !setNamespaceURI.contains( *iter ) ) + { + setNamespaceURI.insert( *iter ); + QDomAttr attr = envelopeFilterDoc.createAttribute( QStringLiteral( "xmlns:" ) + subparts[0] ); + attr.setValue( *iter ); + envelopeFilterDoc.firstChildElement().setAttributeNode( attr ); + } + } + } + } + if ( mLayerPropertiesList.size() == 1 && envelopeFilterDoc.firstChildElement().hasAttribute( QStringLiteral( "xmlns:" ) + mLayerPropertiesList[0].mNamespacePrefix ) ) { @@ -337,7 +404,6 @@ QString QgsWFSSharedData::combineWFSFilters( const std::vector &filters else { // add xmls:PREFIX=URI attributes to top element - QSet setNamespaceURI; for ( const QgsOgcUtils::LayerProperties &props : std::as_const( mLayerPropertiesList ) ) { if ( !props.mNamespacePrefix.isEmpty() && !props.mNamespaceURI.isEmpty() && diff --git a/src/providers/wfs/qgswfsshareddata.h b/src/providers/wfs/qgswfsshareddata.h index df5082ac665..654e20684c5 100644 --- a/src/providers/wfs/qgswfsshareddata.h +++ b/src/providers/wfs/qgswfsshareddata.h @@ -97,6 +97,12 @@ class QgsWFSSharedData : public QObject, public QgsBackgroundCachedSharedData //! Map a field name to the pair (typename, fieldname) that describes its source field QMap< QString, QPair > mMapFieldNameToSrcLayerNameFieldName; + //! Map a field name to the pair (xpath, isNestedContent) + QMap > mFieldNameToXPathAndIsNestedContentMap; + + //! Map a namespace prefix to its URI + QMap mNamespacePrefixToURIMap; + //! Page size for WFS 2.0. 0 = disabled long long mPageSize = 0; diff --git a/tests/src/python/test_provider_wfs.py b/tests/src/python/test_provider_wfs.py index bd72c415010..4113f51ddf8 100644 --- a/tests/src/python/test_provider_wfs.py +++ b/tests/src/python/test_provider_wfs.py @@ -51,11 +51,18 @@ from qgis.PyQt.QtCore import ( QObject, Qt, QTime, + QVariant, ) + import unittest from qgis.testing import start_app, QgisTestCase from utilities import compareWkt, unitTestDataPath +from osgeo import gdal + +# Default value is 2 second, which is too short when run under Valgrind +gdal.SetConfigOption('OGR_GMLAS_XERCES_MAX_TIME', '20') + TEST_DATA_DIR = unitTestDataPath() @@ -6427,6 +6434,53 @@ Can't recognize service requested. self.assertEqual(sublayers[3].featureCount(), -1) self.assertEqual(sublayers[4].featureCount(), -1) + @unittest.skipIf(gdal.GetDriverByName("GMLAS") is None, "OGR GMLAS driver required") + def testWFSComplexFeatures(self): + """Test reading complex features""" + + endpoint = self.__class__.basetestpath + '/fake_qgis_http_endpoint_WFS_complex_features' + + shutil.copy(os.path.join(TEST_DATA_DIR, 'provider', 'wfs', 'inspire_complexfeatures', 'getcapabilities.xml'), sanitize(endpoint, '?SERVICE=WFS?REQUEST=GetCapabilities&VERSION=2.0.0')) + shutil.copy(os.path.join(TEST_DATA_DIR, 'provider', 'wfs', 'inspire_complexfeatures', 'describefeaturetype.xml'), sanitize(endpoint, '?SERVICE=WFS&REQUEST=DescribeFeatureType&VERSION=2.0.0&TYPENAMES=ps:ProtectedSite&TYPENAME=ps:ProtectedSite')) + shutil.copy(os.path.join(TEST_DATA_DIR, 'provider', 'wfs', 'inspire_complexfeatures', 'getfeature_hits.xml'), sanitize(endpoint, '?SERVICE=WFS&REQUEST=GetFeature&VERSION=2.0.0&TYPENAMES=ps:ProtectedSite&RESULTTYPE=hits')) + shutil.copy(os.path.join(TEST_DATA_DIR, 'provider', 'wfs', 'inspire_complexfeatures', 'getfeature.xml'), sanitize(endpoint, '?SERVICE=WFS&REQUEST=GetFeature&VERSION=2.0.0&TYPENAMES=ps:ProtectedSite&SRSNAME=urn:ogc:def:crs:EPSG::25833')) + + vl = QgsVectorLayer("url='http://" + endpoint + "' typename='ps:ProtectedSite' version='2.0.0' skipInitialGetFeature='true'", 'test', 'WFS') + self.assertTrue(vl.isValid()) + self.assertEqual(vl.featureCount(), 1228) + self.assertEqual(len(vl.fields()), 26) + self.assertEqual([field.name() for field in vl.fields()], ['id', 'metadataproperty', 'description_href', 'description_title', 'description_nilreason', 'description', 'descriptionreference_href', 'descriptionreference_title', 'descriptionreference_nilreason', 'identifier_codespace', 'identifier', 'name', 'location_location', 'inspireid_identifier_localid', 'inspireid_identifier_namespace', 'inspireid_identifier_versionid_nilreason', 'inspireid_identifier_versionid_nil', 'inspireid_identifier_versionid', 'legalfoundationdate_nilreason', 'legalfoundationdate', 'legalfoundationdocument_nilreason', 'legalfoundationdocument_owns', 'legalfoundationdocument_ci_citation', 'sitedesignation', 'sitename', 'siteprotectionclassification']) + self.assertEqual(vl.fields()["sitedesignation"].type(), QVariant.String) + + got_f = [f for f in vl.getFeatures()] + self.assertEqual(len(got_f), 1) + geom = got_f[0].geometry() + self.assertFalse(geom.isNull()) + self.assertEqual(got_f[0]["id"], 'ProtectedSite_FFH_553_DE4546-303') + self.assertEqual(got_f[0]["sitedesignation"], '{"ps:DesignationType":{"ps:designation":{"@xlink:href":"http://inspire.ec.europa.eu/codelist/Natura2000DesignationValue/specialAreaOfConservation"},"ps:designationScheme":{"@xlink:href":"http://inspire.ec.europa.eu/codelist/DesignationSchemeValue/natura2000"}}}') + + vl = QgsVectorLayer("url='http://" + endpoint + "' typename='ps:ProtectedSite' version='2.0.0' skipInitialGetFeature='true'", 'test', 'WFS') + vl.setSubsetString("inspireid_identifier_localid = 'ProtectedSite_FFH_553_DE4546'") + + if int(QT_VERSION_STR.split('.')[0]) >= 6: + attrs = 'xmlns:base="http://inspire.ec.europa.eu/schemas/base/3.3" xmlns:ps="http://inspire.ec.europa.eu/schemas/ps/4.0"' + else: + attrs = 'xmlns:ps="http://inspire.ec.europa.eu/schemas/ps/4.0" xmlns:base="http://inspire.ec.europa.eu/schemas/base/3.3"' + with open(sanitize(endpoint, + f"""?SERVICE=WFS&REQUEST=GetFeature&VERSION=2.0.0&TYPENAMES=ps:ProtectedSite&SRSNAME=urn:ogc:def:crs:EPSG::25833&FILTER= + + ps:inspireID/base:Identifier/base:localId + ProtectedSite_FFH_553_DE4546 + + +"""), + 'wb') as f: + with open(os.path.join(TEST_DATA_DIR, 'provider', 'wfs', 'inspire_complexfeatures', 'getfeature.xml'), "rb") as f_source: + f.write(f_source.read()) + + got_f = [f for f in vl.getFeatures()] + self.assertEqual(len(got_f), 1) + if __name__ == '__main__': unittest.main() diff --git a/tests/testdata/provider/wfs/inspire_complexfeatures/describefeaturetype.xml b/tests/testdata/provider/wfs/inspire_complexfeatures/describefeaturetype.xml new file mode 100644 index 00000000000..65b9794b8d5 --- /dev/null +++ b/tests/testdata/provider/wfs/inspire_complexfeatures/describefeaturetype.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/tests/testdata/provider/wfs/inspire_complexfeatures/getcapabilities.xml b/tests/testdata/provider/wfs/inspire_complexfeatures/getcapabilities.xml new file mode 100644 index 00000000000..56635efeb0a --- /dev/null +++ b/tests/testdata/provider/wfs/inspire_complexfeatures/getcapabilities.xml @@ -0,0 +1,652 @@ + + + + INSPIRE Download Service: Protected Sites / Schutzgebiete des Landes Brandenburg (WFS-PS-SCHUTZG) + Der interoperable INSPIRE-Downloaddienst (WFS) Protected Sites (Schutzgebiete des Landes Brandenburg) beinhaltet Informationen zu den Schutzgebieten nach Naturschutzrecht des Landes Brandenburg und Europäische Schutzgebiete. Zu den Schutzgebieten nach Naturschutzrecht des Landes Brandenburg zählen Naturschutzgebiete, Landschaftsschutzgebiete, Biosphärenreservate, Naturparks und Nationalparke. Zum europäischen Schutzgebietssystem Natura 2000 zählen Vogelschutzgebiete (Special Protection Area (SPA)) und Fauna-Flora-Habitat-Gebiete (FFH-Gebiete). Gemäß der INSPIRE-Datenspezifikation Protected Sites (D2.8.I.9_v3.2) liegen die Inhalte der Schutzgebiete INSPIRE-konform vor. Der WFS beinhaltet den FeatureType ProtectedSite. --- The compliant INSPIRE download service (WFS) Protected Sites (Schutzgebiete des Landes Brandenburg) provides information on protected sites in the state of Brandenburg that are legally protected by state environmental law or according to the European Natura 2000 protected areas network. Areas legally protected by state environmental law include nature reserves, protected landscapes, biosphere reserves, nature parks and national parks. Areas according to the European Natura 2000 network include Special Protection Areas (SPA) for birds and Special Areas of Conservation (SAC) defined by the European Union's Habitats Directive (92/43/EEC). The content of the protected areas is compliant to the INSPIRE data specification for the annex theme Protected Sites (D2.8.I.9_v3.2). The WFS consists of the FeatureType ProtectedSite. + + Schutzgebiet + Naturschutz + Natur + Umwelt + Biosphärenreservat + Landschaft + Landschaftsschutzgebiet + Nationalpark + Naturpark + Naturschutzgebiet + Naturschutzrecht + Vogelschutzgebiet + Schutzgebiete + Großschutzgebiet + ProtectedSite + Brandenburg + WFS + interoperabel + interoperability + inspireidentifiziert + + WFS + 2.0.0 + Nutzung erfolgt derzeit kostenfrei unter Beachtung des Urheberrechts. + Es gelten die Bedingungen der Datenlizenz Deutschland – Namensnennung – Version 2.0: https://www.govdata.de/dl-de/by-2-0. Als Bezeichnung des Bereitstellers ist „© Landesamt für Umwelt Brandenburg“ anzugeben. + + + Landesvermessung und Geobasisinformation Brandenburg (LGB) + + + INSPIRE-Zentrale im Land Brandenburg + Kundenservice + + + +49-331-8844-123 + +49-331-8844-16123 + + + Heinrich-Mann-Allee 104 B + Potsdam + Brandenburg + 14473 + Deutschland + kundenservice@inspire.brandenburg.de + + + 24/7 + + + ServiceCenter + + + + + + ps:ProtectedSite + ProtectedSite + Naturschutzgebiete (NSG), Landschaftsschutzgebiete (LSG), Nationalparke (NatP), Naturparke (NP), Biospärenreservate (BR), Fauna-Flora-Habitat-Gebiete (FFH) und Vogelschutzgebiete (SPA, Special Protection Area) des Landes Brandenburg + http://www.opengis.net/def/crs/EPSG/0/25833 + http://www.opengis.net/def/crs/EPSG/0/25832 + http://www.opengis.net/def/crs/EPSG/0/4326 + http://www.opengis.net/def/crs/EPSG/0/4258 + http://www.opengis.net/def/crs/EPSG/0/3034 + http://www.opengis.net/def/crs/EPSG/0/3035 + http://www.opengis.net/def/crs/EPSG/0/3044 + http://www.opengis.net/def/crs/EPSG/0/3045 + http://www.opengis.net/def/crs/EPSG/0/3857 + http://www.opengis.net/def/crs/EPSG/0/4839 + + application/gml+xml; version=3.2 + text/xml; subtype=gml/3.2.1 + + + 11.071522 51.292131 + 14.777398 53.617170 + + + + + + + + + TRUE + + + + TRUE + + + + TRUE + + + + TRUE + + + + TRUE + + + + TRUE + + + + TRUE + + + + TRUE + + + + TRUE + + + + TRUE + + + + FALSE + + + + TRUE + + + + FALSE + + + + TRUE + + + + FALSE + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + xsd:double + + + gml:_Geometry + + + + + xsd:double + + + gml32:AbstractGeometry + + + + + gml:Point + + + gml:_Geometry + + + + + gml32:Point + + + gml32:AbstractGeometry + + + + + xsd:anyType + + + xsd:string + + + xsd:anyType + + + + + xsd:anyType + + + xsd:string + + + + + gml:_Geometry + + + xsd:string + + + xsd:string + + + + + gml32:AbstractGeometry + + + xsd:string + + + xsd:string + + + + + xsd:double + + + xsd:double + + + xsd:double + + + xsd:double + + + + + xsd:integer + + + xsd:integer + + + xsd:integer + + + + + xsd:integer + + + xsd:integer + + + xsd:integer + + + + + gml:Point + + + gml:_Geometry + + + + + gml32:Point + + + gml32:AbstractGeometry + + + + + xsd:boolean + + + gml:_Geometry + + + + + xsd:boolean + + + gml32:AbstractGeometry + + + + + xsd:boolean + + + gml:_Geometry + + + + + xsd:boolean + + + gml32:AbstractGeometry + + + + + xsd:boolean + + + gml:_Geometry + + + + + xsd:boolean + + + gml32:AbstractGeometry + + + + + xsd:double + + + gml:_Geometry + + + + + xsd:double + + + gml32:AbstractGeometry + + + + + xsd:integer + + + xsd:double + + + + + xsd:double + + + gml:_Geometry + + + + + xsd:double + + + gml32:AbstractGeometry + + + + + xsd:integer + + + xsd:double + + + + + + diff --git a/tests/testdata/provider/wfs/inspire_complexfeatures/getfeature.xml b/tests/testdata/provider/wfs/inspire_complexfeatures/getfeature.xml new file mode 100644 index 00000000000..63c0894220e --- /dev/null +++ b/tests/testdata/provider/wfs/inspire_complexfeatures/getfeature.xml @@ -0,0 +1,59 @@ + + + + + + https://registry.gdi-de.org/id/de.bb.inspire.ps.schutzg/ProtectedSite_FFH_553_DE4546-303 + + + + + + 393417.723 5703577.814 393373.225 5703627.160 393355.567 5703647.893 393059.411 5703439.935 392935.194 5703238.164 392910.426 5703197.931 392748.503 5703042.996 392607.511 5702922.719 392568.349 5702894.811 392510.694 5702830.645 392494.856 5702773.768 392479.853 5702712.855 392464.829 5702651.443 392419.821 5702468.704 392405.314 5702444.287 392374.601 5702415.032 392317.941 5702387.341 392275.050 5702366.089 392244.501 5702340.829 392230.178 5702320.907 392212.278 5702299.130 392194.242 5702261.852 392177.115 5702173.514 392168.678 5702126.338 392170.581 5702111.753 392173.880 5702094.610 392168.231 5702078.834 392154.072 5702062.907 392138.272 5702043.546 392120.281 5702031.777 392096.319 5702020.754 392051.341 5702009.592 391982.372 5701998.913 391908.470 5701989.937 391815.141 5701970.752 391763.622 5701970.863 391732.955 5701967.118 391702.766 5701962.853 391675.512 5701956.967 391636.232 5701950.573 391594.038 5701946.300 391567.222 5701938.895 391522.887 5701931.208 391461.032 5701923.239 391411.142 5701914.279 391377.895 5701908.639 391340.695 5701904.161 391303.514 5701900.182 391272.601 5701890.444 391251.434 5701898.816 391230.366 5701885.172 391188.154 5701843.883 391165.473 5701815.299 391150.049 5701792.921 391126.053 5701768.893 391086.318 5701727.001 391041.242 5701676.825 390984.667 5701614.614 390944.993 5701574.221 390904.301 5701533.369 390872.966 5701501.138 390840.837 5701473.942 390827.136 5701456.995 390787.463 5701416.602 390740.978 5701356.478 390722.189 5701325.234 390712.013 5701296.637 390691.381 5700793.249 390735.499 5700661.380 390804.628 5700407.431 390815.234 5700385.486 390821.415 5700353.218 390821.570 5700308.191 390823.253 5700251.595 390826.024 5700184.951 390750.464 5700196.552 390739.130 5700127.484 390737.877 5700072.510 390741.111 5700029.357 390736.727 5699959.004 390727.022 5699880.865 390720.018 5699868.647 390718.180 5699848.213 390731.148 5699810.664 390754.653 5699761.678 390768.267 5699752.116 390819.267 5699690.499 390838.363 5699668.206 390845.788 5699641.890 390849.329 5699606.228 390852.408 5699547.075 390853.845 5699484.487 390856.887 5699387.818 390863.167 5699211.479 390869.103 5699112.190 390875.285 5698957.865 390881.127 5698905.102 390882.965 5698803.479 390882.585 5698720.956 390887.277 5698676.744 390876.847 5698654.161 390861.219 5698626.788 390849.209 5698602.269 390826.233 5698578.699 390800.515 5698549.238 390780.678 5698529.041 390747.887 5698497.870 390692.925 5698450.599 390652.351 5698449.260 390627.075 5698442.792 390576.722 5698410.339 390548.720 5698373.969 390517.660 5698336.224 390486.051 5698309.506 390436.492 5698272.018 390388.019 5698260.998 390366.156 5698252.389 390357.233 5698217.739 390361.363 5698172.048 390359.759 5698145.102 390348.556 5698128.052 390317.794 5698109.804 390304.262 5698060.335 390292.449 5697991.787 390292.708 5697949.256 390280.915 5697881.207 390369.881 5697891.569 390416.611 5697896.658 390448.339 5697901.861 390454.806 5697937.613 390471.224 5697996.468 390486.491 5698063.874 390502.971 5698124.227 390514.718 5698166.765 390527.903 5698207.745 390553.805 5698241.700 390606.736 5698276.048 390674.093 5698332.816 390816.316 5698446.544 390900.389 5698520.635 390921.707 5698564.782 390956.902 5698581.348 391089.748 5698685.956 391280.702 5698829.201 391600.508 5699077.217 391631.830 5699096.943 391654.703 5699118.016 391718.339 5699169.433 391862.834 5699277.564 391895.920 5699303.721 392014.517 5699402.408 391999.969 5699315.965 391990.207 5699199.811 391960.425 5698997.939 391907.753 5698860.033 391946.996 5698889.940 391967.442 5698900.607 391990.230 5698895.171 391997.615 5698867.856 391998.331 5698824.307 392000.996 5698791.683 392028.283 5698749.546 392069.495 5698729.849 392090.404 5698702.981 392100.584 5698695.060 392142.807 5698748.855 392140.645 5698805.970 392140.198 5698880.522 392145.511 5698961.341 392167.475 5699094.503 392185.223 5699185.817 392182.493 5699253.460 392172.030 5699278.900 392171.268 5699296.940 392167.768 5699333.600 392165.509 5699351.701 392160.261 5699418.946 392149.172 5699477.927 392135.786 5699517.494 392126.293 5699554.399 392128.553 5699597.326 392126.559 5699621.919 392130.071 5699658.792 392134.680 5699673.610 392140.124 5699684.392 392146.313 5699701.146 392150.149 5699721.498 392158.213 5699735.174 392168.868 5699763.251 392175.924 5699788.973 392180.137 5699806.309 392182.638 5699830.717 392190.685 5699868.405 392150.042 5699877.574 392161.360 5699909.625 392162.932 5699923.567 392162.936 5699948.078 392161.777 5699968.635 392162.752 5700004.611 392165.355 5700031.517 392178.323 5700054.997 392177.704 5700076.532 392182.531 5700108.849 392187.242 5700126.164 392188.601 5700147.118 392187.381 5700166.176 392181.716 5700174.412 392180.271 5700187.977 392184.446 5700228.825 392194.008 5700242.440 392205.781 5700248.960 392218.695 5700258.936 392231.041 5700279.439 392238.617 5700305.641 392233.149 5700330.876 392221.982 5700351.343 392228.437 5700496.645 392273.882 5700494.783 392325.159 5700513.191 392313.092 5700572.712 392334.152 5700598.361 392345.671 5700610.895 392350.821 5700626.691 392373.461 5700654.276 392393.502 5700679.466 392412.422 5700701.702 392422.913 5700725.783 392433.863 5700748.845 392441.931 5700787.032 392453.269 5700819.582 392475.472 5700848.685 392492.946 5700884.486 392526.127 5700925.145 392563.557 5700959.627 392596.807 5700989.779 392630.475 5701017.912 392667.659 5701046.402 392714.274 5701097.515 392728.159 5701118.956 392742.093 5701129.390 392759.330 5701147.192 392767.762 5701169.857 392776.002 5701200.033 392785.188 5701216.664 392789.216 5701229.505 392783.645 5701252.243 392785.536 5701286.181 392785.729 5701339.698 392788.226 5701400.623 392786.679 5701472.719 392788.947 5701503.640 392792.222 5701522.515 392792.054 5701555.036 392823.458 5701576.760 392866.308 5701597.013 392812.399 5701624.234 392787.024 5701639.781 392775.632 5701654.754 392764.138 5701667.231 392752.595 5701690.715 392747.880 5701709.917 392748.281 5701731.910 392738.351 5701770.334 392724.954 5701797.396 392725.629 5701813.875 392643.125 5701973.328 392657.698 5702023.754 392683.166 5702022.710 392653.572 5702093.955 392601.432 5702152.118 392579.627 5702181.525 392486.501 5702386.934 392483.366 5702408.072 392487.652 5702439.411 392530.904 5702603.713 392566.395 5702737.320 392575.572 5702765.957 392596.195 5702793.124 392634.501 5702824.569 392690.482 5702872.296 392750.471 5702932.365 392786.304 5702927.895 392883.874 5703013.936 392978.010 5703101.618 393027.168 5703178.138 393128.389 5703359.122 393129.729 5703361.518 393350.448 5703523.046 393417.723 5703577.814 + + + + + 392388.973 5701374.469 392385.166 5701403.638 392375.162 5701428.059 392318.559 5701438.383 392280.880 5701434.425 392191.051 5701439.607 392134.195 5701455.944 392090.916 5701486.231 392075.042 5701513.894 392058.464 5701536.584 392033.417 5701560.121 392016.503 5701586.826 392008.386 5701620.674 392003.245 5701653.900 391992.852 5701668.832 391971.898 5701670.192 391951.211 5701678.043 391933.683 5701689.766 391931.006 5701709.885 391922.091 5701724.257 391912.218 5701739.669 391903.241 5701752.542 391891.931 5701769.513 391888.694 5701788.155 391885.227 5701837.819 391883.217 5701886.424 391886.463 5701916.805 391991.716 5701934.000 392072.855 5701948.683 392106.498 5701951.806 392144.497 5701975.760 392165.545 5701988.903 392213.188 5702028.469 392222.374 5702045.101 392232.583 5702086.701 392238.762 5702139.523 392241.945 5702167.448 392245.449 5702200.200 392248.220 5702216.688 392252.308 5702236.463 392264.151 5702265.280 392276.620 5702284.172 392287.511 5702299.272 392301.460 5702314.836 392313.047 5702325.095 392332.774 5702335.490 392372.413 5702351.193 392400.320 5702364.522 392426.175 5702376.610 392440.002 5702383.276 392451.108 5702358.705 392469.537 5702311.906 392488.528 5702268.914 392507.985 5702222.863 392525.997 5702182.682 392543.296 5702146.070 392555.948 5702123.003 392563.452 5702095.433 392570.086 5702041.684 392576.321 5701995.824 392563.979 5701873.025 392556.840 5701784.278 392486.463 5701763.652 392493.417 5701725.850 392506.155 5701658.297 392511.152 5701560.548 392512.470 5701458.447 392521.127 5701425.578 392514.144 5701413.858 392443.247 5701392.753 392388.973 5701374.469 + + + + + 390948.449 5698619.212 390958.133 5698696.851 390950.855 5698751.174 390945.709 5698820.918 390944.312 5698884.504 390943.718 5698931.050 390943.706 5698979.573 390940.472 5699022.725 390937.794 5699103.872 390931.829 5699214.668 390929.692 5699296.794 390925.463 5699364.498 390924.579 5699440.569 390929.724 5699492.883 390931.635 5699527.320 390915.577 5699611.517 390912.187 5699638.669 390947.034 5699646.745 390941.942 5699668.964 390945.094 5699684.842 390959.776 5699725.760 390967.366 5699764.467 390956.985 5699791.905 390929.849 5699825.532 390885.236 5699859.875 390822.684 5699895.954 390807.633 5699907.076 390821.927 5699938.505 390854.023 5700013.725 390864.240 5700043.320 390878.338 5700179.806 390869.717 5700433.276 390851.235 5700531.579 390840.113 5700577.556 390820.494 5700635.886 390792.753 5700764.583 390780.215 5700812.618 390785.156 5700859.938 390786.376 5700901.908 390784.415 5700939.505 390788.812 5700961.335 390788.063 5700991.880 390786.233 5701020.468 390789.655 5701067.350 390791.915 5701110.277 390794.543 5701162.194 390794.257 5701216.230 390797.941 5701245.093 390799.402 5701268.544 390804.134 5701286.358 390819.976 5701306.719 390855.093 5701345.798 390876.387 5701364.935 390888.884 5701376.928 390905.602 5701394.251 390935.623 5701431.038 390956.917 5701450.175 390977.691 5701468.832 391166.102 5701684.213 391220.262 5701748.523 391251.221 5701783.771 391279.608 5701805.118 391300.382 5701823.775 391326.781 5701833.198 391796.794 5701901.472 391797.420 5701867.931 391833.216 5701825.945 391835.332 5701804.348 391835.205 5701776.841 391832.074 5701761.462 391839.724 5701740.639 391855.815 5701730.475 391876.593 5701712.616 391888.067 5701699.640 391900.539 5701686.623 391902.860 5701670.020 391926.450 5701647.543 391942.144 5701639.897 391953.658 5701627.920 391967.772 5701618.337 391976.810 5701606.961 391983.789 5701594.170 391988.709 5701579.962 391993.588 5701564.755 392000.842 5701546.449 391995.459 5701537.165 391987.743 5701531.979 392000.379 5701522.957 392026.691 5701505.872 392049.782 5701483.415 392105.713 5701420.095 392123.842 5701410.848 392142.144 5701393.590 392213.130 5701343.659 392256.250 5701333.888 392297.802 5701334.686 392326.623 5701330.003 392350.348 5701323.028 392363.062 5701291.492 392368.653 5701269.253 392353.168 5701245.377 392328.579 5701206.867 392318.800 5701175.753 392330.691 5701160.759 392347.932 5701142.044 392340.048 5701108.352 392308.779 5700980.074 392295.520 5700925.092 392269.167 5700880.152 392231.680 5700807.654 392198.994 5700681.436 392180.128 5700611.677 392146.345 5700568.541 392137.393 5700545.397 392129.783 5700506.191 392197.974 5700497.894 392190.144 5700477.705 392184.610 5700415.904 392173.271 5700322.326 392165.064 5700244.126 392158.789 5700200.863 392154.470 5700156.520 392148.171 5700088.247 392124.655 5699941.643 392136.914 5699935.638 392137.471 5699912.605 392132.445 5699899.805 392123.660 5699905.167 392118.613 5699891.868 392096.313 5699762.723 392079.538 5699707.384 392070.054 5699671.256 392041.434 5699534.366 392019.465 5699437.722 391677.237 5699179.621 391642.566 5699188.045 391638.874 5699171.188 391650.348 5699158.213 391632.837 5699145.924 390948.449 5698619.212 + + + + + + + ProtectedSite_FFH_553_DE4546-303 + https://registry.gdi-de.org/id/de.bb.inspire.ps.schutzg + + + + + + + + + + + + + deu + + + + + + + Große Röder + latn + + + + + natureConservation + + + \ No newline at end of file diff --git a/tests/testdata/provider/wfs/inspire_complexfeatures/getfeature_hits.xml b/tests/testdata/provider/wfs/inspire_complexfeatures/getfeature_hits.xml new file mode 100644 index 00000000000..8e345f70c4d --- /dev/null +++ b/tests/testdata/provider/wfs/inspire_complexfeatures/getfeature_hits.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file