Skip to content

Commit

Permalink
Merge pull request #52051 from pathmapper/full_pagination
Browse files Browse the repository at this point in the history
[server/OAPIF] Add full pagination for collection items HTML page
  • Loading branch information
elpaso committed Mar 29, 2023
2 parents a533349 + dc04985 commit 184b5e4
Show file tree
Hide file tree
Showing 18 changed files with 4,244 additions and 25 deletions.
14 changes: 8 additions & 6 deletions resources/server/api/ogc/templates/wfs3/getFeatures.html
Expand Up @@ -2,16 +2,18 @@
{% include "header.html" %}

<div class="row">
<nav aria-label="Page navigation example">
<nav aria-label="Page navigation">
<ul class="pagination">
{% for link in links_filter( links, "rel", "prev" ) %}
<li class="page-item"><a class="page-link" href="{{ link.href }}">Previous</a></li>
<li class="page-item"><a class="page-link" href="{{ link.href }}">{{ link.title }}</a></li>
{% endfor %}
{% for pageitem in metadata.pagination %}
<li class="{{ pageitem.class }}"><a class="page-link" {% if existsIn(pageitem, "href" ) %}
href="{{ pageitem.href }}" {% endif %}>{{ pageitem.title }}</a>
</li>
{% endfor %}
<!-- TODO: full pagination: li class="page-item"><a class="page-link" href="#">1</a></li>
<li class="page-item"><a class="page-link" href="#">2</a></li>
<li class="page-item"><a class="page-link" href="#">3</a></li-->
{% for link in links_filter( links, "rel", "next" ) %}
<li class="page-item"><a class="page-link" href="{{ link.href }}">Next</a></li>
<li class="page-item"><a class="page-link" href="{{ link.href }}">{{ link.title }}</a></li>
{% endfor %}
</ul>
</nav>
Expand Down
102 changes: 87 additions & 15 deletions src/server/services/wfs3/qgswfs3handlers.cpp
Expand Up @@ -1399,23 +1399,94 @@ void QgsWfs3CollectionsItemsHandler::handleRequest( const QgsServerApiContext &c
}
}

// Add prev - next links
if ( offset != 0 )
{
json prevLink = selfLink;
prevLink["href"] = cleanedUrlAsString.toStdString() + QStringLiteral( "offset=%1&limit=%2" ).arg( std::max<long>( 0, offset - limit ) ).arg( limit ).toStdString();
prevLink["rel"] = "prev";
prevLink["title"] = "Previous page";
data["links"].push_back( prevLink );
}
// Pagination metadata
json pagination = json::array();

