Skip to content

Commit d87bd22

Browse files
committed
[server] Allow WMS GetFeatureInfo on root layer and groups
cherry-picking commit 0e8b90d from 3_6
1 parent 4ffed0f commit d87bd22

16 files changed

+4605
-14
lines changed

src/server/services/wms/qgswmsgetcapabilities.cpp

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -822,6 +822,15 @@ namespace QgsWms
822822
layerParentElem.appendChild( treeNameElem );
823823
}
824824

825+
if ( hasQueryableChildren( projectLayerTreeRoot, QgsServerProjectUtils::wmsRestrictedLayers( *project ) ) )
826+
{
827+
layerParentElem.setAttribute( QStringLiteral( "queryable" ), QStringLiteral( "1" ) );
828+
}
829+
else
830+
{
831+
layerParentElem.setAttribute( QStringLiteral( "queryable" ), QStringLiteral( "0" ) );
832+
}
833+
825834
appendLayersFromTreeGroup( doc, layerParentElem, serverIface, project, version, request, projectLayerTreeRoot, projectSettings );
826835

827836
combineExtentAndCrsOfGroupChildren( doc, layerParentElem, project, true );
@@ -843,7 +852,7 @@ namespace QgsWms
843852
{
844853
bool useLayerIds = QgsServerProjectUtils::wmsUseLayerIds( *project );
845854
bool siaFormat = QgsServerProjectUtils::wmsInfoFormatSia2045( *project );
846-
QStringList restrictedLayers = QgsServerProjectUtils::wmsRestrictedLayers( *project );
855+
const QStringList restrictedLayers = QgsServerProjectUtils::wmsRestrictedLayers( *project );
847856

848857
QList< QgsLayerTreeNode * > layerTreeGroupChildren = layerTreeGroup->children();
849858
for ( int i = 0; i < layerTreeGroupChildren.size(); ++i )
@@ -910,6 +919,16 @@ namespace QgsWms
910919
layerElem.appendChild( treeNameElem );
911920
}
912921

922+
// Set queryable if any of the children are
923+
if ( hasQueryableChildren( treeNode, restrictedLayers ) )
924+
{
925+
layerElem.setAttribute( QStringLiteral( "queryable" ), QStringLiteral( "1" ) );
926+
}
927+
else
928+
{
929+
layerElem.setAttribute( QStringLiteral( "queryable" ), QStringLiteral( "0" ) );
930+
}
931+
913932
appendLayersFromTreeGroup( doc, layerElem, serverIface, project, version, request, treeGroupChild, projectSettings );
914933

915934
combineExtentAndCrsOfGroupChildren( doc, layerElem, project );
@@ -1812,6 +1831,26 @@ namespace QgsWms
18121831
}
18131832
}
18141833

1834+
bool hasQueryableChildren( const QgsLayerTreeNode *childNode, const QStringList &wmsRestrictedLayers )
1835+
{
1836+
if ( childNode->nodeType() == QgsLayerTreeNode::NodeGroup )
1837+
{
1838+
for ( int j = 0; j < childNode->children().size(); ++j )
1839+
{
1840+
if ( hasQueryableChildren( childNode->children().at( j ), wmsRestrictedLayers ) )
1841+
return true;
1842+
}
1843+
return false;
1844+
}
1845+
else if ( childNode->nodeType() == QgsLayerTreeNode::NodeLayer )
1846+
{
1847+
const auto treeLayer { static_cast<const QgsLayerTreeLayer *>( childNode ) };
1848+
const auto l { treeLayer->layer() };
1849+
return ! wmsRestrictedLayers.contains( l->name() ) && l->flags().testFlag( QgsMapLayer::Identifiable );
1850+
}
1851+
return false;
1852+
}
1853+
18151854

18161855
} // namespace QgsWms
18171856

src/server/services/wms/qgswmsgetcapabilities.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ namespace QgsWms
8383
QDomDocument getCapabilities( QgsServerInterface *serverIface, const QgsProject *project,
8484
const QString &version, const QgsServerRequest &request,
8585
bool projectSettings );
86+
87+
bool hasQueryableChildren( const QgsLayerTreeNode *childNode, const QStringList &wmsRestrictedLayers );
8688
} // namespace QgsWms
8789

