Skip to content

Commit

Permalink
[server] Allow WMS GetFeatureInfo on root layer and groups
Browse files Browse the repository at this point in the history
If the group (or the root) has any queryable child, it is flagged as queryable

Also fixes an issue with json enconding with rasters.

Fixes #21697

Funded by: Kanton Zug
  • Loading branch information
elpaso committed Apr 4, 2019
1 parent 7e23f4d commit 9a689af
Show file tree
Hide file tree
Showing 12 changed files with 4,529 additions and 10 deletions.
41 changes: 40 additions & 1 deletion src/server/services/wms/qgswmsgetcapabilities.cpp
Expand Up @@ -841,6 +841,15 @@ namespace QgsWms
layerParentElem.appendChild( treeNameElem );
}

if ( hasQueryableChildren( projectLayerTreeRoot, QgsServerProjectUtils::wmsRestrictedLayers( *project ) ) )
{
layerParentElem.setAttribute( QStringLiteral( "queryable" ), QStringLiteral( "1" ) );
}
else
{
layerParentElem.setAttribute( QStringLiteral( "queryable" ), QStringLiteral( "0" ) );
}

appendLayersFromTreeGroup( doc, layerParentElem, serverIface, project, version, request, projectLayerTreeRoot, projectSettings );

combineExtentAndCrsOfGroupChildren( doc, layerParentElem, project, true );
Expand All @@ -862,7 +871,7 @@ namespace QgsWms
{
bool useLayerIds = QgsServerProjectUtils::wmsUseLayerIds( *project );
bool siaFormat = QgsServerProjectUtils::wmsInfoFormatSia2045( *project );
QStringList restrictedLayers = QgsServerProjectUtils::wmsRestrictedLayers( *project );
const QStringList restrictedLayers = QgsServerProjectUtils::wmsRestrictedLayers( *project );

QList< QgsLayerTreeNode * > layerTreeGroupChildren = layerTreeGroup->children();
for ( int i = 0; i < layerTreeGroupChildren.size(); ++i )
Expand Down Expand Up @@ -929,6 +938,16 @@ namespace QgsWms
layerElem.appendChild( treeNameElem );
}

// Set queryable if any of the children are
if ( hasQueryableChildren( treeNode, restrictedLayers ) )
{
layerElem.setAttribute( QStringLiteral( "queryable" ), QStringLiteral( "1" ) );
}
else
{
layerElem.setAttribute( QStringLiteral( "queryable" ), QStringLiteral( "0" ) );
}

appendLayersFromTreeGroup( doc, layerElem, serverIface, project, version, request, treeGroupChild, projectSettings );

combineExtentAndCrsOfGroupChildren( doc, layerElem, project );
Expand Down Expand Up @@ -1857,6 +1876,26 @@ namespace QgsWms
}
}

bool hasQueryableChildren( const QgsLayerTreeNode *childNode, const QStringList &wmsRestrictedLayers )
{
if ( childNode->nodeType() == QgsLayerTreeNode::NodeGroup )
{
for ( int j = 0; j < childNode->children().size(); ++j )
{
if ( hasQueryableChildren( childNode->children().at( j ), wmsRestrictedLayers ) )
return true;
}
return false;
}
else if ( childNode->nodeType() == QgsLayerTreeNode::NodeLayer )
{
const auto treeLayer { static_cast<const QgsLayerTreeLayer *>( childNode ) };
const auto l { treeLayer->layer() };
return ! wmsRestrictedLayers.contains( l->name() ) && l->flags().testFlag( QgsMapLayer::Identifiable );
}
return false;
}


} // namespace QgsWms

Expand Down
2 changes: 2 additions & 0 deletions src/server/services/wms/qgswmsgetcapabilities.h
Expand Up @@ -82,6 +82,8 @@ namespace QgsWms
QDomDocument getCapabilities( QgsServerInterface *serverIface, const QgsProject *project,
const QString &version, const QgsServerRequest &request,
bool projectSettings );

