Skip to content

Commit 3c4b2a7

Browse files
authored
Merge pull request #9704 from elpaso/bugfix-21697-server-gfi-top-group-layers
[server] Allow WMS GetFeatureInfo on root layer and groups
2 parents 5e0fd15 + d995070 commit 3c4b2a7

14 files changed

+4578
-12
lines changed

src/server/services/wms/qgswmsgetcapabilities.cpp

+40-1
Original file line numberDiff line numberDiff line change
@@ -846,6 +846,15 @@ namespace QgsWms
846846
layerParentElem.appendChild( treeNameElem );
847847
}
848848

849+
if ( hasQueryableChildren( projectLayerTreeRoot, QgsServerProjectUtils::wmsRestrictedLayers( *project ) ) )
850+
{
851+
layerParentElem.setAttribute( QStringLiteral( "queryable" ), QStringLiteral( "1" ) );
852+
}
853+
else
854+
{
855+
layerParentElem.setAttribute( QStringLiteral( "queryable" ), QStringLiteral( "0" ) );
856+
}
857+
849858
appendLayersFromTreeGroup( doc, layerParentElem, serverIface, project, version, request, projectLayerTreeRoot, projectSettings );
850859

851860
combineExtentAndCrsOfGroupChildren( doc, layerParentElem, project, true );
@@ -867,7 +876,7 @@ namespace QgsWms
867876
{
868877
bool useLayerIds = QgsServerProjectUtils::wmsUseLayerIds( *project );
869878
bool siaFormat = QgsServerProjectUtils::wmsInfoFormatSia2045( *project );
870-
QStringList restrictedLayers = QgsServerProjectUtils::wmsRestrictedLayers( *project );
879+
const QStringList restrictedLayers = QgsServerProjectUtils::wmsRestrictedLayers( *project );
871880

872881
QList< QgsLayerTreeNode * > layerTreeGroupChildren = layerTreeGroup->children();
873882
for ( int i = 0; i < layerTreeGroupChildren.size(); ++i )
@@ -934,6 +943,16 @@ namespace QgsWms
934943
layerElem.appendChild( treeNameElem );
935944
}
936945

946+
// Set queryable if any of the children are
947+
if ( hasQueryableChildren( treeNode, restrictedLayers ) )
948+
{
949+
layerElem.setAttribute( QStringLiteral( "queryable" ), QStringLiteral( "1" ) );
950+
}
951+
else
952+
{
953+
layerElem.setAttribute( QStringLiteral( "queryable" ), QStringLiteral( "0" ) );
954+
}
955+
937956
appendLayersFromTreeGroup( doc, layerElem, serverIface, project, version, request, treeGroupChild, projectSettings );
938957

939958
combineExtentAndCrsOfGroupChildren( doc, layerElem, project );
@@ -1863,6 +1882,26 @@ namespace QgsWms
18631882
}
18641883
}
18651884

1885+
bool hasQueryableChildren( const QgsLayerTreeNode *childNode, const QStringList &wmsRestrictedLayers )
1886+
{
1887+
if ( childNode->nodeType() == QgsLayerTreeNode::NodeGroup )
1888+
{
1889+
for ( int j = 0; j < childNode->children().size(); ++j )
1890+
{
1891+
if ( hasQueryableChildren( childNode->children().at( j ), wmsRestrictedLayers ) )
1892+
return true;
1893+
}
1894+
return false;
1895+
}
1896+
else if ( childNode->nodeType() == QgsLayerTreeNode::NodeLayer )
1897+
{
1898+
const auto treeLayer { static_cast<const QgsLayerTreeLayer *>( childNode ) };
1899+
const auto l { treeLayer->layer() };
1900+
return ! wmsRestrictedLayers.contains( l->name() ) && l->flags().testFlag( QgsMapLayer::Identifiable );
1901+
}
1902+
return false;
1903+
}
1904+
18661905

18671906
} // namespace QgsWms
18681907

src/server/services/wms/qgswmsgetcapabilities.h

+2
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ namespace QgsWms
8282
QDomDocument getCapabilities( QgsServerInterface *serverIface, const QgsProject *project,
8383
const QString &version, const QgsServerRequest &request,
8484
bool projectSettings );
85+
86+
bool hasQueryableChildren( const QgsLayerTreeNode *childNode, const QStringList &wmsRestrictedLayers );
8587
} // namespace QgsWms
8688

8789
#endif

src/server/services/wms/qgswmsrendercontext.cpp

+33-1
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,32 @@ qreal QgsWmsRenderContext::dotsPerMm() const
159159
return dpm / 1000.0;
160160
}
161161