8890
#endif

src/server/services/wms/qgswmsrenderer.cpp

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -886,7 +886,8 @@ namespace QgsWms
886886
{
887887
// Verifying Mandatory parameters
888888
// The QUERY_LAYERS parameter is Mandatory
889-
QStringList queryLayers = mWmsParameters.queryLayersNickname();
889+
QStringList queryLayers = flattenedQueryLayers();
890+
890891
if ( queryLayers.isEmpty() )
891892
{
892893
QString msg = QObject::tr( "QUERY_LAYERS parameter is required for GetFeatureInfo" );
@@ -1183,7 +1184,8 @@ namespace QgsWms
11831184
QDomDocument QgsRenderer::featureInfoDocument( QList<QgsMapLayer *> &layers, const QgsMapSettings &mapSettings,
11841185
const QImage *outputImage, const QString &version ) const
11851186
{
1186-
QStringList queryLayers = mWmsParameters.queryLayersNickname();
1187+
1188+
const QStringList queryLayers = flattenedQueryLayers( );
11871189

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

@@ -1366,8 +1368,40 @@ namespace QgsWms
13661368
}
13671369
else if ( ( validLayer && !queryableLayer ) || ( !validLayer && mLayerGroups.contains( queryLayer ) ) )
13681370
{
1369-
QString msg = QObject::tr( "Layer '%1' is not queryable" ).arg( queryLayer );
1370-
throw QgsBadRequestException( QStringLiteral( "LayerNotQueryable" ), msg );
1371+
auto queryLayerName { queryLayer };
1372+
// Check if this layer belongs to a group and the group has any queryable layers
1373+
bool hasGroupAndQueryable { false };
1374+
if ( ! mWmsParameters.queryLayersNickname().contains( queryLayer ) )
1375+
{
1376+
// Find which group this layer belongs to
1377+
const auto &constNicks { mWmsParameters.queryLayersNickname() };
1378+
for ( const auto &ql : constNicks )
1379+
{
1380+
if ( mLayerGroups.contains( ql ) )
1381+
{
1382+
const auto &constLayers { mLayerGroups[ql] };
1383+
for ( const auto &ml : constLayers )
1384+
{
1385+
if ( ( ! ml->shortName().isEmpty() && ml->shortName() == queryLayer ) || ( ml->name() == queryLayer ) )
1386+
{
1387+
queryLayerName = ql;
1388+
}
1389+
if ( ml->flags().testFlag( QgsMapLayer::Identifiable ) )
1390+
{
1391+
hasGroupAndQueryable = true;
1392+
break;
1393+
}
1394+
}
1395+
break;
1396+
}
1397+
}
1398+
}
1399+
// Only throw if it's not a group or the group has no queryable children
1400+
if ( ! hasGroupAndQueryable )
1401+
{
1402+
const QString msg { QObject::tr( "The layer '%1' is not queryable." ).arg( queryLayerName ) };
1403+
throw QgsBadRequestException( QStringLiteral( "LayerNotQueryable" ), msg );
1404+
}
13711405
}
13721406
}
13731407

@@ -3138,5 +3172,32 @@ namespace QgsWms
31383172
{
31393173
std::unique_ptr<QImage> tmpImage( createImage( 1, 1, false ) );
31403174
return tmpImage->dotsPerMeterX() / 1000.0;
3175+
}
3176+
3177+
QStringList QgsRenderer::flattenedQueryLayers() const
3178+
{
3179+
QStringList result;
3180+
std::function <QStringList( const QString &name )> findLeaves = [ & ]( const QString & name ) -> QStringList
3181+
{
3182+
QStringList _result;
3183+
if ( mLayerGroups.contains( name ) )
3184+
{
3185+
for ( const auto &l : mLayerGroups[ name ] )
3186+
{
3187+
_result.append( findLeaves( l->shortName().isEmpty() ? l->name() : l->shortName() ) );
3188+
}
3189+
}
3190+
else
3191+
{
3192+
_result.append( name );
3193+
}
3194+
return _result;
3195+
};
3196+
for ( const auto &name : mWmsParameters.queryLayersNickname() )
3197+
{
3198+
result.append( findLeaves( name ) );
3199+
}
3200+
return result;
31413201
}
3202+
31423203
} // namespace QgsWms