if ( limit + offset < matchedFeaturesCount )
if ( limit != 0 )
{
json nextLink = selfLink;
nextLink["href"] = cleanedUrlAsString.toStdString() + QStringLiteral( "offset=%1&limit=%2" ).arg( std::min<long>( matchedFeaturesCount, limit + offset ) ).arg( limit ).toStdString();
nextLink["rel"] = "next";
nextLink["title"] = "Next page";
data["links"].push_back( nextLink );
// Add prev - next links
json prevLink;
if ( offset != 0 )
{
prevLink = selfLink;
prevLink["href"] = cleanedUrlAsString.toStdString() + QStringLiteral( "offset=%1&limit=%2" ).arg( std::max<long>( 0, offset - limit ) ).arg( limit ).toStdString();
prevLink["rel"] = "prev";
prevLink["title"] = "Previous page";
data["links"].push_back( prevLink );
}

json nextLink;
if ( limit + offset < matchedFeaturesCount )
{
nextLink = selfLink;
nextLink["href"] = cleanedUrlAsString.toStdString() + QStringLiteral( "offset=%1&limit=%2" ).arg( std::min<long>( matchedFeaturesCount, limit + offset ) ).arg( limit ).toStdString();
nextLink["rel"] = "next";
nextLink["title"] = "Next page";
data["links"].push_back( nextLink );
}

// Pagination
if ( matchedFeaturesCount - limit > 0 )
{
const int totalPages { static_cast<int>( std::ceil( static_cast<float>( matchedFeaturesCount ) / static_cast<float>( limit ) ) ) };
const int currentPage { static_cast<int>( offset / limit + 1 ) };
const std::string currentPageLink { selfLink["href"] };

std::string prevPageLink;
if ( prevLink.contains( std::string{ "href" } ) )
{
prevPageLink = prevLink["href"];
}

std::string nextPageLink;
if ( nextLink.contains( std::string{ "href" } ) )
{
nextPageLink = nextLink["href"];
}

const std::string firstPageLink { cleanedUrlAsString.toStdString() + QStringLiteral( "offset=0&limit=%1" ).arg( limit ).toStdString() };
const std::string lastPageLink { cleanedUrlAsString.toStdString() + QStringLiteral( "offset=%1&limit=%2" ).arg( totalPages * limit - limit ).arg( limit ).toStdString() };

if ( currentPage != 1 )
{
pagination.push_back( {{ "title", "1" }, { "href", firstPageLink }, { "class", "page-item" }} ) ;
}
if ( currentPage > 3 )
{
pagination.push_back( {{ "title", "\u2026" }, { "class", "page-item disabled" }} ) ;
}
if ( currentPage > 2 )
{
pagination.push_back( {{ "title", std::to_string( currentPage - 1 ) }, { "href", prevPageLink }, { "class", "page-item" }} ) ;
}
pagination.push_back( {{ "title", std::to_string( currentPage ) }, { "href", currentPageLink }, { "class", "page-item active" }} ) ;
if ( currentPage < totalPages - 1 )
{
pagination.push_back( {{ "title", std::to_string( currentPage + 1 ) }, { "href", nextPageLink }, { "class", "page-item" }} ) ;
}
if ( currentPage < totalPages - 2 )
{
pagination.push_back( {{ "title", "\u2026" }, { "class", "page-item disabled" }} ) ;
}
if ( currentPage != totalPages )
{
pagination.push_back( {{ "title", std::to_string( totalPages ) }, { "href", lastPageLink }, { "class", "page-item" }} ) ;
}

// Add first - last links
// Since we are having them ready, not mandatory by the spec but allowed
json firstLink = selfLink;
firstLink["href"] = firstPageLink;
firstLink["rel"] = "first";
firstLink["title"] = "First page";
data["links"].push_back( firstLink );

json lastLink = selfLink;
lastLink["href"] = lastPageLink;
lastLink["rel"] = "last";
lastLink["title"] = "Last page";
data["links"].push_back( lastLink );
}
}

json navigation = json::array();
Expand All @@ -1431,6 +1502,7 @@ void QgsWfs3CollectionsItemsHandler::handleRequest( const QgsServerApiContext &c
"geojsonUrl", href( context, "/",
QgsServerOgcApi::contentTypeToExtension( QgsServerOgcApi::ContentType::GEOJSON ) )
},
{ "pagination", pagination },
{ "navigation", navigation }
};

Expand Down
51 changes: 51 additions & 0 deletions tests/src/python/test_qgsserver_api.py
Expand Up @@ -540,6 +540,48 @@ def test_wfs3_collection_items_html(self):
self.compareApi(request, project,
'test_wfs3_collections_items_testlayer_èé.html')

def test_wfs3_collection_items_html_limit_0(self):
"""Test WFS3 API items limit=0"""
project = QgsProject()
project.read(os.path.join(self.temporary_path, 'qgis_server', 'test_project_api.qgs'))
request = QgsBufferServerRequest(
'http://server.qgis.org/wfs3/collections/testlayer%20èé/items.html?limit=0')
self.compareApi(request, project,
'test_wfs3_collections_items_testlayer_èé_1.html')