bool hasQueryableChildren( const QgsLayerTreeNode *childNode, const QStringList &wmsRestrictedLayers );
} // namespace QgsWms

#endif
33 changes: 32 additions & 1 deletion src/server/services/wms/qgswmsrendercontext.cpp
Expand Up @@ -159,6 +159,32 @@ qreal QgsWmsRenderContext::dotsPerMm() const
return dpm / 1000.0;
}

QStringList QgsWmsRenderContext::flattenedQueryLayers() const
{
QStringList result;
std::function <QStringList( const QString &name )> findLeaves = [ & ]( const QString & name ) -> QStringList
{
QStringList _result;
if ( mLayerGroups.contains( name ) )
{
for ( const auto &l : mLayerGroups[ name ] )
{
_result.append( findLeaves( l->shortName().isEmpty() ? l->name() : l->shortName() ) );
}
}
else
{
_result.append( name );
}
return _result;
};
for ( const auto &name : mParameters.queryLayersNickname() )
{
result.append( findLeaves( name ) );
}
return result;
}

QList<QgsMapLayer *> QgsWmsRenderContext::layersToRender() const
{
return mLayersToRender;
Expand Down Expand Up @@ -340,7 +366,7 @@ void QgsWmsRenderContext::searchLayersToRender()

if ( mFlags & AddQueryLayers )
{
for ( const QString &layer : mParameters.queryLayersNickname() )
for ( const QString &layer : flattenedQueryLayers() )
{
if ( mNicknameLayers.contains( layer )
&& !mLayersToRender.contains( mNicknameLayers[layer] ) )
Expand Down Expand Up @@ -471,6 +497,11 @@ bool QgsWmsRenderContext::layerScaleVisibility( const QString &name ) const
return visible;
}

QMap<QString, QList<QgsMapLayer *> > QgsWmsRenderContext::layerGroups() const
{
return mLayerGroups;
}

void QgsWmsRenderContext::removeUnwantedLayers()
{
QList<QgsMapLayer *> layers;
Expand Down
12 changes: 12 additions & 0 deletions src/server/services/wms/qgswmsrendercontext.h
Expand Up @@ -179,6 +179,12 @@ namespace QgsWms
*/
qreal dotsPerMm() const;

/**
* Return a list of query layer names where group names are replaced by the names of their layer components.
* \since QGIS 3.8
*/
QStringList flattenedQueryLayers() const;

#ifdef HAVE_SERVER_PYTHON_PLUGINS

/**
Expand All @@ -187,6 +193,12 @@ namespace QgsWms
QgsAccessControl *accessControl() const;
#endif

/**
* Returns a map having layer group names as keys and a list of layer instances as values.
* \since QGIS 3.8
*/
QMap<QString, QList<QgsMapLayer *> > layerGroups() const;

private:
void initNicknameLayers();
void initRestrictedLayers();
Expand Down
43 changes: 39 additions & 4 deletions src/server/services/wms/qgswmsrenderer.cpp
Expand Up @@ -1135,7 +1135,7 @@ namespace QgsWms
QDomDocument QgsRenderer::featureInfoDocument( QList<QgsMapLayer *> &layers, const QgsMapSettings &mapSettings,
const QImage *outputImage, const QString &version ) const
{
QStringList queryLayers = mWmsParameters.queryLayersNickname();
const QStringList queryLayers = mContext.flattenedQueryLayers( );

bool ijDefined = ( !mWmsParameters.i().isEmpty() && !mWmsParameters.j().isEmpty() );

Expand Down Expand Up @@ -1322,8 +1322,39 @@ namespace QgsWms
{
QgsWmsParameter param( QgsWmsParameter::LAYER );
param.mValue = queryLayer;
throw QgsBadRequestException( QgsServiceException::OGC_LAYER_NOT_QUERYABLE,
param );
// Check if this layer belongs to a group and the group has any queryable layers
bool hasGroupAndQueryable { false };
if ( ! mContext.parameters().queryLayersNickname().contains( queryLayer ) )
{
// Find which group this layer belongs to
const auto &constNicks { mContext.parameters().queryLayersNickname() };
for ( const auto &ql : constNicks )
{
if ( mContext.layerGroups().contains( ql ) )
{
const auto &constLayers { mContext.layerGroups()[ql] };
for ( const auto &ml : constLayers )
{
if ( ( ! ml->shortName().isEmpty() && ml->shortName() == queryLayer ) || ( ml->name() == queryLayer ) )
{
param.mValue = ql;
}
if ( ml->flags().testFlag( QgsMapLayer::Identifiable ) )
{
hasGroupAndQueryable = true;
break;
}
}
break;
}
}
}
// Only throw if it's not a group or the group has no queryable children
if ( ! hasGroupAndQueryable )
{
throw QgsBadRequestException( QgsServiceException::OGC_LAYER_NOT_QUERYABLE,
param );
}
}
}

Expand Down Expand Up @@ -2206,7 +2237,7 @@ namespace QgsWms
exporter.setAttributes( attributes );
exporter.setIncludeGeometry( withGeometry );

for ( const auto feature : features )
for ( const auto &feature : qgis::as_const( features ) )
{
if ( json.right( 1 ).compare( QStringLiteral( "}" ) ) == 0 )
{
Expand All @@ -2219,6 +2250,10 @@ namespace QgsWms
}
else // raster layer
{
if ( json.right( 1 ).compare( QStringLiteral( "}" ) ) == 0 )
{
json.append( QStringLiteral( "," ) );
}
json.append( QStringLiteral( "{" ) );
json.append( QStringLiteral( "\"type\":\"Feature\",\n" ) );
json.append( QStringLiteral( "\"id\":\"%1\",\n" ).arg( layer->name() ) );
Expand Down
111 changes: 111 additions & 0 deletions tests/src/python/test_qgsserver_wms_getfeatureinfo.py
Expand Up @@ -591,6 +591,117 @@ def testGetFeatureInfoPostgresTypes(self):
attribute.get('value')), {
'c': 4.0, 'd': 5.0})

def testGetFeatureInfoGroupedLayers(self):
"""Test that we can get feature info from the top and group layers"""

# areas+and+symbols (not nested)
self.wms_request_compare('GetFeatureInfo',
'&BBOX=52.44095517977704901,10.71171069440170776,52.440955186258563,10.71171070552261817' +
'&CRS=EPSG:4326' +
'&WIDTH=2&HEIGHT=2' +
'&QUERY_LAYERS=areas+and+symbols' +
'&INFO_FORMAT=application/json' +
'&I=0&J=1' +
'&FEATURE_COUNT=10',
'wms_getfeatureinfo_group_name_areas',
'test_project_wms_grouped_layers.qgs')

# areas+and+symbols (nested)
self.wms_request_compare('GetFeatureInfo',
'&BBOX=52.44095517977704901,10.71171069440170776,52.440955186258563,10.71171070552261817' +
'&CRS=EPSG:4326' +
'&WIDTH=2&HEIGHT=2' +
'&QUERY_LAYERS=areas+and+symbols' +
'&INFO_FORMAT=application/json' +
'&I=0&J=1' +
'&FEATURE_COUNT=10',
'wms_getfeatureinfo_group_name_areas',
'test_project_wms_grouped_nested_layers.qgs')

# as-areas-short-name
self.wms_request_compare('GetFeatureInfo',
'&BBOX=52.44095517977704901,10.71171069440170776,52.440955186258563,10.71171070552261817' +
'&CRS=EPSG:4326' +
'&WIDTH=2&HEIGHT=2' +
'&QUERY_LAYERS=as-areas-short-name' +
'&INFO_FORMAT=application/json' +
'&I=0&J=1' +
'&FEATURE_COUNT=10',
'wms_getfeatureinfo_group_name_areas',
'test_project_wms_grouped_nested_layers.qgs')

# Top level: QGIS Server - Grouped Layer
self.wms_request_compare('GetFeatureInfo',
'&BBOX=52.44095517977704901,10.71171069440170776,52.440955186258563,10.71171070552261817' +
'&CRS=EPSG:4326' +
'&WIDTH=2&HEIGHT=2' +
'&QUERY_LAYERS=QGIS+Server+-+Grouped Nested Layer' +
'&INFO_FORMAT=application/json' +
'&I=0&J=1' +
'&FEATURE_COUNT=10',
'wms_getfeatureinfo_group_name_top',
'test_project_wms_grouped_nested_layers.qgs')

# Multiple matches from 2 layer groups
self.wms_request_compare('GetFeatureInfo',
'&BBOX=52.44095517977704901,10.71171069440170776,52.440955186258563,10.71171070552261817' +
'&CRS=EPSG:4326' +
'&WIDTH=2&HEIGHT=2' +
'&QUERY_LAYERS=areas+and+symbols,city+and+district+boundaries' +
'&INFO_FORMAT=application/json' +
'&I=0&J=1' +
'&FEATURE_COUNT=10',
'wms_getfeatureinfo_group_name_areas_cities',
'test_project_wms_grouped_nested_layers.qgs')

# no_query group (nested)
self.wms_request_compare('GetFeatureInfo',
'&BBOX=52.44095517977704901,10.71171069440170776,52.440955186258563,10.71171070552261817' +
'&CRS=EPSG:4326' +
'&WIDTH=2&HEIGHT=2' +
'&QUERY_LAYERS=no_query' +
'&INFO_FORMAT=application/json' +
'&I=0&J=1' +
'&FEATURE_COUNT=10',
'wms_getfeatureinfo_group_no_query',
'test_project_wms_grouped_nested_layers.qgs')

# query_child group (nested)
self.wms_request_compare('GetFeatureInfo',
'&BBOX=52.44095517977704901,10.71171069440170776,52.440955186258563,10.71171070552261817' +
'&CRS=EPSG:4326' +
'&WIDTH=2&HEIGHT=2' +
'&QUERY_LAYERS=query_child' +
'&INFO_FORMAT=application/json' +
'&I=0&J=1' +
'&FEATURE_COUNT=10',
'wms_getfeatureinfo_group_query_child',
'test_project_wms_grouped_nested_layers.qgs')

# child_ok group (nested)
self.wms_request_compare('GetFeatureInfo',
'&BBOX=52.44095517977704901,10.71171069440170776,52.440955186258563,10.71171070552261817' +
'&CRS=EPSG:4326' +
'&WIDTH=2&HEIGHT=2' +
'&QUERY_LAYERS=child_ok' +
'&INFO_FORMAT=application/json' +
'&I=0&J=1' +
'&FEATURE_COUNT=10',
'wms_getfeatureinfo_group_query_child',
'test_project_wms_grouped_nested_layers.qgs')

# as_areas_query_copy == as-areas-short-name-query-copy (nested)
self.wms_request_compare('GetFeatureInfo',
'&BBOX=52.44095517977704901,10.71171069440170776,52.440955186258563,10.71171070552261817' +
'&CRS=EPSG:4326' +
'&WIDTH=2&HEIGHT=2' +
'&QUERY_LAYERS=as-areas-short-name-query-copy' +
'&INFO_FORMAT=application/json' +
'&I=0&J=1' +
'&FEATURE_COUNT=10',
'wms_getfeatureinfo_group_query_child',
'test_project_wms_grouped_nested_layers.qgs')


if __name__ == '__main__':
unittest.main()

0 comments on commit 9a689af

Please sign in to comment.