src/server/services/wms/qgswmsrenderer.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,8 @@ namespace QgsWms
306306
QStringList mRestrictedLayers;
307307
QMap<QString, QgsMapLayer *> mNicknameLayers;
308308
QMap<QString, QList<QgsMapLayer *> > mLayerGroups;
309-
QList<QgsMapLayer *> mTemporaryLayers;
309+
QList<QgsMapLayer *> mTemporaryLayers;
310+
QStringList flattenedQueryLayers() const;
310311

311312
public:
312313

tests/src/python/test_qgsserver_wms_getfeatureinfo.py

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,117 @@ def testGetFeatureInfoPostgresTypes(self):
516516
attribute.get('value')), {
517517
'c': 4.0, 'd': 5.0})
518518

519+
def testGetFeatureInfoGroupedLayers(self):
520+
"""Test that we can get feature info from the top and group layers"""
521+
522+
# areas+and+symbols (not nested)
523+
self.wms_request_compare('GetFeatureInfo',
524+
'&BBOX=52.44095517977704901,10.71171069440170776,52.440955186258563,10.71171070552261817' +
525+
'&CRS=EPSG:4326' +
526+
'&WIDTH=2&HEIGHT=2' +
527+
'&QUERY_LAYERS=areas+and+symbols' +
528+
'&INFO_FORMAT=text/plain' +
529+
'&I=0&J=1' +
530+
'&FEATURE_COUNT=10',
531+
'wms_getfeatureinfo_group_name_areas',
532+
'test_project_wms_grouped_layers.qgs')
533+
534+
# areas+and+symbols (nested)
535+
self.wms_request_compare('GetFeatureInfo',
536+
'&BBOX=52.44095517977704901,10.71171069440170776,52.440955186258563,10.71171070552261817' +
537+
'&CRS=EPSG:4326' +
538+
'&WIDTH=2&HEIGHT=2' +
539+
'&QUERY_LAYERS=areas+and+symbols' +
540+
'&INFO_FORMAT=text/plain' +
541+
'&I=0&J=1' +
542+
'&FEATURE_COUNT=10',
543+
'wms_getfeatureinfo_group_name_areas_nested',
544+
'test_project_wms_grouped_nested_layers.qgs')
545+
546+
# as-areas-short-name
547+
self.wms_request_compare('GetFeatureInfo',
548+
'&BBOX=52.44095517977704901,10.71171069440170776,52.440955186258563,10.71171070552261817' +
549+
'&CRS=EPSG:4326' +
550+
'&WIDTH=2&HEIGHT=2' +
551+
'&QUERY_LAYERS=as-areas-short-name' +
552+
'&INFO_FORMAT=text/plain' +
553+
'&I=0&J=1' +
554+
'&FEATURE_COUNT=10',
555+
'wms_getfeatureinfo_group_name_areas_nested_shortname',
556+
'test_project_wms_grouped_nested_layers.qgs')
557+
558+
# Top level: QGIS Server - Grouped Layer
559+
self.wms_request_compare('GetFeatureInfo',
560+
'&BBOX=52.44095517977704901,10.71171069440170776,52.440955186258563,10.71171070552261817' +
561+
'&CRS=EPSG:4326' +
562+
'&WIDTH=2&HEIGHT=2' +
563+
'&QUERY_LAYERS=QGIS+Server+-+Grouped Nested Layer' +
564+
'&INFO_FORMAT=text/plain' +
565+
'&I=0&J=1' +
566+
'&FEATURE_COUNT=10',
567+
'wms_getfeatureinfo_group_name_top',
568+
'test_project_wms_grouped_nested_layers.qgs')
569+
570+
# Multiple matches from 2 layer groups
571+
self.wms_request_compare('GetFeatureInfo',
572+
'&BBOX=52.44095517977704901,10.71171069440170776,52.440955186258563,10.71171070552261817' +
573+
'&CRS=EPSG:4326' +
574+
'&WIDTH=2&HEIGHT=2' +
575+
'&QUERY_LAYERS=areas+and+symbols,city+and+district+boundaries' +
576+
'&INFO_FORMAT=text/plain' +
577+
'&I=0&J=1' +
578+
'&FEATURE_COUNT=10',
579+
'wms_getfeatureinfo_group_name_areas_cities',
580+
'test_project_wms_grouped_nested_layers.qgs')
581+
582+
# no_query group (nested)
583+
self.wms_request_compare('GetFeatureInfo',
584+
'&BBOX=52.44095517977704901,10.71171069440170776,52.440955186258563,10.71171070552261817' +
585+
'&CRS=EPSG:4326' +
586+
'&WIDTH=2&HEIGHT=2' +
587+
'&QUERY_LAYERS=no_query' +
588+
'&INFO_FORMAT=text/plain' +
589+
'&I=0&J=1' +
590+
'&FEATURE_COUNT=10',
591+
'wms_getfeatureinfo_group_no_query',
592+
'test_project_wms_grouped_nested_layers.qgs')
593+
594+
# query_child group (nested)
595+
self.wms_request_compare('GetFeatureInfo',
596+
'&BBOX=52.44095517977704901,10.71171069440170776,52.440955186258563,10.71171070552261817' +
597+
'&CRS=EPSG:4326' +
598+
'&WIDTH=2&HEIGHT=2' +
599+
'&QUERY_LAYERS=query_child' +
600+
'&INFO_FORMAT=text/plain' +
601+
'&I=0&J=1' +
602+
'&FEATURE_COUNT=10',
603+
'wms_getfeatureinfo_group_query_child',
604+
'test_project_wms_grouped_nested_layers.qgs')
605+
606+
# child_ok group (nested)
607+
self.wms_request_compare('GetFeatureInfo',
608+
'&BBOX=52.44095517977704901,10.71171069440170776,52.440955186258563,10.71171070552261817' +
609+
'&CRS=EPSG:4326' +
610+
'&WIDTH=2&HEIGHT=2' +
611+
'&QUERY_LAYERS=child_ok' +
612+
'&INFO_FORMAT=text/plain' +
613+
'&I=0&J=1' +
614+
'&FEATURE_COUNT=10',
615+
'wms_getfeatureinfo_group_query_child',
616+
'test_project_wms_grouped_nested_layers.qgs')
617+
618+
# as_areas_query_copy == as-areas-short-name-query-copy (nested)
619+
self.wms_request_compare('GetFeatureInfo',
620+
'&BBOX=52.44095517977704901,10.71171069440170776,52.440955186258563,10.71171070552261817' +
621+
'&CRS=EPSG:4326' +
622+
'&WIDTH=2&HEIGHT=2' +
623+
'&QUERY_LAYERS=as-areas-short-name-query-copy' +
624+
'&INFO_FORMAT=text/plain' +
625+
'&I=0&J=1' +
626+
'&FEATURE_COUNT=10',
627+
'wms_getfeatureinfo_group_query_child',
628+
'test_project_wms_grouped_nested_layers.qgs')
629+
519630

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

