Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
[WFS] Improve support of layers with unknown geometry type with Deegr…
…ee servers

or any server implementing IsPoint, IsCurve and IsSurface filtering
functions.

When such a layer is added to the project, the Select Items to Add
dialog box used for example for OGR vector layers with unknown geometry
type is displayed, enabling the user to select one or several layers
among ones of NoGeometry/MultiPoint/MultiCurve/MultiSurface/GeometryCollection
types.

Fixes #49328
  • Loading branch information
rouault committed Jan 3, 2023
1 parent 0c8f010 commit 28f94da
Show file tree
Hide file tree
Showing 17 changed files with 1,352 additions and 195 deletions.
2 changes: 2 additions & 0 deletions src/providers/wfs/CMakeLists.txt
Expand Up @@ -5,9 +5,11 @@
set(WFS_SRCS
${CMAKE_SOURCE_DIR}/external/nlohmann/json.hpp
qgswfsprovider.cpp
qgswfsprovidermetadata.cpp
qgswfscapabilities.cpp
qgswfsdataitems.cpp
qgswfsfeatureiterator.cpp
qgswfsgetfeature.cpp
qgswfsrequest.cpp
qgswfsconnection.cpp
qgswfsdatasourceuri.cpp
Expand Down
30 changes: 30 additions & 0 deletions src/providers/wfs/qgswfscapabilities.cpp
Expand Up @@ -119,6 +119,36 @@ QString QgsWfsCapabilities::Capabilities::getNamespaceParameterValue( const QStr
return QString();
}

bool QgsWfsCapabilities::Capabilities::supportsGeometryTypeFilters() const
{
// Detect servers, such as Deegree, that expose additional filter functions
// to test if a geometry is a (multi)point, (multi)curve or (multi)surface
// This can be used to figure out which geometry types are present in layers
// that describe a generic geometry type.
bool hasIsPoint = false;
bool hasIsCurve = false;
bool hasIsSurface = false;
for ( const auto &function : functionList )
{
if ( function.minArgs == 1 && function.maxArgs == 1 )
{
if ( function.name == QLatin1String( "IsPoint" ) )
{
hasIsPoint = true;
}
else if ( function.name == QLatin1String( "IsCurve" ) )
{
hasIsCurve = true;
}
else if ( function.name == QLatin1String( "IsSurface" ) )
{
hasIsSurface = true;
}
}
}
return hasIsPoint && hasIsCurve && hasIsSurface;
}

class CPLXMLTreeUniquePointer
{
public:
Expand Down
3 changes: 3 additions & 0 deletions src/providers/wfs/qgswfscapabilities.h
Expand Up @@ -110,6 +110,9 @@ class QgsWfsCapabilities : public QgsWfsRequest
QString addPrefixIfNeeded( const QString &name ) const;
QString getNamespaceForTypename( const QString &name ) const;
QString getNamespaceParameterValue( const QString &WFSVersion, const QString &typeName ) const;

//! Returns whether the server supports IsPoint, IsCurve and IsSurface functions
bool supportsGeometryTypeFilters() const;
};

//! Application level error
Expand Down
1 change: 1 addition & 0 deletions src/providers/wfs/qgswfsconstants.cpp
Expand Up @@ -42,6 +42,7 @@ const QString QgsWFSConstants::URI_PARAM_PAGING_ENABLED( "pagingEnabled" );
const QString QgsWFSConstants::URI_PARAM_PAGE_SIZE( "pageSize" );
const QString QgsWFSConstants::URI_PARAM_WFST_1_1_PREFER_COORDINATES( "preferCoordinatesForWfsT11" );
const QString QgsWFSConstants::URI_PARAM_SKIP_INITIAL_GET_FEATURE( "skipInitialGetFeature" );
const QString QgsWFSConstants::URI_PARAM_GEOMETRY_TYPE_FILTER( QStringLiteral( "geometryTypeFilter" ) );

const QString QgsWFSConstants::VERSION_AUTO( QStringLiteral( "auto" ) );

1 change: 1 addition & 0 deletions src/providers/wfs/qgswfsconstants.h
Expand Up @@ -50,6 +50,7 @@ struct QgsWFSConstants
static const QString URI_PARAM_PAGE_SIZE;
static const QString URI_PARAM_WFST_1_1_PREFER_COORDINATES;
static const QString URI_PARAM_SKIP_INITIAL_GET_FEATURE;
static const QString URI_PARAM_GEOMETRY_TYPE_FILTER;

//
static const QString VERSION_AUTO;
Expand Down
13 changes: 12 additions & 1 deletion src/providers/wfs/qgswfsdatasourceuri.cpp
Expand Up @@ -172,7 +172,8 @@ QSet<QString> QgsWFSDataSourceURI::unknownParamKeys() const
QgsWFSConstants::URI_PARAM_PAGING_ENABLED,
QgsWFSConstants::URI_PARAM_PAGE_SIZE,
QgsWFSConstants::URI_PARAM_WFST_1_1_PREFER_COORDINATES,
QgsWFSConstants::URI_PARAM_SKIP_INITIAL_GET_FEATURE
QgsWFSConstants::URI_PARAM_SKIP_INITIAL_GET_FEATURE,
QgsWFSConstants::URI_PARAM_GEOMETRY_TYPE_FILTER,
};