162+
QStringList QgsWmsRenderContext::flattenedQueryLayers() const
163+
{
164+
QStringList result;
165+
std::function <QStringList( const QString &name )> findLeaves = [ & ]( const QString & name ) -> QStringList
166+
{
167+
QStringList _result;
168+
if ( mLayerGroups.contains( name ) )
169+
{
170+
for ( const auto &l : mLayerGroups[ name ] )
171+
{
172+
_result.append( findLeaves( l->shortName().isEmpty() ? l->name() : l->shortName() ) );
173+
}
174+
}
175+
else
176+
{
177+
_result.append( name );
178+
}
179+
return _result;
180+
};
181+
for ( const auto &name : mParameters.queryLayersNickname() )
182+
{
183+
result.append( findLeaves( name ) );
184+
}
185+
return result;
186+
}
187+
162188
QList<QgsMapLayer *> QgsWmsRenderContext::layersToRender() const
163189
{
164190
return mLayersToRender;
@@ -340,7 +366,8 @@ void QgsWmsRenderContext::searchLayersToRender()
340366

341367
if ( mFlags & AddQueryLayers )
342368
{
343-
for ( const QString &layer : mParameters.queryLayersNickname() )
369+
const auto constLayers { flattenedQueryLayers() };
370+
for ( const QString &layer : constLayers )
344371
{
345372
if ( mNicknameLayers.contains( layer )
346373
&& !mLayersToRender.contains( mNicknameLayers[layer] ) )
@@ -471,6 +498,11 @@ bool QgsWmsRenderContext::layerScaleVisibility( const QString &name ) const
471498
return visible;
472499
}
473500

501+
QMap<QString, QList<QgsMapLayer *> > QgsWmsRenderContext::layerGroups() const
502+
{
503+
return mLayerGroups;
504+
}
505+
474506
void QgsWmsRenderContext::removeUnwantedLayers()
475507
{
476508
QList<QgsMapLayer *> layers;

src/server/services/wms/qgswmsrendercontext.h

+12
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,12 @@ namespace QgsWms
179179
*/
180180
qreal dotsPerMm() const;
181181

182+
/**
183+
* Returns a list of query layer names where group names are replaced by the names of their layer components.
184+
* \since QGIS 3.8
185+
*/
186+
QStringList flattenedQueryLayers() const;
187+
182188
#ifdef HAVE_SERVER_PYTHON_PLUGINS
183189

184190
/**
@@ -187,6 +193,12 @@ namespace QgsWms
187193
QgsAccessControl *accessControl() const;
188194
#endif
189195

196+
/**
197+
* Returns a map having layer group names as keys and a list of layers as values.
198+
* \since QGIS 3.8
199+
*/
200+
QMap<QString, QList<QgsMapLayer *> > layerGroups() const;
201+
190202
private:
191203
void initNicknameLayers();
192204
void initRestrictedLayers();

src/server/services/wms/qgswmsrenderer.cpp

+39-4
Original file line numberDiff line numberDiff line change
@@ -1135,7 +1135,7 @@ namespace QgsWms
11351135
QDomDocument QgsRenderer::featureInfoDocument( QList<QgsMapLayer *> &layers, const QgsMapSettings &mapSettings,
11361136
const QImage *outputImage, const QString &version ) const
11371137
{
1138-
QStringList queryLayers = mWmsParameters.queryLayersNickname();
1138+
const QStringList queryLayers = mContext.flattenedQueryLayers( );
11391139

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

@@ -1322,8 +1322,39 @@ namespace QgsWms
13221322
{
13231323
QgsWmsParameter param( QgsWmsParameter::LAYER );
13241324
param.mValue = queryLayer;
1325-
throw QgsBadRequestException( QgsServiceException::OGC_LAYER_NOT_QUERYABLE,
1326-
param );
1325+
// Check if this layer belongs to a group and the group has any queryable layers
1326+
bool hasGroupAndQueryable { false };
1327+
if ( ! mContext.parameters().queryLayersNickname().contains( queryLayer ) )
1328+
{
1329+
// Find which group this layer belongs to
1330+
const auto &constNicks { mContext.parameters().queryLayersNickname() };
1331+
for ( const auto &ql : constNicks )
1332+
{
1333+
if ( mContext.layerGroups().contains( ql ) )
1334+
{
1335+
const auto &constLayers { mContext.layerGroups()[ql] };
1336+
for ( const auto &ml : constLayers )
1337+
{
1338+
if ( ( ! ml->shortName().isEmpty() && ml->shortName() == queryLayer ) || ( ml->name() == queryLayer ) )
1339+
{
1340+
param.mValue = ql;
1341+
}
1342+
if ( ml->flags().testFlag( QgsMapLayer::Identifiable ) )
1343+
{
1344+
hasGroupAndQueryable = true;
1345+
break;
1346+
}
1347+
}
1348+
break;
1349+
}
1350+
}
1351+
}
1352+
// Only throw if it's not a group or the group has no queryable children
1353+
if ( ! hasGroupAndQueryable )
1354+
{
1355+
throw QgsBadRequestException( QgsServiceException::OGC_LAYER_NOT_QUERYABLE,
1356+
param );
1357+
}
13271358
}
13281359
}
13291360

@@ -2206,7 +2237,7 @@ namespace QgsWms
22062237
exporter.setAttributes( attributes );
22072238
exporter.setIncludeGeometry( withGeometry );
22082239

2209-
for ( const auto feature : features )
2240+
for ( const auto &feature : qgis::as_const( features ) )
22102241
{
22112242
if ( json.right( 1 ).compare( QStringLiteral( "}" ) ) == 0 )
22122243
{
@@ -2219,6 +2250,10 @@ namespace QgsWms
22192250
}
22202251
else // raster layer
22212252
{
2253+
if ( json.right( 1 ).compare( QStringLiteral( "}" ) ) == 0 )
2254+
{
2255+
json.append( QStringLiteral( "," ) );
2256+
}
22222257
json.append( QStringLiteral( "{" ) );
22232258
json.append( QStringLiteral( "\"type\":\"Feature\",\n" ) );
22242259
json.append( QStringLiteral( "\"id\":\"%1\",\n" ).arg( layer->name() ) );

tests/src/python/test_qgsserver_wms_getfeatureinfo.py

+111
Original file line numberDiff line numberDiff line change
@@ -591,6 +591,117 @@ def testGetFeatureInfoPostgresTypes(self):
591591
attribute.get('value')), {
592592
'c': 4.0, 'd': 5.0})
593593

594+
def testGetFeatureInfoGroupedLayers(self):
595+
"""Test that we can get feature info from the top and group layers"""
596+
597+
# areas+and+symbols (not nested)
598+
self.wms_request_compare('GetFeatureInfo',
599+
'&BBOX=52.44095517977704901,10.71171069440170776,52.440955186258563,10.71171070552261817' +
600+
'&CRS=EPSG:4326' +
601+
'&WIDTH=2&HEIGHT=2' +
602+
'&QUERY_LAYERS=areas+and+symbols' +
603+
'&INFO_FORMAT=application/json' +
604+
'&I=0&J=1' +
605+
'&FEATURE_COUNT=10',
606+
'wms_getfeatureinfo_group_name_areas',
607+
'test_project_wms_grouped_layers.qgs')
608+
609+
# areas+and+symbols (nested)
610+
self.wms_request_compare('GetFeatureInfo',
611+
'&BBOX=52.44095517977704901,10.71171069440170776,52.440955186258563,10.71171070552261817' +
612+
'&CRS=EPSG:4326' +
613+
'&WIDTH=2&HEIGHT=2' +
614+
'&QUERY_LAYERS=areas+and+symbols' +
615+
'&INFO_FORMAT=application/json' +
616+
'&I=0&J=1' +
617+
'&FEATURE_COUNT=10',
618+
'wms_getfeatureinfo_group_name_areas',
619+
'test_project_wms_grouped_nested_layers.qgs')
620+
621+
# as-areas-short-name
622+
self.wms_request_compare('GetFeatureInfo',
623+
'&BBOX=52.44095517977704901,10.71171069440170776,52.440955186258563,10.71171070552261817' +
624+
'&CRS=EPSG:4326' +
625+
'&WIDTH=2&HEIGHT=2' +
626+
'&QUERY_LAYERS=as-areas-short-name' +
627+
'&INFO_FORMAT=application/json' +
628+
'&I=0&J=1' +
629+
'&FEATURE_COUNT=10',
630+
'wms_getfeatureinfo_group_name_areas',
631+
'test_project_wms_grouped_nested_layers.qgs')
632+
633+
# Top level: QGIS Server - Grouped Layer
634+
self.wms_request_compare('GetFeatureInfo',
635+
'&BBOX=52.44095517977704901,10.71171069440170776,52.440955186258563,10.71171070552261817' +
636+
'&CRS=EPSG:4326' +
637+
'&WIDTH=2&HEIGHT=2' +
638+
'&QUERY_LAYERS=QGIS+Server+-+Grouped Nested Layer' +
639+
'&INFO_FORMAT=application/json' +
640+
'&I=0&J=1' +
641+
'&FEATURE_COUNT=10',
642+
'wms_getfeatureinfo_group_name_top',
643+
'test_project_wms_grouped_nested_layers.qgs')
644+
645+
# Multiple matches from 2 layer groups
646+
self.wms_request_compare('GetFeatureInfo',
647+
'&BBOX=52.44095517977704901,10.71171069440170776,52.440955186258563,10.71171070552261817' +
648+
'&CRS=EPSG:4326' +
649+
'&WIDTH=2&HEIGHT=2' +
650+
'&QUERY_LAYERS=areas+and+symbols,city+and+district+boundaries' +
651+
'&INFO_FORMAT=application/json' +
652+
'&I=0&J=1' +
653+
'&FEATURE_COUNT=10',
654+
'wms_getfeatureinfo_group_name_areas_cities',
655+
'test_project_wms_grouped_nested_layers.qgs')
656+
657+
# no_query group (nested)
658+
self.wms_request_compare('GetFeatureInfo',
659+
'&BBOX=52.44095517977704901,10.71171069440170776,52.440955186258563,10.71171070552261817' +
660+
'&CRS=EPSG:4326' +
661+
'&WIDTH=2&HEIGHT=2' +
662+
'&QUERY_LAYERS=no_query' +
663+
'&INFO_FORMAT=application/json' +
664+
'&I=0&J=1' +
665+
'&FEATURE_COUNT=10',
666+
'wms_getfeatureinfo_group_no_query',
667+
'test_project_wms_grouped_nested_layers.qgs')
668+
669+
# query_child group (nested)
670+
self.wms_request_compare('GetFeatureInfo',
671+
'&BBOX=52.44095517977704901,10.71171069440170776,52.440955186258563,10.71171070552261817' +
672+
'&CRS=EPSG:4326' +
673+
'&WIDTH=2&HEIGHT=2' +
674+
'&QUERY_LAYERS=query_child' +
675+
'&INFO_FORMAT=application/json' +
676+
'&I=0&J=1' +
677+
'&FEATURE_COUNT=10',
678+
'wms_getfeatureinfo_group_query_child',
679+
'test_project_wms_grouped_nested_layers.qgs')
680+
681+
# child_ok group (nested)
682+
self.wms_request_compare('GetFeatureInfo',
683+
'&BBOX=52.44095517977704901,10.71171069440170776,52.440955186258563,10.71171070552261817' +
684+
'&CRS=EPSG:4326' +
685+
'&WIDTH=2&HEIGHT=2' +
686+
'&QUERY_LAYERS=child_ok' +
687+
'&INFO_FORMAT=application/json' +
688+
'&I=0&J=1' +
689+
'&FEATURE_COUNT=10',
690+
'wms_getfeatureinfo_group_query_child',
691+
'test_project_wms_grouped_nested_layers.qgs')
692+
693+
# as_areas_query_copy == as-areas-short-name-query-copy (nested)
694+
self.wms_request_compare('GetFeatureInfo',
695+
'&BBOX=52.44095517977704901,10.71171069440170776,52.440955186258563,10.71171070552261817' +
696+
'&CRS=EPSG:4326' +
697+
'&WIDTH=2&HEIGHT=2' +
698+
'&QUERY_LAYERS=as-areas-short-name-query-copy' +
699+
'&INFO_FORMAT=application/json' +
700+
'&I=0&J=1' +
701+
'&FEATURE_COUNT=10',
702+
'wms_getfeatureinfo_group_query_child',
703+
'test_project_wms_grouped_nested_layers.qgs')
704+
594705

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

tests/testdata/qgis_server/getprojectsettings.txt

+2-2
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ Content-Type: text/xml; charset=utf-8
283283
<Attribute precision="0" type="QString" editType="TextEdit" typeName="String" name="utf8nameè" comment="" length="13"/>
284284
</Attributes>
285285
</Layer>
286-
<Layer mutuallyExclusive="0" visible="1">
286+
<Layer mutuallyExclusive="0" visible="1" queryable="1">
287287
<Name>group_name</Name>
288288
<Title>Group title</Title>
289289
<Abstract>Group abstract</Abstract>
@@ -329,7 +329,7 @@ Content-Type: text/xml; charset=utf-8
329329
</Attributes>
330330
</Layer>
331331
</Layer>
332-
<Layer mutuallyExclusive="0" visible="1">
332+
<Layer mutuallyExclusive="0" queryable="0" visible="1">
333333
<Name>groupwithoutshortname</Name>
334334
<Title>groupwithoutshortname</Title>
335335
<CRS>CRS:84</CRS>

0 commit comments

Comments
 (0)