def test_wfs3_collection_items_html_pagination(self):
"""Test WFS3 API items pagination"""
project = QgsProject()
project.read(os.path.join(self.temporary_path, 'qgis_server', 'test_project_wms_grouped_nested_layers.qgs'))
request = QgsBufferServerRequest(
'http://server.qgis.org/wfs3/collections/as-areas-short-name/items.html?offset=0&limit=6')
self.compareApi(request, project,
'test_wfs3_collections_items_as_areas_short_name_1.html')
request = QgsBufferServerRequest(
'http://server.qgis.org/wfs3/collections/as-areas-short-name/items.html?offset=6&limit=6')
self.compareApi(request, project,
'test_wfs3_collections_items_as_areas_short_name_2.html')
request = QgsBufferServerRequest(
'http://server.qgis.org/wfs3/collections/as-areas-short-name/items.html?offset=12&limit=6')
self.compareApi(request, project,
'test_wfs3_collections_items_as_areas_short_name_3.html')
request = QgsBufferServerRequest(
'http://server.qgis.org/wfs3/collections/as-areas-short-name/items.html?offset=18&limit=6')
self.compareApi(request, project,
'test_wfs3_collections_items_as_areas_short_name_4.html')
request = QgsBufferServerRequest(
'http://server.qgis.org/wfs3/collections/as-areas-short-name/items.html?offset=24&limit=6')
self.compareApi(request, project,
'test_wfs3_collections_items_as_areas_short_name_5.html')
request = QgsBufferServerRequest(
'http://server.qgis.org/wfs3/collections/as-areas-short-name/items.html?offset=30&limit=6')
self.compareApi(request, project,
'test_wfs3_collections_items_as_areas_short_name_6.html')
request = QgsBufferServerRequest(
'http://server.qgis.org/wfs3/collections/as-areas-short-name/items.html?offset=36&limit=6')
self.compareApi(request, project,
'test_wfs3_collections_items_as_areas_short_name_7.html')

def test_wfs3_collection_items_crs(self):
"""Test WFS3 API items with CRS"""
project = QgsProject()
Expand Down Expand Up @@ -636,6 +678,15 @@ def test_wfs3_collection_items_limit_offset_2(self):
self.compareApi(
request, project, 'test_wfs3_collections_items_testlayer_èé_limit_1_offset_2.json')

def test_wfs3_collection_items_limit_2(self):
"""Test WFS3 API limit 2"""
project = QgsProject()
project.read(os.path.join(self.temporary_path, 'qgis_server', 'test_project_api.qgs'))
request = QgsBufferServerRequest(
'http://server.qgis.org/wfs3/collections/testlayer%20èé/items?limit=2')
self.compareApi(
request, project, 'test_wfs3_collections_items_testlayer_èé_limit_2.json')

def test_wfs3_collection_items_bbox(self):
"""Test WFS3 API bbox"""
project = QgsProject()
Expand Down
Expand Up @@ -989,6 +989,18 @@ Content-Type: application/geo+json
"rel": "next",
"title": "Next page",
"type": "application/geo+json"
},
{
"href": "http://server.qgis.org/wfs3/collections/as-areas-short-name/items?crs=http%3A%2F%2Fwww.opengis.net%2Fdef%2Fcrs%2FEPSG%2F0%2F3857&offset=0&limit=10",
"rel": "first",
"title": "First page",
"type": "application/geo+json"
},
{
"href": "http://server.qgis.org/wfs3/collections/as-areas-short-name/items?crs=http%3A%2F%2Fwww.opengis.net%2Fdef%2Fcrs%2FEPSG%2F0%2F3857&offset=30&limit=10",
"rel": "last",
"title": "Last page",
"type": "application/geo+json"
}
],
"numberMatched": 38,
Expand Down
Expand Up @@ -989,6 +989,18 @@ Content-Type: application/geo+json
"rel": "next",
"title": "Next page",
"type": "application/geo+json"
},
{
"href": "http://server.qgis.org/wfs3/collections/as-areas-short-name/items?crs=http%3A%2F%2Fwww.opengis.net%2Fdef%2Fcrs%2FEPSG%2F0%2F4326&offset=0&limit=10",
"rel": "first",
"title": "First page",
"type": "application/geo+json"
},
{
"href": "http://server.qgis.org/wfs3/collections/as-areas-short-name/items?crs=http%3A%2F%2Fwww.opengis.net%2Fdef%2Fcrs%2FEPSG%2F0%2F4326&offset=30&limit=10",
"rel": "last",
"title": "Last page",
"type": "application/geo+json"
}
],
"numberMatched": 38,
Expand Down

0 comments on commit 184b5e4

Please sign in to comment.