QSet<QString> l_unknownParamKeys;
Expand Down Expand Up @@ -355,6 +356,16 @@ void QgsWFSDataSourceURI::setFilter( const QString &filter )
}
}

bool QgsWFSDataSourceURI::hasGeometryTypeFilter() const
{
return mURI.hasParam( QgsWFSConstants::URI_PARAM_GEOMETRY_TYPE_FILTER );
}

QgsWkbTypes::Type QgsWFSDataSourceURI::geometryTypeFilter() const
{
return QgsWkbTypes::parseType( mURI.param( QgsWFSConstants::URI_PARAM_GEOMETRY_TYPE_FILTER ) );
}

QString QgsWFSDataSourceURI::sql() const
{
return mURI.sql();
Expand Down
6 changes: 6 additions & 0 deletions src/providers/wfs/qgswfsdatasourceuri.h
Expand Up @@ -87,6 +87,12 @@ class QgsWFSDataSourceURI
//! Sets OGC filter xml or a QGIS expression
void setFilter( const QString &filterIn );

//! Returns whether there is a geometry type filter.
bool hasGeometryTypeFilter() const;

//! Gets the geometry type filter.
QgsWkbTypes::Type geometryTypeFilter() const;

//! Gets SQL query
QString sql() const;

Expand Down
132 changes: 43 additions & 89 deletions src/providers/wfs/qgswfsfeatureiterator.cpp
Expand Up @@ -174,14 +174,14 @@ QUrl QgsWFSFeatureDownloaderImpl::buildURL( qint64 startIndex, long long maxFeat
query.addQueryItem( QStringLiteral( "SRSNAME" ), srsName );
}

const QgsRectangle &rect = mShared->currentRect();

// In case we must issue a BBOX and we have a filter, we must combine
// both as a single filter, as both BBOX and FILTER aren't supported together
if ( ( !rect.isNull() && !mShared->mWFSFilter.isEmpty() )
|| ( !mShared->mServerExpression.isEmpty() && !mShared->mWFSFilter.isEmpty() )
|| ( !rect.isNull() && !mShared->mServerExpression.isEmpty() )
)
std::vector<QString> filters;
if ( !mShared->mServerExpression.isEmpty() )
filters.push_back( mShared->mServerExpression );

const QgsRectangle &rect = mShared->currentRect();
if ( !rect.isNull() && ( !mShared->mWFSFilter.isEmpty() || !mShared->mServerExpression.isEmpty() || !mShared->mWFSGeometryTypeFilter.isEmpty() ) )
{
QgsOgcUtils::GMLVersion gmlVersion;
QgsOgcUtils::FilterVersion filterVersion;
Expand All @@ -203,95 +203,37 @@ QUrl QgsWFSFeatureDownloaderImpl::buildURL( qint64 startIndex, long long maxFeat
gmlVersion = QgsOgcUtils::GML_3_2_1;
filterVersion = QgsOgcUtils::FILTER_FES_2_0;
}

QDomDocument bboxDoc;
QDomDocument filterDoc;
QDomDocument expressionDoc;

QString geometryAttribute( mShared->mGeometryAttribute );
if ( mShared->mLayerPropertiesList.size() > 1 )
geometryAttribute = mShared->mURI.typeName() + "/" + geometryAttribute;

QDomNode bboxNode;
QDomNode filterNode;
QDomNode expressionNode;

QDomDocument envelopeFilterDoc;

//having a filter
if ( !mShared->mWFSFilter.isEmpty() )
{
( void )filterDoc.setContent( mShared->mWFSFilter, true );
filterNode = filterDoc.firstChildElement().firstChildElement();
filterNode = filterDoc.firstChildElement().removeChild( filterNode );
envelopeFilterDoc = filterDoc;
}

//having an expression
if ( !mShared->mServerExpression.isEmpty() )
{
( void )expressionDoc.setContent( mShared->mServerExpression, true );
expressionNode = expressionDoc.firstChildElement().firstChildElement();
expressionNode = expressionDoc.firstChildElement().removeChild( expressionNode );
envelopeFilterDoc = expressionDoc;
}

//having a bbox
if ( !rect.isNull() )
{
double minx = rect.xMinimum();
double miny = rect.yMinimum();
double maxx = rect.xMaximum();
double maxy = rect.yMaximum();
QString filterBbox( QStringLiteral( "intersects_bbox($geometry, geomFromWKT('LINESTRING(%1 %2,%3 %4)'))" ).
arg( minx ).arg( miny ).arg( maxx ).arg( maxy ) );
QgsExpression bboxExp( filterBbox );
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() );
bboxDoc.appendChild( bboxElem );

bboxNode = bboxElem.firstChildElement();
bboxNode = bboxElem.removeChild( bboxNode );
envelopeFilterDoc = bboxDoc;
}

QDomElement andElem = envelopeFilterDoc.createElement( ( filterVersion == QgsOgcUtils::FILTER_FES_2_0 ) ? "fes:And" : "ogc:And" );
if ( !expressionNode.isNull() )
andElem.appendChild( expressionNode );
if ( !bboxNode.isNull() )
andElem.appendChild( bboxNode );
if ( !filterNode.isNull() )
andElem.appendChild( filterNode );

envelopeFilterDoc.firstChildElement().appendChild( andElem );

if ( mShared->mLayerPropertiesList.size() == 1 &&
envelopeFilterDoc.firstChildElement().hasAttribute( QStringLiteral( "xmlns:" ) + mShared->mLayerPropertiesList[0].mNamespacePrefix ) )
{
// nothing to do
}
else
{
// add xmls:PREFIX=URI attributes to top element
QSet<QString> setNamespaceURI;
for ( const QgsOgcUtils::LayerProperties &props : std::as_const( mShared->mLayerPropertiesList ) )
{
if ( !props.mNamespacePrefix.isEmpty() && !props.mNamespaceURI.isEmpty() &&
!setNamespaceURI.contains( props.mNamespaceURI ) )
{
setNamespaceURI.insert( props.mNamespaceURI );
QDomAttr attr = envelopeFilterDoc.createAttribute( QStringLiteral( "xmlns:" ) + props.mNamespacePrefix );
attr.setValue( props.mNamespaceURI );
envelopeFilterDoc.firstChildElement().setAttributeNode( attr );
}
}
}
double minx = rect.xMinimum();
double miny = rect.yMinimum();
double maxx = rect.xMaximum();
double maxy = rect.yMaximum();
QString filterBbox( QStringLiteral( "intersects_bbox($geometry, geomFromWKT('LINESTRING(%1 %2,%3 %4)'))" ).
arg( minx ).arg( miny ).arg( maxx ).arg( maxy ) );
QgsExpression bboxExp( filterBbox );
QDomDocument bboxDoc;
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() );
bboxDoc.appendChild( bboxElem );