tests/testdata/qgis_server/db.gpkg

0 Bytes
Binary file not shown.

tests/testdata/qgis_server/getprojectsettings.txt

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,66 @@ Content-Type: text/xml; charset=utf-8
198198
<Attribute precision="0" type="QString" editType="TextEdit" typeName="String" name="utf8nameè" comment="" length="13"/>
199199
</Attributes>
200200
</Layer>
201-
<Layer mutuallyExclusive="0" visible="1">
201+
<Layer geometryType="Point" queryable="1" displayField="alias_name" visible="1">
202+
<Name>fields_alias</Name>
203+
<Title>A test vector layer</Title>
204+
<Abstract>A test vector layer with unicode òà</Abstract>
205+
<CRS>CRS:84</CRS>
206+
<CRS>EPSG:4326</CRS>
207+
<CRS>EPSG:3857</CRS>
208+
<EX_GeographicBoundingBox>
209+
<westBoundLongitude>8.20346</westBoundLongitude>
210+
<eastBoundLongitude>8.20355</eastBoundLongitude>
211+
<southBoundLatitude>44.9014</southBoundLatitude>
212+
<northBoundLatitude>44.9015</northBoundLatitude>
213+
</EX_GeographicBoundingBox>
214+
<BoundingBox maxy="5.60603e+06" maxx="913215" miny="5.60601e+06" CRS="EPSG:3857" minx="913205"/>
215+
<BoundingBox maxy="8.20355" maxx="44.9015" miny="8.20346" CRS="EPSG:4326" minx="44.9014"/>
216+
<Style>
217+
<Name>default</Name>
218+
<Title>default</Title>
219+
<LegendURL>
220+
<Format>image/png</Format>
221+
<OnlineResource xlink:type="simple" xlink:href="https://www.qgis.org/?*****" xmlns:xlink="http://www.w3.org/1999/xlink"/>
222+
</LegendURL>
223+
</Style>
224+
<TreeName>fields_alias</TreeName>
225+
<Attributes>
226+
<Attribute precision="0" type="qlonglong" editType="TextEdit" typeName="Integer64" alias="alias_id" name="id" comment="" length="10"/>
227+
<Attribute precision="0" type="QString" editType="TextEdit" typeName="String" alias="alias_name" name="name" comment="" length="10"/>
228+
<Attribute precision="0" type="QString" editType="TextEdit" typeName="String" name="utf8nameè" comment="" length="13"/>
229+
</Attributes>
230+
</Layer>
231+
<Layer geometryType="Point" queryable="1" displayField="name" visible="1">
232+
<Name>exclude_attribute</Name>
233+
<Title>A test vector layer</Title>
234+
<Abstract>A test vector layer with unicode òà</Abstract>
235+
<CRS>CRS:84</CRS>
236+
<CRS>EPSG:4326</CRS>
237+
<CRS>EPSG:3857</CRS>
238+
<EX_GeographicBoundingBox>
239+
<westBoundLongitude>8.20346</westBoundLongitude>
240+
<eastBoundLongitude>8.20355</eastBoundLongitude>
241+
<southBoundLatitude>44.9014</southBoundLatitude>
242+
<northBoundLatitude>44.9015</northBoundLatitude>
243+
</EX_GeographicBoundingBox>
244+
<BoundingBox maxy="5.60603e+06" maxx="913215" miny="5.60601e+06" CRS="EPSG:3857" minx="913205"/>
245+
<BoundingBox maxy="8.20355" maxx="44.9015" miny="8.20346" CRS="EPSG:4326" minx="44.9014"/>
246+
<Style>
247+
<Name>default</Name>
248+
<Title>default</Title>
249+
<LegendURL>
250+
<Format>image/png</Format>
251+
<OnlineResource xlink:type="simple" xlink:href="https://www.qgis.org/?*****" xmlns:xlink="http://www.w3.org/1999/xlink"/>
252+
</LegendURL>
253+
</Style>
254+
<TreeName>exclude_attribute</TreeName>
255+
<Attributes>
256+
<Attribute precision="0" type="qlonglong" editType="TextEdit" typeName="Integer64" name="id" comment="" length="10"/>
257+
<Attribute precision="0" type="QString" editType="TextEdit" typeName="String" name="utf8nameè" comment="" length="13"/>
258+
</Attributes>
259+
</Layer>
260+
<Layer mutuallyExclusive="0" visible="1" queryable="1">
202261
<Name>group_name</Name>
203262
<Title>Group title</Title>
204263
<Abstract>Group abstract</Abstract>
@@ -244,7 +303,7 @@ Content-Type: text/xml; charset=utf-8
244303
</Attributes>
245304
</Layer>
246305
</Layer>
247-
<Layer mutuallyExclusive="0" visible="1">
306+
<Layer mutuallyExclusive="0" queryable="0" visible="1">
248307
<Name>groupwithoutshortname</Name>
249308
<Title>groupwithoutshortname</Title>
250309
<CRS>CRS:84</CRS>

0 commit comments

Comments
 (0)