filters.push_back( bboxDoc.toString() );
}
if ( !mShared->mWFSFilter.isEmpty() )
filters.push_back( mShared->mWFSFilter );
if ( !mShared->mWFSGeometryTypeFilter.isEmpty() )
filters.push_back( mShared->mWFSGeometryTypeFilter );

query.addQueryItem( QStringLiteral( "FILTER" ), sanitizeFilter( envelopeFilterDoc.toString() ) );
if ( filters.size() >= 2 )
{
query.addQueryItem( QStringLiteral( "FILTER" ), sanitizeFilter( mShared->combineWFSFilters( filters ) ) );
}
else if ( !rect.isNull() )
{
Expand Down Expand Up @@ -338,6 +280,11 @@ QUrl QgsWFSFeatureDownloaderImpl::buildURL( qint64 startIndex, long long maxFeat
{
query.addQueryItem( QStringLiteral( "FILTER" ), sanitizeFilter( mShared->mServerExpression ) );
}
else if ( !mShared->mWFSGeometryTypeFilter.isEmpty() )
{
query.addQueryItem( QStringLiteral( "FILTER" ), sanitizeFilter( mShared->mWFSGeometryTypeFilter ) );
}


if ( !mShared->mSortBy.isEmpty() && !forHits )
{
Expand Down Expand Up @@ -742,6 +689,13 @@ void QgsWFSFeatureDownloaderImpl::run( bool serializeFeatures, long long maxFeat
f.setGeometry( QgsGeometry( newGC ) );
}
}
else if ( f.hasGeometry() && !mShared->mWFSGeometryTypeFilter.isEmpty() &&
QgsWkbTypes::flatType( f.geometry().wkbType() ) != mShared->mWKBType )
{
QgsGeometry g = f.geometry();
g.convertToCurvedMultiType();
f.setGeometry( g );
}

featureList.push_back( QgsFeatureUniqueIdPair( f, gmlId ) );
delete featPair.first;
Expand Down
69 changes: 69 additions & 0 deletions src/providers/wfs/qgswfsgetfeature.cpp
@@ -0,0 +1,69 @@
/***************************************************************************
qgswfsgetfeature.cpp
---------------------
begin : November 2022
copyright : (C) 2022 by Even Rouault
email : even.rouault at spatialys.com
***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/

#include "qgswfsgetfeature.h"
#include "qgsmessagelog.h"
#include <QUrlQuery>

QgsWFSGetFeature::QgsWFSGetFeature( QgsWFSDataSourceURI &uri )
: QgsWfsRequest( uri )
{
}

bool QgsWFSGetFeature::request( bool synchronous, const QString &WFSVersion,
const QString &typeName, const QString &filter, bool hitsOnly, const QgsWfsCapabilities::Capabilities &caps )
{
QUrl url( mUri.requestUrl( QStringLiteral( "GetFeature" ) ) );
QUrlQuery query( url );
query.addQueryItem( QStringLiteral( "VERSION" ), WFSVersion );

const QString namespaceValue( caps.getNamespaceParameterValue( WFSVersion, typeName ) );

if ( WFSVersion.startsWith( QLatin1String( "2.0" ) ) )
{
query.addQueryItem( QStringLiteral( "TYPENAMES" ), typeName );
if ( !namespaceValue.isEmpty() )
{
query.addQueryItem( QStringLiteral( "NAMESPACES" ), namespaceValue );
}
}
else
{
query.addQueryItem( QStringLiteral( "TYPENAME" ), typeName );
}

if ( !namespaceValue.isEmpty() )
{
query.addQueryItem( QStringLiteral( "NAMESPACE" ), namespaceValue );
}

if ( !filter.isEmpty() )
{
query.addQueryItem( QStringLiteral( "FILTER" ), filter );
}

if ( hitsOnly )
{
query.addQueryItem( QStringLiteral( "RESULTTYPE" ), "hits" );
}

url.setQuery( query );
return sendGET( url, QString(), synchronous, /*forceRefresh=*/ true, /* cache=*/ false );
}

QString QgsWFSGetFeature::errorMessageWithReason( const QString &reason )
{
return tr( "Download of feature type failed: %1" ).arg( reason );
}
40 changes: 40 additions & 0 deletions src/providers/wfs/qgswfsgetfeature.h
@@ -0,0 +1,40 @@
/***************************************************************************
qgswfsgetfeature.h
---------------------
begin : November 2022
copyright : (C) 2022 by Even Rouault
email : even.rouault at spatialys.com
***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
#ifndef QGSWFSGETFEATURE_H
#define QGSWFSGETFEATURE_H

#include "qgswfsrequest.h"
#include "qgswfscapabilities.h"

//! Manages the QgsWFSGetFeature request
class QgsWFSGetFeature : public QgsWfsRequest
{
Q_OBJECT
public:
explicit QgsWFSGetFeature( QgsWFSDataSourceURI &uri );

//! Issue the request
bool request( bool synchronous,
const QString &WFSVersion,
const QString &typeName,
const QString &filter,
bool hitsOnly,
const QgsWfsCapabilities::Capabilities &caps );

protected:
QString errorMessageWithReason( const QString &reason ) override;
};

#endif // QGSWFSGETFEATURE_H

0 comments on commit 28f94da

Please sign in to comment.