From 9bd23b5ac3ec3e52bda9835f836dd1ada364caaf Mon Sep 17 00:00:00 2001 From: rldhont Date: Fri, 20 Jul 2018 11:00:52 +0200 Subject: [PATCH 01/33] [Server][Feature][needs-docs] Server Cache can be manage by plugins First commit to add a way to manage the QGIS Server cache with plugins. In this commit only GetCapabilities document can be cached by plugins. --- .../auto_generated/qgsserverinterface.sip.in | 13 ++ python/server/server_auto.sip | 6 + src/server/CMakeLists.txt | 2 + src/server/qgsservercachefilter.cpp | 56 +++++++ src/server/qgsservercachefilter.h | 93 +++++++++++ src/server/qgsservercachemanager.cpp | 72 ++++++++ src/server/qgsservercachemanager.h | 111 +++++++++++++ src/server/qgsserverinterface.h | 14 ++ src/server/qgsserverinterfaceimpl.cpp | 13 ++ src/server/qgsserverinterfaceimpl.h | 13 +- .../services/wcs/qgswcsgetcapabilities.cpp | 37 ++++- .../services/wfs/qgswfsgetcapabilities.cpp | 37 ++++- .../wfs/qgswfsgetcapabilities_1_0_0.cpp | 37 ++++- .../services/wms/qgswmsgetcapabilities.cpp | 39 ++++- tests/src/python/CMakeLists.txt | 1 + .../src/python/test_qgsserver_cachemanager.py | 154 ++++++++++++++++++ 16 files changed, 685 insertions(+), 13 deletions(-) create mode 100644 src/server/qgsservercachefilter.cpp create mode 100644 src/server/qgsservercachefilter.h create mode 100644 src/server/qgsservercachemanager.cpp create mode 100644 src/server/qgsservercachemanager.h create mode 100644 tests/src/python/test_qgsserver_cachemanager.py diff --git a/python/server/auto_generated/qgsserverinterface.sip.in b/python/server/auto_generated/qgsserverinterface.sip.in index 2146213ead96..a14c4d67fb3d 100644 --- a/python/server/auto_generated/qgsserverinterface.sip.in +++ b/python/server/auto_generated/qgsserverinterface.sip.in @@ -83,6 +83,19 @@ Register an access control filter virtual QgsAccessControl *accessControls() const = 0; %Docstring Gets the registered access control filters +%End + + virtual void registerServerCache( QgsServerCacheFilter *serverCache /Transfer/, int priority = 0 ) = 0; +%Docstring +Register a server cache filter + +:param serverCache: the server cache to register +:param priority: the priority used to order them +%End + + virtual QgsServerCacheManager *cacheManager() const = 0; +%Docstring +Gets the registered server cache filters %End virtual QString getEnv( const QString &name ) const = 0; diff --git a/python/server/server_auto.sip b/python/server/server_auto.sip index 7c6a879dc9c0..248908e50494 100644 --- a/python/server/server_auto.sip +++ b/python/server/server_auto.sip @@ -29,3 +29,9 @@ %If ( HAVE_SERVER_PYTHON_PLUGINS ) %Include auto_generated/qgsaccesscontrol.sip %End +%If ( HAVE_SERVER_PYTHON_PLUGINS ) +%Include auto_generated/qgsservercachefilter.sip +%End +%If ( HAVE_SERVER_PYTHON_PLUGINS ) +%Include auto_generated/qgsservercachemanager.sip +%End diff --git a/src/server/CMakeLists.txt b/src/server/CMakeLists.txt index 7722404a945f..247d2dcf4299 100644 --- a/src/server/CMakeLists.txt +++ b/src/server/CMakeLists.txt @@ -73,6 +73,8 @@ IF (WITH_SERVER_PLUGINS) qgsserverfilter.cpp qgsaccesscontrolfilter.cpp qgsaccesscontrol.cpp + qgsservercachefilter.cpp + qgsservercachemanager.cpp ) ENDIF (WITH_SERVER_PLUGINS) diff --git a/src/server/qgsservercachefilter.cpp b/src/server/qgsservercachefilter.cpp new file mode 100644 index 000000000000..fe6e8d632381 --- /dev/null +++ b/src/server/qgsservercachefilter.cpp @@ -0,0 +1,56 @@ +/*************************************************************************** + qgsservercachefilter.cpp + ------------------------ + Cache interface for Qgis Server plugins + + begin : 2018-07-05 + copyright : (C) 2018 by René-Luc D'Hont + email : rldhont at 3liz dot 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 "qgsservercachefilter.h" + +#include + +//! Constructor +QgsServerCacheFilter::QgsServerCacheFilter( const QgsServerInterface *serverInterface ): + mServerInterface( serverInterface ) +{ +} + +//! Returns cached document +QByteArray QgsServerCacheFilter::getCachedDocument( const QgsProject *project, const QgsServerRequest &request, const QString &key ) const +{ + Q_UNUSED( project ); + Q_UNUSED( request ); + Q_UNUSED( key ); + return QByteArray(); +} + +//! Updates or inserts the document in cache +bool QgsServerCacheFilter::setCachedDocument( const QDomDocument *doc, const QgsProject *project, const QgsServerRequest &request, const QString &key ) const +{ + Q_UNUSED( doc ); + Q_UNUSED( project ); + Q_UNUSED( request ); + Q_UNUSED( key ); + return false; +} + +//! Deletes the cached document +bool QgsServerCacheFilter::deleteCachedDocument( const QgsProject *project, const QgsServerRequest &request, const QString &key ) const +{ + Q_UNUSED( project ); + Q_UNUSED( request ); + Q_UNUSED( key ); + return false; +} diff --git a/src/server/qgsservercachefilter.h b/src/server/qgsservercachefilter.h new file mode 100644 index 000000000000..fab907db6061 --- /dev/null +++ b/src/server/qgsservercachefilter.h @@ -0,0 +1,93 @@ +/*************************************************************************** + qgsservercachefilter.h + ------------------------ + Cache interface for Qgis Server plugins + + begin : 2018-07-05 + copyright : (C) 2018 by René-Luc D'Hont + email : rldhont at 3liz dot 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 QGSSERVERCACHEPLUGIN_H +#define QGSSERVERCACHEPLUGIN_H + +#include +#include +#include +#include "qgsproject.h" +#include "qgsserverrequest.h" +#include "qgis_server.h" +#include "qgis_sip.h" + +SIP_IF_MODULE( HAVE_SERVER_PYTHON_PLUGINS ) + +class QgsServerInterface; + + +/** + * \ingroup server + * \class QgsServerCacheFilter + * \brief Class defining cache interface for QGIS Server plugins. + */ +class SERVER_EXPORT QgsServerCacheFilter +{ + + public: + + /** + * Constructor + * QgsServerInterface passed to plugins constructors + * and must be passed to QgsServerCacheFilter instances. + */ + QgsServerCacheFilter( const QgsServerInterface *serverInterface ); + + virtual ~QgsServerCacheFilter() = default; + + /** + * Returns cached document (or 0 if document not in cache) like capabilities + * \param project the project used to generate the document to provide path + * \param request the request used to generate the document to provider parameters or data + * \param key the key provided by the access control to identify differents documents for the same request + * \returns QByteArray of the cached document or an empty one if no corresponding document found + */ + virtual QByteArray getCachedDocument( const QgsProject *project, const QgsServerRequest &request, const QString &key ) const; + + /** + * Updates or inserts the document in cache like capabilities + * \param doc the document to cache + * \param project the project used to generate the document to provide path + * \param request the request used to generate the document to provider parameters or data + * \param key the key provided by the access control to identify differents documents for the same request + * \returns true if the document has been cached + */ + virtual bool setCachedDocument( const QDomDocument *doc, const QgsProject *project, const QgsServerRequest &request, const QString &key ) const; + + /** + * Deletes the cached document + * \param project the project used to generate the document to provide path + * \param request the request used to generate the document to provider parameters or data + * \param key the key provided by the access control to identify differents documents for the same request + * \returns true if the document has been deleted + */ + virtual bool deleteCachedDocument( const QgsProject *project, const QgsServerRequest &request, const QString &key ) const; + + private: + + //! The server interface + const QgsServerInterface *mServerInterface = nullptr; + +}; + +//! The registry definition +typedef QMultiMap QgsServerCacheFilterMap; + +#endif // QGSSERVERSECURITY_H diff --git a/src/server/qgsservercachemanager.cpp b/src/server/qgsservercachemanager.cpp new file mode 100644 index 000000000000..f044f161d6bb --- /dev/null +++ b/src/server/qgsservercachemanager.cpp @@ -0,0 +1,72 @@ +/*************************************************************************** + qgsservercachemanager.cpp + ------------------------- + + begin : 2018-07-05 + copyright : (C) 2018 by René-Luc D'Hont + email : rldhont at 3liz dot 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 "qgsservercachemanager.h" + +//! Returns cached document (or 0 if document not in cache) like capabilities +const QDomDocument *QgsServerCacheManager::getCachedDocument( const QgsProject *project, const QgsServerRequest &request, const QString &key ) const +{ + QgsServerCacheFilterMap::const_iterator scIterator; + for ( scIterator = mPluginsServerCaches->constBegin(); scIterator != mPluginsServerCaches->constEnd(); ++scIterator ) + { + QByteArray content = scIterator.value()->getCachedDocument( project, request, key ); + if ( !content.isEmpty() ) + { + QDomDocument doc; + if ( doc.setContent( content ) ) + { + return &doc; + } + } + } + return nullptr; +} + +//! Updates or inserts the document in cache like capabilities +bool QgsServerCacheManager::setCachedDocument( const QDomDocument *doc, const QgsProject *project, const QgsServerRequest &request, const QString &key ) const +{ + QgsServerCacheFilterMap::const_iterator scIterator; + for ( scIterator = mPluginsServerCaches->constBegin(); scIterator != mPluginsServerCaches->constEnd(); ++scIterator ) + { + if ( scIterator.value()->setCachedDocument( doc, project, request, key ) ) + { + return true; + } + } + return false; +} + +//! Deletes the cached document +bool QgsServerCacheManager::deleteCachedDocument( const QgsProject *project, const QgsServerRequest &request, const QString &key ) const +{ + QgsServerCacheFilterMap::const_iterator scIterator; + for ( scIterator = mPluginsServerCaches->constBegin(); scIterator != mPluginsServerCaches->constEnd(); ++scIterator ) + { + if ( scIterator.value()->deleteCachedDocument( project, request, key ) ) + { + return true; + } + } + return false; +} + +//! Register a new access control filter +void QgsServerCacheManager::registerServerCache( QgsServerCacheFilter *serverCache, int priority ) +{ + mPluginsServerCaches->insert( priority, serverCache ); +} diff --git a/src/server/qgsservercachemanager.h b/src/server/qgsservercachemanager.h new file mode 100644 index 000000000000..60912ae277f0 --- /dev/null +++ b/src/server/qgsservercachemanager.h @@ -0,0 +1,111 @@ +/*************************************************************************** + qgsservercachemanager.h + ----------------------- + + begin : 2018-07-05 + copyright : (C) 2018 by René-Luc D'Hont + email : rldhont at 3liz dot 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 QGSSERVERCACHEMANAGER_H +#define QGSSERVERCACHEMANAGER_H + +#include "qgsservercachefilter.h" +#include "qgsserverrequest.h" + +#include +#include +#include "qgsproject.h" +#include "qgis_server.h" +#include "qgis_sip.h" + +SIP_IF_MODULE( HAVE_SERVER_PYTHON_PLUGINS ) + +class QgsServerCachePlugin; + + +/** + * \ingroup server + * \class QgsServerCacheManager + * \brief A helper class that centralizes caches accesses given by all the server cache filter plugins. + * \since QGIS 3.4 + */ +class SERVER_EXPORT QgsServerCacheManager +{ +#ifdef SIP_RUN +#include "qgsservercachefilter.h" +#endif + + public: + //! Constructor + QgsServerCacheManager() + { + mPluginsServerCaches = new QgsServerCacheFilterMap(); + mResolved = false; + } + + //! Constructor + QgsServerCacheManager( const QgsServerCacheManager © ) + { + mPluginsServerCaches = new QgsServerCacheFilterMap( *copy.mPluginsServerCaches ); + mResolved = copy.mResolved; + } + + + ~QgsServerCacheManager() + { + delete mPluginsServerCaches; + } + + /** + * Returns cached document (or 0 if document not in cache) like capabilities + * \param project the project used to generate the document to provide path + * \param request the request used to generate the document to provider parameters or data + * \param key the key provided by the access control to identify differents documents for the same request + * \returns the cached document or 0 if no corresponding document found + */ + const QDomDocument *getCachedDocument( const QgsProject *project, const QgsServerRequest &request, const QString &key ) const; + + /** + * Updates or inserts the document in cache like capabilities + * \param doc the document to cache + * \param project the project used to generate the document to provide path + * \param request the request used to generate the document to provider parameters or data + * \param key the key provided by the access control to identify differents documents for the same request + * \returns true if the document has been cached + */ + bool setCachedDocument( const QDomDocument *doc, const QgsProject *project, const QgsServerRequest &request, const QString &key ) const; + + /** + * Deletes the cached document + * \param project the project used to generate the document to provide path + * \param request the request used to generate the document to provider parameters or data + * \param key the key provided by the access control to identify differents documents for the same request + * \returns true if the document has been deleted + */ + bool deleteCachedDocument( const QgsProject *project, const QgsServerRequest &request, const QString &key ) const; + + /** + * Register a server cache filter + * \param serverCache the server cache to add + * \param priority the priority used to define the order + */ + void registerServerCache( QgsServerCacheFilter *serverCache, int priority = 0 ); + + private: + //! The ServerCache plugins registry + QgsServerCacheFilterMap *mPluginsServerCaches = nullptr; + + bool mResolved; +}; + +#endif diff --git a/src/server/qgsserverinterface.h b/src/server/qgsserverinterface.h index 03aa0cc54bf4..a2bfee8ebaea 100644 --- a/src/server/qgsserverinterface.h +++ b/src/server/qgsserverinterface.h @@ -30,9 +30,13 @@ #ifdef HAVE_SERVER_PYTHON_PLUGINS #include "qgsaccesscontrolfilter.h" #include "qgsaccesscontrol.h" +#include "qgsservercachefilter.h" +#include "qgsservercachemanager.h" #else class QgsAccessControl; class QgsAccessControlFilter; +class QgsServerCacheManager; +class QgsServerCacheFilter; #endif #include "qgsserviceregistry.h" #include "qgis_server.h" @@ -118,6 +122,16 @@ class SERVER_EXPORT QgsServerInterface //! Gets the registered access control filters virtual QgsAccessControl *accessControls() const = 0; + /** + * Register a server cache filter + * \param serverCache the server cache to register + * \param priority the priority used to order them + */ + virtual void registerServerCache( QgsServerCacheFilter *serverCache SIP_TRANSFER, int priority = 0 ) = 0; + + //! Gets the registered server cache filters + virtual QgsServerCacheManager *cacheManager() const = 0; + //! Returns an enrironment variable, used to pass environment variables to Python virtual QString getEnv( const QString &name ) const = 0; diff --git a/src/server/qgsserverinterfaceimpl.cpp b/src/server/qgsserverinterfaceimpl.cpp index de7832a105d5..ddbcdb8622ac 100644 --- a/src/server/qgsserverinterfaceimpl.cpp +++ b/src/server/qgsserverinterfaceimpl.cpp @@ -29,8 +29,10 @@ QgsServerInterfaceImpl::QgsServerInterfaceImpl( QgsCapabilitiesCache *capCache, mRequestHandler = nullptr; #ifdef HAVE_SERVER_PYTHON_PLUGINS mAccessControls = new QgsAccessControl(); + mCacheManager = new QgsServerCacheManager(); #else mAccessControls = nullptr; + mCacheManager = nullptr; #endif } @@ -84,6 +86,17 @@ void QgsServerInterfaceImpl::registerAccessControl( QgsAccessControlFilter *acce #endif } +//! Register a new access control filter +void QgsServerInterfaceImpl::registerServerCache( QgsServerCacheFilter *serverCache, int priority ) +{ +#ifdef HAVE_SERVER_PYTHON_PLUGINS + mCacheManager->registerServerCache( serverCache, priority ); +#else + Q_UNUSED( serverCache ); + Q_UNUSED( priority ); +#endif +} + void QgsServerInterfaceImpl::removeConfigCacheEntry( const QString &path ) { diff --git a/src/server/qgsserverinterfaceimpl.h b/src/server/qgsserverinterfaceimpl.h index 39f19b4ff4fc..a8a7e4b973f5 100644 --- a/src/server/qgsserverinterfaceimpl.h +++ b/src/server/qgsserverinterfaceimpl.h @@ -50,8 +50,8 @@ class QgsServerInterfaceImpl : public QgsServerInterface QgsRequestHandler *requestHandler() override { return mRequestHandler; } void registerFilter( QgsServerFilter *filter, int priority = 0 ) override; QgsServerFiltersMap filters() override { return mFilters; } + //! Register an access control filter - // void registerAccessControl( QgsAccessControlFilter *accessControl, int priority = 0 ) override; /** @@ -59,6 +59,16 @@ class QgsServerInterfaceImpl : public QgsServerInterface * \returns the access control helper */ QgsAccessControl *accessControls() const override { return mAccessControls; } + + //! Register a server cache filter + void registerServerCache( QgsServerCacheFilter *serverCache, int priority = 0 ) override; + + /** + * Gets the helper over all the registered server cache filters + * \returns the server cache helper + */ + QgsServerCacheManager *cacheManager() const override { return mCacheManager; } + QString getEnv( const QString &name ) const override; QString configFilePath() override { return mConfigFilePath; } void setConfigFilePath( const QString &configFilePath ) override; @@ -74,6 +84,7 @@ class QgsServerInterfaceImpl : public QgsServerInterface QString mConfigFilePath; QgsServerFiltersMap mFilters; QgsAccessControl *mAccessControls = nullptr; + QgsServerCacheManager *mCacheManager = nullptr; QgsCapabilitiesCache *mCapabilitiesCache = nullptr; QgsRequestHandler *mRequestHandler = nullptr; QgsServiceRegistry *mServiceRegistry = nullptr; diff --git a/src/server/services/wcs/qgswcsgetcapabilities.cpp b/src/server/services/wcs/qgswcsgetcapabilities.cpp index 1020205da281..aa760ec1c377 100644 --- a/src/server/services/wcs/qgswcsgetcapabilities.cpp +++ b/src/server/services/wcs/qgswcsgetcapabilities.cpp @@ -37,10 +37,43 @@ namespace QgsWcs void writeGetCapabilities( QgsServerInterface *serverIface, const QgsProject *project, const QString &version, const QgsServerRequest &request, QgsServerResponse &response ) { - QDomDocument doc = createGetCapabilitiesDocument( serverIface, project, version, request ); + QStringList cacheKeyList; + bool cache = true; + + QgsAccessControl *accessControl = serverIface->accessControls(); + if ( accessControl ) + cache = accessControl->fillCacheKey( cacheKeyList ); + + QDomDocument doc; + QString cacheKey = cacheKeyList.join( QStringLiteral( "-" ) ); + const QDomDocument *capabilitiesDocument; + + QgsServerCacheManager *cacheManager = serverIface->cacheManager(); + if ( cacheManager && cache ) + { + capabilitiesDocument = cacheManager->getCachedDocument( project, request, cacheKey ); + } + + if ( !capabilitiesDocument ) //capabilities xml not in cache. Create a new one + { + doc = createGetCapabilitiesDocument( serverIface, project, version, request ); + + if ( cache && cacheManager ) + { + if ( cacheManager->setCachedDocument( &doc, project, request, cacheKey ) ) + { + capabilitiesDocument = cacheManager->getCachedDocument( project, request, cacheKey ); + } + } + if ( !capabilitiesDocument ) + { + doc = doc.cloneNode().toDocument(); + capabilitiesDocument = &doc; + } + } response.setHeader( "Content-Type", "text/xml; charset=utf-8" ); - response.write( doc.toByteArray() ); + response.write( capabilitiesDocument->toByteArray() ); } diff --git a/src/server/services/wfs/qgswfsgetcapabilities.cpp b/src/server/services/wfs/qgswfsgetcapabilities.cpp index df63ed486a5f..a628e49c1b56 100644 --- a/src/server/services/wfs/qgswfsgetcapabilities.cpp +++ b/src/server/services/wfs/qgswfsgetcapabilities.cpp @@ -41,10 +41,43 @@ namespace QgsWfs void writeGetCapabilities( QgsServerInterface *serverIface, const QgsProject *project, const QString &version, const QgsServerRequest &request, QgsServerResponse &response ) { - QDomDocument doc = createGetCapabilitiesDocument( serverIface, project, version, request ); + QStringList cacheKeyList; + bool cache = true; + + QgsAccessControl *accessControl = serverIface->accessControls(); + if ( accessControl ) + cache = accessControl->fillCacheKey( cacheKeyList ); + + QDomDocument doc; + QString cacheKey = cacheKeyList.join( QStringLiteral( "-" ) ); + const QDomDocument *capabilitiesDocument; + + QgsServerCacheManager *cacheManager = serverIface->cacheManager(); + if ( cacheManager && cache ) + { + capabilitiesDocument = cacheManager->getCachedDocument( project, request, cacheKey ); + } + + if ( !capabilitiesDocument ) //capabilities xml not in cache. Create a new one + { + doc = createGetCapabilitiesDocument( serverIface, project, version, request ); + + if ( cache && cacheManager ) + { + if ( cacheManager->setCachedDocument( &doc, project, request, cacheKey ) ) + { + capabilitiesDocument = cacheManager->getCachedDocument( project, request, cacheKey ); + } + } + if ( !capabilitiesDocument ) + { + doc = doc.cloneNode().toDocument(); + capabilitiesDocument = &doc; + } + } response.setHeader( "Content-Type", "text/xml; charset=utf-8" ); - response.write( doc.toByteArray() ); + response.write( capabilitiesDocument->toByteArray() ); } diff --git a/src/server/services/wfs/qgswfsgetcapabilities_1_0_0.cpp b/src/server/services/wfs/qgswfsgetcapabilities_1_0_0.cpp index 77423fcfd6f2..a771b8dc6229 100644 --- a/src/server/services/wfs/qgswfsgetcapabilities_1_0_0.cpp +++ b/src/server/services/wfs/qgswfsgetcapabilities_1_0_0.cpp @@ -43,10 +43,43 @@ namespace QgsWfs void writeGetCapabilities( QgsServerInterface *serverIface, const QgsProject *project, const QString &version, const QgsServerRequest &request, QgsServerResponse &response ) { - QDomDocument doc = createGetCapabilitiesDocument( serverIface, project, version, request ); + QStringList cacheKeyList; + bool cache = true; + + QgsAccessControl *accessControl = serverIface->accessControls(); + if ( accessControl ) + cache = accessControl->fillCacheKey( cacheKeyList ); + + QDomDocument doc; + QString cacheKey = cacheKeyList.join( QStringLiteral( "-" ) ); + const QDomDocument *capabilitiesDocument; + + QgsServerCacheManager *cacheManager = serverIface->cacheManager(); + if ( cacheManager && cache ) + { + capabilitiesDocument = cacheManager->getCachedDocument( project, request, cacheKey ); + } + + if ( !capabilitiesDocument ) //capabilities xml not in cache. Create a new one + { + doc = createGetCapabilitiesDocument( serverIface, project, version, request ); + + if ( cache && cacheManager ) + { + if ( cacheManager->setCachedDocument( &doc, project, request, cacheKey ) ) + { + capabilitiesDocument = cacheManager->getCachedDocument( project, request, cacheKey ); + } + } + if ( !capabilitiesDocument ) + { + doc = doc.cloneNode().toDocument(); + capabilitiesDocument = &doc; + } + } response.setHeader( "Content-Type", "text/xml; charset=utf-8" ); - response.write( doc.toByteArray() ); + response.write( capabilitiesDocument->toByteArray() ); } diff --git a/src/server/services/wms/qgswmsgetcapabilities.cpp b/src/server/services/wms/qgswmsgetcapabilities.cpp index 0785ed82c85d..c3fedd687c89 100644 --- a/src/server/services/wms/qgswmsgetcapabilities.cpp +++ b/src/server/services/wms/qgswmsgetcapabilities.cpp @@ -100,15 +100,26 @@ namespace QgsWms cacheKeyList << request.url().host(); bool cache = true; -#ifdef HAVE_SERVER_PYTHON_PLUGINS QgsAccessControl *accessControl = serverIface->accessControls(); if ( accessControl ) cache = accessControl->fillCacheKey( cacheKeyList ); -#endif + QDomDocument doc; QString cacheKey = cacheKeyList.join( QStringLiteral( "-" ) ); - const QDomDocument *capabilitiesDocument = capabilitiesCache->searchCapabilitiesDocument( configFilePath, cacheKey ); + const QDomDocument *capabilitiesDocument; + + QgsServerCacheManager *cacheManager = serverIface->cacheManager(); + if ( cacheManager && cache ) + { + if ( cacheKeyList.count() == 2 ) + capabilitiesDocument = cacheManager->getCachedDocument( project, request, QStringLiteral( "" ) ); + else if ( cacheKeyList.count() > 2 ) + capabilitiesDocument = cacheManager->getCachedDocument( project, request, cacheKeyList.at( 3 ) ); + } + + if ( !capabilitiesDocument ) //capabilities xml not in cache plugins + capabilitiesDocument = capabilitiesCache->searchCapabilitiesDocument( configFilePath, cacheKey ); if ( !capabilitiesDocument ) //capabilities xml not in cache. Create a new one { QgsMessageLog::logMessage( QStringLiteral( "Capabilities document not found in cache" ) ); @@ -117,10 +128,26 @@ namespace QgsWms if ( cache ) { - capabilitiesCache->insertCapabilitiesDocument( configFilePath, cacheKey, &doc ); - capabilitiesDocument = capabilitiesCache->searchCapabilitiesDocument( configFilePath, cacheKey ); + if ( cacheManager ) + { + if ( cacheKeyList.count() == 2 && + cacheManager->setCachedDocument( &doc, project, request, QStringLiteral( "" ) ) ) + { + capabilitiesDocument = cacheManager->getCachedDocument( project, request, QStringLiteral( "" ) ); + } + else if ( cacheKeyList.count() > 2 && + cacheManager->setCachedDocument( &doc, project, request, cacheKeyList.at( 3 ) ) ) + { + capabilitiesDocument = cacheManager->getCachedDocument( project, request, cacheKeyList.at( 3 ) ); + } + } + else + { + capabilitiesCache->insertCapabilitiesDocument( configFilePath, cacheKey, &doc ); + capabilitiesDocument = capabilitiesCache->searchCapabilitiesDocument( configFilePath, cacheKey ); + } } - else + if ( !capabilitiesDocument ) { doc = doc.cloneNode().toDocument(); capabilitiesDocument = &doc; diff --git a/tests/src/python/CMakeLists.txt b/tests/src/python/CMakeLists.txt index bab9627d80b3..f198c7e4834b 100644 --- a/tests/src/python/CMakeLists.txt +++ b/tests/src/python/CMakeLists.txt @@ -268,6 +268,7 @@ IF (WITH_SERVER) ADD_PYTHON_TEST(PyQgsServerAccessControlWFS test_qgsserver_accesscontrol_wfs.py) ADD_PYTHON_TEST(PyQgsServerAccessControlWCS test_qgsserver_accesscontrol_wcs.py) ADD_PYTHON_TEST(PyQgsServerAccessControlWFSTransactional test_qgsserver_accesscontrol_wfs_transactional.py) + ADD_PYTHON_TEST(PyQgsServerCacheManager test_qgsserver_cachemanager.py) ADD_PYTHON_TEST(PyQgsServerWFS test_qgsserver_wfs.py) ADD_PYTHON_TEST(PyQgsServerWFST test_qgsserver_wfst.py) ADD_PYTHON_TEST(PyQgsOfflineEditingWFS test_offline_editing_wfs.py) diff --git a/tests/src/python/test_qgsserver_cachemanager.py b/tests/src/python/test_qgsserver_cachemanager.py new file mode 100644 index 000000000000..3f245ca66fbb --- /dev/null +++ b/tests/src/python/test_qgsserver_cachemanager.py @@ -0,0 +1,154 @@ +# -*- coding: utf-8 -*- +"""QGIS Unit tests for QgsServer. + +.. note:: 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. +""" +__author__ = 'René-Luc DHONT' +__date__ = '19/07/2018' +__copyright__ = 'Copyright 2015, The QGIS Project' +# This will get replaced with a git SHA1 when you do a git archive +__revision__ = '$Format:%H$' + +print('CTEST_FULL_OUTPUT') + +import qgis # NOQA + +import os +import urllib.request +import urllib.parse +import urllib.error +import tempfile +import hashlib + +from qgis.testing import unittest +from utilities import unitTestDataPath +from qgis.server import QgsServer, QgsServerCacheFilter, QgsServerRequest, QgsBufferServerRequest, QgsBufferServerResponse +from qgis.core import QgsApplication, QgsFontUtils +from qgis.PyQt.QtCore import QFile, QByteArray +from qgis.PyQt.QtXml import QDomDocument + + +class PyServerCache(QgsServerCacheFilter): + + """ Used to have restriction access """ + + # Be able to deactivate the access control to have a reference point + _active = False + + def __init__(self, server_iface): + super(QgsServerCacheFilter, self).__init__(server_iface) + self._cache_dir = os.path.join(tempfile.gettempdir(), "qgs_server_cache") + if not os.path.exists(self._cache_dir): + os.mkdir(self._cache_dir) + + def getCachedDocument(self, project, request, key): + m = hashlib.md5() + paramMap = request.parameters() + urlParam = "&".join(["%s=%s" % (k, paramMap[k]) for k in paramMap.keys()]) + m.update(urlParam.encode('utf8')) + + if not os.path.exists(os.path.join(self._cache_dir, m.hexdigest() + ".xml")): + return QByteArray() + + doc = QDomDocument(m.hexdigest() + ".xml") + with open(os.path.join(self._cache_dir, m.hexdigest() + ".xml"), "r") as f: + statusOK, errorStr, errorLine, errorColumn = doc.setContent(f.read(), True) + if not statusOK: + print("Could not read or find the contents document. Error at line %d, column %d:\n%s" % (errorLine, errorColumn, errorStr)) + return QByteArray() + + return doc.toByteArray() + + def setCachedDocument(self, doc, project, request, key): + m = hashlib.md5() + paramMap = request.parameters() + urlParam = "&".join(["%s=%s" % (k, paramMap[k]) for k in paramMap.keys()]) + m.update(urlParam.encode('utf8')) + with open(os.path.join(self._cache_dir, m.hexdigest() + ".xml"), "w") as f: + f.write(doc.toString()) + return os.path.exists(os.path.join(self._cache_dir, m.hexdigest() + ".xml")) + + +class TestQgsServerCacheManager(unittest.TestCase): + + @classmethod + def _handle_request(cls, qs, requestMethod=QgsServerRequest.GetMethod, data=None): + if data is not None: + data = data.encode('utf-8') + request = QgsBufferServerRequest(qs, requestMethod, {}, data) + response = QgsBufferServerResponse() + cls._server.handleRequest(request, response) + headers = [] + rh = response.headers() + rk = sorted(rh.keys()) + for k in rk: + headers.append(("%s: %s" % (k, rh[k])).encode('utf-8')) + return b"\n".join(headers) + b"\n\n", bytes(response.body()) + + @classmethod + def setUpClass(cls): + """Run before all tests""" + cls._app = QgsApplication([], False) + cls._server = QgsServer() + cls._handle_request("") + cls._server_iface = cls._server.serverInterface() + cls._servercache = PyServerCache(cls._server_iface) + cls._server_iface.registerServerCache(cls._servercache, 100) + + @classmethod + def tearDownClass(cls): + """Run after all tests""" + filelist = [f for f in os.listdir(cls._servercache._cache_dir) if f.endswith(".xml")] + for f in filelist: + os.remove(os.path.join(cls._servercache._cache_dir, f)) + del cls._server + cls._app.exitQgis + + def _result(self, data): + headers = {} + for line in data[0].decode('UTF-8').split("\n"): + if line != "": + header = line.split(":") + self.assertEqual(len(header), 2, line) + headers[str(header[0])] = str(header[1]).strip() + + return data[1], headers + + def _execute_request(self, qs, requestMethod=QgsServerRequest.GetMethod, data=None): + request = QgsBufferServerRequest(qs, requestMethod, {}, data) + response = QgsBufferServerResponse() + self._server.handleRequest(request, response) + headers = [] + rh = response.headers() + rk = sorted(rh.keys()) + for k in rk: + headers.append(("%s: %s" % (k, rh[k])).encode('utf-8')) + return b"\n".join(headers) + b"\n\n", bytes(response.body()) + + def setUp(self): + """Create the server instance""" + self.fontFamily = QgsFontUtils.standardTestFontFamily() + QgsFontUtils.loadStandardTestFonts(['All']) + + d = unitTestDataPath('qgis_server_accesscontrol') + '/' + self._project_path = os.path.join(d, "project.qgs") + + def test_getcapabilities(self): + project = self._project_path + assert os.path.exists(project), "Project file not found: " + project + + query_string = '?MAP=%s&SERVICE=WMS&VERSION=1.3.0&REQUEST=%s' % (urllib.parse.quote(project), 'GetCapabilities') + header, body = self._execute_request(query_string) + + query_string = '?MAP=%s&SERVICE=WFS&VERSION=1.1.0&REQUEST=%s' % (urllib.parse.quote(project), 'GetCapabilities') + header, body = self._execute_request(query_string) + + query_string = '?MAP=%s&SERVICE=WCS&VERSION=1.0.0&REQUEST=%s' % (urllib.parse.quote(project), 'GetCapabilities') + header, body = self._execute_request(query_string) + + +if __name__ == "__main__": + unittest.main() From 066f84fd2837cf443e70a48228f06fa49a170952 Mon Sep 17 00:00:00 2001 From: rldhont Date: Mon, 23 Jul 2018 15:37:13 +0200 Subject: [PATCH 02/33] [Server][Feature][needs-docs] Using QByteArray in Cache Manager, fixing capabilities pointer and enhancing tests --- src/server/qgsservercachefilter.cpp | 7 ++ src/server/qgsservercachefilter.h | 7 ++ src/server/qgsservercachemanager.cpp | 24 ++++-- src/server/qgsservercachemanager.h | 9 ++- .../services/wcs/qgswcsgetcapabilities.cpp | 16 +++- .../services/wfs/qgswfsgetcapabilities.cpp | 16 +++- .../wfs/qgswfsgetcapabilities_1_0_0.cpp | 16 +++- .../services/wms/qgswmsgetcapabilities.cpp | 28 +++++-- .../src/python/test_qgsserver_cachemanager.py | 77 ++++++++++++++++++- 9 files changed, 174 insertions(+), 26 deletions(-) diff --git a/src/server/qgsservercachefilter.cpp b/src/server/qgsservercachefilter.cpp index fe6e8d632381..63ff9368a0d3 100644 --- a/src/server/qgsservercachefilter.cpp +++ b/src/server/qgsservercachefilter.cpp @@ -54,3 +54,10 @@ bool QgsServerCacheFilter::deleteCachedDocument( const QgsProject *project, cons Q_UNUSED( key ); return false; } + +//! Deletes all cached documents for a QGIS project +bool QgsServerCacheFilter::deleteCachedDocuments( const QgsProject *project ) const +{ + Q_UNUSED( project ); + return false; +} diff --git a/src/server/qgsservercachefilter.h b/src/server/qgsservercachefilter.h index fab907db6061..6665fa7cfa91 100644 --- a/src/server/qgsservercachefilter.h +++ b/src/server/qgsservercachefilter.h @@ -80,6 +80,13 @@ class SERVER_EXPORT QgsServerCacheFilter */ virtual bool deleteCachedDocument( const QgsProject *project, const QgsServerRequest &request, const QString &key ) const; + /** + * Deletes all cached documents for a QGIS project + * \param project the project used to generate the document to provide path + * \returns true if the documents have been deleted + */ + virtual bool deleteCachedDocuments( const QgsProject *project ) const; + private: //! The server interface diff --git a/src/server/qgsservercachemanager.cpp b/src/server/qgsservercachemanager.cpp index f044f161d6bb..b18cdb92aaf0 100644 --- a/src/server/qgsservercachemanager.cpp +++ b/src/server/qgsservercachemanager.cpp @@ -19,7 +19,7 @@ #include "qgsservercachemanager.h" //! Returns cached document (or 0 if document not in cache) like capabilities -const QDomDocument *QgsServerCacheManager::getCachedDocument( const QgsProject *project, const QgsServerRequest &request, const QString &key ) const +QByteArray QgsServerCacheManager::getCachedDocument( const QgsProject *project, const QgsServerRequest &request, const QString &key ) const { QgsServerCacheFilterMap::const_iterator scIterator; for ( scIterator = mPluginsServerCaches->constBegin(); scIterator != mPluginsServerCaches->constEnd(); ++scIterator ) @@ -27,14 +27,10 @@ const QDomDocument *QgsServerCacheManager::getCachedDocument( const QgsProject * QByteArray content = scIterator.value()->getCachedDocument( project, request, key ); if ( !content.isEmpty() ) { - QDomDocument doc; - if ( doc.setContent( content ) ) - { - return &doc; - } + return content; } } - return nullptr; + return QByteArray(); } //! Updates or inserts the document in cache like capabilities @@ -65,6 +61,20 @@ bool QgsServerCacheManager::deleteCachedDocument( const QgsProject *project, con return false; } +//! Deletes all cached documents for a QGIS Project +bool QgsServerCacheManager::deleteCachedDocuments( const QgsProject *project ) const +{ + QgsServerCacheFilterMap::const_iterator scIterator; + for ( scIterator = mPluginsServerCaches->constBegin(); scIterator != mPluginsServerCaches->constEnd(); ++scIterator ) + { + if ( scIterator.value()->deleteCachedDocuments( project ) ) + { + return true; + } + } + return false; +} + //! Register a new access control filter void QgsServerCacheManager::registerServerCache( QgsServerCacheFilter *serverCache, int priority ) { diff --git a/src/server/qgsservercachemanager.h b/src/server/qgsservercachemanager.h index 60912ae277f0..3ccfc8566dc2 100644 --- a/src/server/qgsservercachemanager.h +++ b/src/server/qgsservercachemanager.h @@ -73,7 +73,7 @@ class SERVER_EXPORT QgsServerCacheManager * \param key the key provided by the access control to identify differents documents for the same request * \returns the cached document or 0 if no corresponding document found */ - const QDomDocument *getCachedDocument( const QgsProject *project, const QgsServerRequest &request, const QString &key ) const; + QByteArray getCachedDocument( const QgsProject *project, const QgsServerRequest &request, const QString &key ) const; /** * Updates or inserts the document in cache like capabilities @@ -94,6 +94,13 @@ class SERVER_EXPORT QgsServerCacheManager */ bool deleteCachedDocument( const QgsProject *project, const QgsServerRequest &request, const QString &key ) const; + /** + * Deletes all cached documents for a QGIS project + * \param project the project used to generate the document to provide path + * \returns true if the document has been deleted + */ + bool deleteCachedDocuments( const QgsProject *project ) const; + /** * Register a server cache filter * \param serverCache the server cache to add diff --git a/src/server/services/wcs/qgswcsgetcapabilities.cpp b/src/server/services/wcs/qgswcsgetcapabilities.cpp index aa760ec1c377..74fe35db1c7d 100644 --- a/src/server/services/wcs/qgswcsgetcapabilities.cpp +++ b/src/server/services/wcs/qgswcsgetcapabilities.cpp @@ -46,12 +46,17 @@ namespace QgsWcs QDomDocument doc; QString cacheKey = cacheKeyList.join( QStringLiteral( "-" ) ); - const QDomDocument *capabilitiesDocument; + const QDomDocument *capabilitiesDocument = nullptr; QgsServerCacheManager *cacheManager = serverIface->cacheManager(); if ( cacheManager && cache ) { - capabilitiesDocument = cacheManager->getCachedDocument( project, request, cacheKey ); + QByteArray content = cacheManager->getCachedDocument( project, request, cacheKey ); + if ( !content.isEmpty() && doc.setContent( content ) ) + { + doc = doc.cloneNode().toDocument(); + capabilitiesDocument = &doc; + } } if ( !capabilitiesDocument ) //capabilities xml not in cache. Create a new one @@ -62,7 +67,12 @@ namespace QgsWcs { if ( cacheManager->setCachedDocument( &doc, project, request, cacheKey ) ) { - capabilitiesDocument = cacheManager->getCachedDocument( project, request, cacheKey ); + QByteArray content = cacheManager->getCachedDocument( project, request, cacheKey ); + if ( !content.isEmpty() && doc.setContent( content ) ) + { + doc = doc.cloneNode().toDocument(); + capabilitiesDocument = &doc; + } } } if ( !capabilitiesDocument ) diff --git a/src/server/services/wfs/qgswfsgetcapabilities.cpp b/src/server/services/wfs/qgswfsgetcapabilities.cpp index a628e49c1b56..653c41e91b83 100644 --- a/src/server/services/wfs/qgswfsgetcapabilities.cpp +++ b/src/server/services/wfs/qgswfsgetcapabilities.cpp @@ -50,12 +50,17 @@ namespace QgsWfs QDomDocument doc; QString cacheKey = cacheKeyList.join( QStringLiteral( "-" ) ); - const QDomDocument *capabilitiesDocument; + const QDomDocument *capabilitiesDocument = nullptr; QgsServerCacheManager *cacheManager = serverIface->cacheManager(); if ( cacheManager && cache ) { - capabilitiesDocument = cacheManager->getCachedDocument( project, request, cacheKey ); + QByteArray content = cacheManager->getCachedDocument( project, request, cacheKey ); + if ( !content.isEmpty() && doc.setContent( content ) ) + { + doc = doc.cloneNode().toDocument(); + capabilitiesDocument = &doc; + } } if ( !capabilitiesDocument ) //capabilities xml not in cache. Create a new one @@ -66,7 +71,12 @@ namespace QgsWfs { if ( cacheManager->setCachedDocument( &doc, project, request, cacheKey ) ) { - capabilitiesDocument = cacheManager->getCachedDocument( project, request, cacheKey ); + QByteArray content = cacheManager->getCachedDocument( project, request, cacheKey ); + if ( !content.isEmpty() && doc.setContent( content ) ) + { + doc = doc.cloneNode().toDocument(); + capabilitiesDocument = &doc; + } } } if ( !capabilitiesDocument ) diff --git a/src/server/services/wfs/qgswfsgetcapabilities_1_0_0.cpp b/src/server/services/wfs/qgswfsgetcapabilities_1_0_0.cpp index a771b8dc6229..8ddb306e1877 100644 --- a/src/server/services/wfs/qgswfsgetcapabilities_1_0_0.cpp +++ b/src/server/services/wfs/qgswfsgetcapabilities_1_0_0.cpp @@ -52,12 +52,17 @@ namespace QgsWfs QDomDocument doc; QString cacheKey = cacheKeyList.join( QStringLiteral( "-" ) ); - const QDomDocument *capabilitiesDocument; + const QDomDocument *capabilitiesDocument = nullptr; QgsServerCacheManager *cacheManager = serverIface->cacheManager(); if ( cacheManager && cache ) { - capabilitiesDocument = cacheManager->getCachedDocument( project, request, cacheKey ); + QByteArray content = cacheManager->getCachedDocument( project, request, cacheKey ); + if ( !content.isEmpty() && doc.setContent( content ) ) + { + doc = doc.cloneNode().toDocument(); + capabilitiesDocument = &doc; + } } if ( !capabilitiesDocument ) //capabilities xml not in cache. Create a new one @@ -68,7 +73,12 @@ namespace QgsWfs { if ( cacheManager->setCachedDocument( &doc, project, request, cacheKey ) ) { - capabilitiesDocument = cacheManager->getCachedDocument( project, request, cacheKey ); + QByteArray content = cacheManager->getCachedDocument( project, request, cacheKey ); + if ( !content.isEmpty() && doc.setContent( content ) ) + { + doc = doc.cloneNode().toDocument(); + capabilitiesDocument = &doc; + } } } if ( !capabilitiesDocument ) diff --git a/src/server/services/wms/qgswmsgetcapabilities.cpp b/src/server/services/wms/qgswmsgetcapabilities.cpp index c3fedd687c89..7d6c2cc5d2e1 100644 --- a/src/server/services/wms/qgswmsgetcapabilities.cpp +++ b/src/server/services/wms/qgswmsgetcapabilities.cpp @@ -107,15 +107,27 @@ namespace QgsWms QDomDocument doc; QString cacheKey = cacheKeyList.join( QStringLiteral( "-" ) ); - const QDomDocument *capabilitiesDocument; + const QDomDocument *capabilitiesDocument = nullptr; QgsServerCacheManager *cacheManager = serverIface->cacheManager(); if ( cacheManager && cache ) { + QByteArray content; if ( cacheKeyList.count() == 2 ) - capabilitiesDocument = cacheManager->getCachedDocument( project, request, QStringLiteral( "" ) ); + content = cacheManager->getCachedDocument( project, request, QStringLiteral( "" ) ); else if ( cacheKeyList.count() > 2 ) - capabilitiesDocument = cacheManager->getCachedDocument( project, request, cacheKeyList.at( 3 ) ); + content = cacheManager->getCachedDocument( project, request, cacheKeyList.at( 3 ) ); + + if ( !content.isEmpty() && doc.setContent( content ) ) + { + QgsMessageLog::logMessage( QStringLiteral( "Found capabilities document in cache manager" ) ); + doc = doc.cloneNode().toDocument(); + capabilitiesDocument = &doc; + } + else + { + QgsMessageLog::logMessage( QStringLiteral( "Capabilities document not found in cache manager" ) ); + } } if ( !capabilitiesDocument ) //capabilities xml not in cache plugins @@ -130,15 +142,21 @@ namespace QgsWms { if ( cacheManager ) { + QByteArray content; if ( cacheKeyList.count() == 2 && cacheManager->setCachedDocument( &doc, project, request, QStringLiteral( "" ) ) ) { - capabilitiesDocument = cacheManager->getCachedDocument( project, request, QStringLiteral( "" ) ); + content = cacheManager->getCachedDocument( project, request, QStringLiteral( "" ) ); } else if ( cacheKeyList.count() > 2 && cacheManager->setCachedDocument( &doc, project, request, cacheKeyList.at( 3 ) ) ) { - capabilitiesDocument = cacheManager->getCachedDocument( project, request, cacheKeyList.at( 3 ) ); + content = cacheManager->getCachedDocument( project, request, cacheKeyList.at( 3 ) ); + } + if ( !content.isEmpty() && doc.setContent( content ) ) + { + doc = doc.cloneNode().toDocument(); + capabilitiesDocument = &doc; } } else diff --git a/tests/src/python/test_qgsserver_cachemanager.py b/tests/src/python/test_qgsserver_cachemanager.py index 3f245ca66fbb..8cc0dd6cb2dc 100644 --- a/tests/src/python/test_qgsserver_cachemanager.py +++ b/tests/src/python/test_qgsserver_cachemanager.py @@ -26,7 +26,7 @@ from qgis.testing import unittest from utilities import unitTestDataPath from qgis.server import QgsServer, QgsServerCacheFilter, QgsServerRequest, QgsBufferServerRequest, QgsBufferServerResponse -from qgis.core import QgsApplication, QgsFontUtils +from qgis.core import QgsApplication, QgsFontUtils, QgsProject from qgis.PyQt.QtCore import QFile, QByteArray from qgis.PyQt.QtXml import QDomDocument @@ -71,6 +71,22 @@ def setCachedDocument(self, doc, project, request, key): f.write(doc.toString()) return os.path.exists(os.path.join(self._cache_dir, m.hexdigest() + ".xml")) + def deleteCachedDocument(self, project, request, key): + m = hashlib.md5() + paramMap = request.parameters() + urlParam = "&".join(["%s=%s" % (k, paramMap[k]) for k in paramMap.keys()]) + m.update(urlParam.encode('utf8')) + if os.path.exists(os.path.join(self._cache_dir, m.hexdigest() + ".xml")): + os.remove(os.path.join(self._cache_dir, m.hexdigest() + ".xml")) + return not os.path.exists(os.path.join(self._cache_dir, m.hexdigest() + ".xml")) + + def deleteCachedDocuments(self, project): + filelist = [f for f in os.listdir(self._cache_dir) if f.endswith(".xml")] + for f in filelist: + os.remove(os.path.join(self._cache_dir, f)) + filelist = [f for f in os.listdir(self._cache_dir) if f.endswith(".xml")] + return len(filelist) == 0 + class TestQgsServerCacheManager(unittest.TestCase): @@ -101,9 +117,7 @@ def setUpClass(cls): @classmethod def tearDownClass(cls): """Run after all tests""" - filelist = [f for f in os.listdir(cls._servercache._cache_dir) if f.endswith(".xml")] - for f in filelist: - os.remove(os.path.join(cls._servercache._cache_dir, f)) + #cls._servercache.deleteCachedDocuments(None) del cls._server cls._app.exitQgis @@ -140,14 +154,69 @@ def test_getcapabilities(self): project = self._project_path assert os.path.exists(project), "Project file not found: " + project + # without cache query_string = '?MAP=%s&SERVICE=WMS&VERSION=1.3.0&REQUEST=%s' % (urllib.parse.quote(project), 'GetCapabilities') header, body = self._execute_request(query_string) + doc = QDomDocument("wms_getcapabilities_130.xml") + doc.setContent(body) + # with cache + header, body = self._execute_request(query_string) + + # without cache + query_string = '?MAP=%s&SERVICE=WMS&VERSION=1.1.1&REQUEST=%s' % (urllib.parse.quote(project), 'GetCapabilities') + header, body = self._execute_request(query_string) + # with cache + header, body = self._execute_request(query_string) + # without cache query_string = '?MAP=%s&SERVICE=WFS&VERSION=1.1.0&REQUEST=%s' % (urllib.parse.quote(project), 'GetCapabilities') header, body = self._execute_request(query_string) + # with cache + header, body = self._execute_request(query_string) + + # without cache + query_string = '?MAP=%s&SERVICE=WFS&VERSION=1.0.0&REQUEST=%s' % (urllib.parse.quote(project), 'GetCapabilities') + header, body = self._execute_request(query_string) + # with cache + header, body = self._execute_request(query_string) + # without cache query_string = '?MAP=%s&SERVICE=WCS&VERSION=1.0.0&REQUEST=%s' % (urllib.parse.quote(project), 'GetCapabilities') header, body = self._execute_request(query_string) + # with cache + header, body = self._execute_request(query_string) + + filelist = [f for f in os.listdir(self._servercache._cache_dir) if f.endswith(".xml")] + self.assertEqual(len(filelist), 5, 'Not enough file in cache') + + cacheManager = self._server_iface.cacheManager() + + self.assertTrue(cacheManager.deleteCachedDocuments(None), 'deleteCachedDocuments does not retrun True') + + filelist = [f for f in os.listdir(self._servercache._cache_dir) if f.endswith(".xml")] + self.assertEqual(len(filelist), 0, 'All files in cache are not deleted ') + + prj = QgsProject() + prj.read(project) + + query_string = '?MAP=%s&SERVICE=WMS&VERSION=1.3.0&REQUEST=%s' % (urllib.parse.quote(project), 'GetCapabilities') + request = QgsBufferServerRequest(query_string, QgsServerRequest.GetMethod, {}, None) + + cContent = cacheManager.getCachedDocument(prj, request, '') + + self.assertTrue(cContent.isEmpty(), 'getCachedDocument is not None') + + self.assertTrue(cacheManager.setCachedDocument(doc, prj, request, ''), 'setCachedDocument false') + + cContent = cacheManager.getCachedDocument(prj, request, '') + + self.assertFalse(cContent.isEmpty(), 'getCachedDocument is empty') + + cDoc = QDomDocument("wms_getcapabilities_130.xml") + self.assertTrue(cDoc.setContent(cContent), 'cachedDocument not XML doc') + self.assertEqual(doc.documentElement().tagName(), cDoc.documentElement().tagName(), 'cachedDocument not equal to provide document') + + self.assertTrue(cacheManager.deleteCachedDocuments(None), 'deleteCachedDocuments does not retrun True') if __name__ == "__main__": From 732b96e252dc5d91341788f59bfbd40c98ff77be Mon Sep 17 00:00:00 2001 From: rldhont Date: Wed, 25 Jul 2018 17:39:49 +0200 Subject: [PATCH 03/33] [Server][WCS] clean comments --- src/server/services/wcs/qgswcs.cpp | 2 +- src/server/services/wcs/qgswcsutils.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/server/services/wcs/qgswcs.cpp b/src/server/services/wcs/qgswcs.cpp index 49d64537d8fa..7d513b5f2450 100644 --- a/src/server/services/wcs/qgswcs.cpp +++ b/src/server/services/wcs/qgswcs.cpp @@ -102,7 +102,7 @@ namespace QgsWcs }; -} // namespace QgsWfs +} // namespace QgsWcs /** * \ingroup server diff --git a/src/server/services/wcs/qgswcsutils.h b/src/server/services/wcs/qgswcsutils.h index cbd6a3689fe8..2480989a6094 100644 --- a/src/server/services/wcs/qgswcsutils.h +++ b/src/server/services/wcs/qgswcsutils.h @@ -58,7 +58,7 @@ namespace QgsWcs //XXX At some point, should be moved to common library QgsRectangle parseBbox( const QString &bboxStr ); - // Define namespaces used in WFS documents + // Define namespaces used in WCS documents const QString WCS_NAMESPACE = QStringLiteral( "http://www.opengis.net/wcs" ); const QString GML_NAMESPACE = QStringLiteral( "http://www.opengis.net/gml" ); const QString OGC_NAMESPACE = QStringLiteral( "http://www.opengis.net/ogc" ); From 6d1a45b73893d00e4ca7e0cee9e86c478d4208d8 Mon Sep 17 00:00:00 2001 From: rldhont Date: Wed, 25 Jul 2018 17:41:46 +0200 Subject: [PATCH 04/33] [Server][Feature][needs-docs] Add WMTS service This commit contains the first line of code for WMTS service in QGIS server. The implementation is mainly for standard implementation. --- src/server/services/CMakeLists.txt | 1 + src/server/services/wmts/CMakeLists.txt | 57 ++ src/server/services/wmts/qgswmts.cpp | 126 +++++ .../services/wmts/qgswmtsgetcapabilities.cpp | 491 ++++++++++++++++++ .../services/wmts/qgswmtsgetcapabilities.h | 62 +++ src/server/services/wmts/qgswmtsgettile.cpp | 222 ++++++++ src/server/services/wmts/qgswmtsgettile.h | 28 + .../services/wmts/qgswmtsserviceexception.h | 105 ++++ src/server/services/wmts/qgswmtsutils.cpp | 269 ++++++++++ src/server/services/wmts/qgswmtsutils.h | 101 ++++ 10 files changed, 1462 insertions(+) create mode 100644 src/server/services/wmts/CMakeLists.txt create mode 100644 src/server/services/wmts/qgswmts.cpp create mode 100644 src/server/services/wmts/qgswmtsgetcapabilities.cpp create mode 100644 src/server/services/wmts/qgswmtsgetcapabilities.h create mode 100644 src/server/services/wmts/qgswmtsgettile.cpp create mode 100644 src/server/services/wmts/qgswmtsgettile.h create mode 100644 src/server/services/wmts/qgswmtsserviceexception.h create mode 100644 src/server/services/wmts/qgswmtsutils.cpp create mode 100644 src/server/services/wmts/qgswmtsutils.h diff --git a/src/server/services/CMakeLists.txt b/src/server/services/CMakeLists.txt index 470eff3bd029..c48165eeb760 100644 --- a/src/server/services/CMakeLists.txt +++ b/src/server/services/CMakeLists.txt @@ -10,4 +10,5 @@ ADD_SUBDIRECTORY(DummyService) ADD_SUBDIRECTORY(wms) ADD_SUBDIRECTORY(wfs) ADD_SUBDIRECTORY(wcs) +ADD_SUBDIRECTORY(wmts) diff --git a/src/server/services/wmts/CMakeLists.txt b/src/server/services/wmts/CMakeLists.txt new file mode 100644 index 000000000000..8c237ee2eb41 --- /dev/null +++ b/src/server/services/wmts/CMakeLists.txt @@ -0,0 +1,57 @@ + +######################################################## +# Files + +SET (wmts_SRCS + qgswmts.cpp + qgswmtsutils.cpp + qgswmtsgetcapabilities.cpp + qgswmtsgettile.cpp +) + +######################################################## +# Build + +ADD_LIBRARY (wmts MODULE ${wmts_SRCS}) + + +INCLUDE_DIRECTORIES(SYSTEM + ${GDAL_INCLUDE_DIR} + ${POSTGRES_INCLUDE_DIR} +) + +INCLUDE_DIRECTORIES( + ${CMAKE_BINARY_DIR}/src/core + ${CMAKE_BINARY_DIR}/src/python + ${CMAKE_BINARY_DIR}/src/analysis + ${CMAKE_BINARY_DIR}/src/server + ${CMAKE_CURRENT_BINARY_DIR} + ../wms + ../../../core + ../../../core/dxf + ../../../core/expression + ../../../core/geometry + ../../../core/metadata + ../../../core/raster + ../../../core/symbology + ../../../core/layertree + ../.. + .. + . +) + + +TARGET_LINK_LIBRARIES(wmts + qgis_core + qgis_server +) + + +######################################################## +# Install + +INSTALL(TARGETS wmts + RUNTIME DESTINATION ${QGIS_SERVER_MODULE_DIR} + LIBRARY DESTINATION ${QGIS_SERVER_MODULE_DIR} +) + diff --git a/src/server/services/wmts/qgswmts.cpp b/src/server/services/wmts/qgswmts.cpp new file mode 100644 index 000000000000..6cc16cb596c0 --- /dev/null +++ b/src/server/services/wmts/qgswmts.cpp @@ -0,0 +1,126 @@ +/*************************************************************************** + qgswmts.cpp + ------------------------- + begin : July 23 , 2018 + copyright : (C) 2018 by René-Luc D'Hont + email : rldhont at 3liz dot 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 "qgsmodule.h" +#include "qgswmtsutils.h" +#include "qgswmtsgetcapabilities.h" +#include "qgswmtsgettile.h" + +#define QSTR_COMPARE( str, lit )\ + (str.compare( QStringLiteral( lit ), Qt::CaseInsensitive ) == 0) + +namespace QgsWmts +{ + + /** + * \ingroup server + * \class QgsWmts::Service + * \brief OGC web service specialized for WMTS + * \since QGIS 3.0 + */ + class Service: public QgsService + { + public: + + /** + * Constructor for WMTS service. + * \param serverIface Interface for plugins. + */ + Service( QgsServerInterface *serverIface ) + : mServerIface( serverIface ) + {} + + QString name() const override { return QStringLiteral( "WMTS" ); } + QString version() const override { return implementationVersion(); } + + bool allowMethod( QgsServerRequest::Method method ) const override + { + return method == QgsServerRequest::GetMethod || method == QgsServerRequest::PostMethod; + } + + void executeRequest( const QgsServerRequest &request, QgsServerResponse &response, + const QgsProject *project ) override + { + Q_UNUSED( project ); + + QgsServerRequest::Parameters params = request.parameters(); + QString versionString = params.value( "VERSION" ); + + // Set the default version + if ( versionString.isEmpty() ) + { + versionString = version(); + } + + // Get the request + QString req = params.value( QStringLiteral( "REQUEST" ) ); + if ( req.isEmpty() ) + { + throw QgsServiceException( QStringLiteral( "OperationNotSupported" ), + QStringLiteral( "Please check the value of the REQUEST parameter" ) ); + } + + if ( QSTR_COMPARE( req, "GetCapabilities" ) ) + { + writeGetCapabilities( mServerIface, project, versionString, request, response ); + } + else if ( QSTR_COMPARE( req, "GetTile" ) ) + { + writeGetTile( mServerIface, project, versionString, request, response ); + } + else + { + // Operation not supported + throw QgsServiceException( QStringLiteral( "OperationNotSupported" ), + QStringLiteral( "Request %1 is not supported" ).arg( req ) ); + } + } + + private: + QgsServerInterface *mServerIface = nullptr; + }; + + +} // namespace QgsWmts + +/** + * \ingroup server + * \class QgsWmtsModule + * \brief Service module specialized for WMTS + * \since QGIS 3.4 + */ +class QgsWmtsModule: public QgsServiceModule +{ + public: + void registerSelf( QgsServiceRegistry ®istry, QgsServerInterface *serverIface ) override + { + QgsDebugMsg( "WMTSModule::registerSelf called" ); + registry.registerService( new QgsWmts::Service( serverIface ) ); + } +}; + + +// Entry points +QGISEXTERN QgsServiceModule *QGS_ServiceModule_Init() +{ + static QgsWmtsModule module; + return &module; +} +QGISEXTERN void QGS_ServiceModule_Exit( QgsServiceModule * ) +{ + // Nothing to do +} diff --git a/src/server/services/wmts/qgswmtsgetcapabilities.cpp b/src/server/services/wmts/qgswmtsgetcapabilities.cpp new file mode 100644 index 000000000000..96d03f7c8db2 --- /dev/null +++ b/src/server/services/wmts/qgswmtsgetcapabilities.cpp @@ -0,0 +1,491 @@ +/*************************************************************************** + qgswmtsgecapabilities.cpp + ------------------------- + begin : July 23 , 2017 + copyright : (C) 2018 by René-Luc D'Hont + email : rldhont at 3liz dot 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 "qgswmtsutils.h" +#include "qgsserverprojectutils.h" +#include "qgswmtsgetcapabilities.h" + +#include "qgsproject.h" +#include "qgsexception.h" +#include "qgsmapserviceexception.h" +#include "qgscoordinatereferencesystem.h" + +#include + +namespace QgsWmts +{ + + /** + * Output WMTS GetCapabilities response + */ + void writeGetCapabilities( QgsServerInterface *serverIface, const QgsProject *project, const QString &version, + const QgsServerRequest &request, QgsServerResponse &response ) + { + QStringList cacheKeyList; + bool cache = true; + + QgsAccessControl *accessControl = serverIface->accessControls(); + if ( accessControl ) + cache = accessControl->fillCacheKey( cacheKeyList ); + + QDomDocument doc; + QString cacheKey = cacheKeyList.join( QStringLiteral( "-" ) ); + const QDomDocument *capabilitiesDocument = nullptr; + + QgsServerCacheManager *cacheManager = serverIface->cacheManager(); + if ( cacheManager && cache ) + { + QByteArray content = cacheManager->getCachedDocument( project, request, cacheKey ); + if ( !content.isEmpty() && doc.setContent( content ) ) + { + doc = doc.cloneNode().toDocument(); + capabilitiesDocument = &doc; + } + } + + if ( !capabilitiesDocument ) //capabilities xml not in cache. Create a new one + { + doc = createGetCapabilitiesDocument( serverIface, project, version, request ); + + if ( cache && cacheManager ) + { + if ( cacheManager->setCachedDocument( &doc, project, request, cacheKey ) ) + { + QByteArray content = cacheManager->getCachedDocument( project, request, cacheKey ); + if ( !content.isEmpty() && doc.setContent( content ) ) + { + doc = doc.cloneNode().toDocument(); + capabilitiesDocument = &doc; + } + } + } + if ( !capabilitiesDocument ) + { + doc = doc.cloneNode().toDocument(); + capabilitiesDocument = &doc; + } + } + + response.setHeader( "Content-Type", "text/xml; charset=utf-8" ); + response.write( capabilitiesDocument->toByteArray() ); + } + + + QDomDocument createGetCapabilitiesDocument( QgsServerInterface *serverIface, const QgsProject *project, const QString &version, + const QgsServerRequest &request ) + { + Q_UNUSED( version ); + + QDomDocument doc; + + //wmts:Capabilities element + QDomElement wmtsCapabilitiesElement = doc.createElement( QStringLiteral( "Capabilities" )/*wmts:Capabilities*/ ); + wmtsCapabilitiesElement.setAttribute( QStringLiteral( "xmlns" ), WMTS_NAMESPACE ); + wmtsCapabilitiesElement.setAttribute( QStringLiteral( "xmlns:gml" ), GML_NAMESPACE ); + wmtsCapabilitiesElement.setAttribute( QStringLiteral( "xmlns:ows" ), OWS_NAMESPACE ); + wmtsCapabilitiesElement.setAttribute( QStringLiteral( "xmlns:xlink" ), QStringLiteral( "http://www.w3.org/1999/xlink" ) ); + wmtsCapabilitiesElement.setAttribute( QStringLiteral( "xmlns:xsi" ), QStringLiteral( "http://www.w3.org/2001/XMLSchema-instance" ) ); + wmtsCapabilitiesElement.setAttribute( QStringLiteral( "xsi:schemaLocation" ), WMTS_NAMESPACE + " http://schemas.opengis.net/wmts/1.0/wmtsGetCapabilities_response.xsd" ); + wmtsCapabilitiesElement.setAttribute( QStringLiteral( "version" ), implementationVersion() ); + doc.appendChild( wmtsCapabilitiesElement ); + + //INSERT ServiceIdentification + wmtsCapabilitiesElement.appendChild( getServiceIdentificationElement( doc, project ) ); + + //INSERT ServiceProvider + wmtsCapabilitiesElement.appendChild( getServiceProviderElement( doc, project ) ); + + //INSERT OperationsMetadata + wmtsCapabilitiesElement.appendChild( getOperationsMetadataElement( doc, project, request ) ); + + //INSERT Contents + wmtsCapabilitiesElement.appendChild( getContentsElement( doc, serverIface, project ) ); + + return doc; + + } + + QDomElement getServiceIdentificationElement( QDomDocument &doc, const QgsProject *project ) + { + //Service identification + QDomElement serviceElem = doc.createElement( QStringLiteral( "ows:ServiceIdentification" ) ); + + //Service type + QDomElement typeElem = doc.createElement( QStringLiteral( "ows:ServiceType" ) ); + QDomText typeText = doc.createTextNode( "OGC WMTS" ); + typeElem.appendChild( typeText ); + serviceElem.appendChild( typeElem ); + + //Service type version + QDomElement typeVersionElem = doc.createElement( QStringLiteral( "ows:ServiceTypeVersion" ) ); + QDomText typeVersionText = doc.createTextNode( implementationVersion() ); + typeVersionElem.appendChild( typeVersionText ); + serviceElem.appendChild( typeVersionElem ); + + QString title = QgsServerProjectUtils::owsServiceTitle( *project ); + if ( !title.isEmpty() ) + { + QDomElement titleElem = doc.createElement( QStringLiteral( "ows:Title" ) ); + QDomText titleText = doc.createTextNode( title ); + titleElem.appendChild( titleText ); + serviceElem.appendChild( titleElem ); + } + + QString abstract = QgsServerProjectUtils::owsServiceAbstract( *project ); + if ( !abstract.isEmpty() ) + { + QDomElement abstractElem = doc.createElement( QStringLiteral( "ows:Abstract" ) ); + QDomText abstractText = doc.createCDATASection( abstract ); + abstractElem.appendChild( abstractText ); + serviceElem.appendChild( abstractElem ); + } + + QStringList keywords = QgsServerProjectUtils::owsServiceKeywords( *project ); + if ( !keywords.isEmpty() ) + { + QDomElement keywordsElem = doc.createElement( QStringLiteral( "ows:Keywords" ) ); + for ( int i = 0; i < keywords.size(); ++i ) + { + QDomElement keywordElem = doc.createElement( QStringLiteral( "ows:Keyword" ) ); + QDomText keywordText = doc.createTextNode( keywords.at( i ) ); + keywordElem.appendChild( keywordText ); + keywordsElem.appendChild( keywordElem ); + } + serviceElem.appendChild( keywordsElem ); + } + + QDomElement feesElem = doc.createElement( QStringLiteral( "ows:Fees" ) ); + QDomText feesText = doc.createTextNode( QStringLiteral( "None" ) ); // default value if fees are unknown + QString fees = QgsServerProjectUtils::owsServiceFees( *project ); + if ( !fees.isEmpty() ) + { + feesText = doc.createTextNode( fees ); + } + feesElem.appendChild( feesText ); + serviceElem.appendChild( feesElem ); + + QDomElement accessConstraintsElem = doc.createElement( QStringLiteral( "ows:AccessConstraints" ) ); + QDomText accessConstraintsText = doc.createTextNode( QStringLiteral( "None" ) ); // default value if access constraints are unknown + QString accessConstraints = QgsServerProjectUtils::owsServiceAccessConstraints( *project ); + if ( !accessConstraints.isEmpty() ) + { + accessConstraintsText = doc.createTextNode( accessConstraints ); + } + accessConstraintsElem.appendChild( accessConstraintsText ); + serviceElem.appendChild( accessConstraintsElem ); + + //End + return serviceElem; + } + + QDomElement getServiceProviderElement( QDomDocument &doc, const QgsProject *project ) + { + //Service provider + QDomElement serviceElem = doc.createElement( QStringLiteral( "ows:ServiceProvider" ) ); + + QString contactOrganization = QgsServerProjectUtils::owsServiceContactOrganization( *project ); + if ( !contactOrganization.isEmpty() ) + { + QDomElement contactOrganizationElem = doc.createElement( QStringLiteral( "ows:ProviderName" ) ); + QDomText contactOrganizationText = doc.createTextNode( contactOrganization ); + contactOrganizationElem.appendChild( contactOrganizationText ); + serviceElem.appendChild( contactOrganizationElem ); + } + + QString onlineResource = QgsServerProjectUtils::owsServiceOnlineResource( *project ); + if ( !onlineResource.isEmpty() ) + { + QDomElement onlineResourceElem = doc.createElement( QStringLiteral( "ows:ProviderSite" ) ); + onlineResourceElem.setAttribute( QStringLiteral( "xlink:href" ), onlineResource ); + serviceElem.appendChild( onlineResourceElem ); + } + + //Contact informations + QString contactPerson = QgsServerProjectUtils::owsServiceContactPerson( *project ); + QString contactPosition = QgsServerProjectUtils::owsServiceContactPosition( *project ); + QString contactMail = QgsServerProjectUtils::owsServiceContactMail( *project ); + QString contactPhone = QgsServerProjectUtils::owsServiceContactPhone( *project ); + if ( !contactPerson.isEmpty() || + !contactPosition.isEmpty() || + !contactMail.isEmpty() || + !contactPhone.isEmpty() ) + { + QDomElement serviceContactElem = doc.createElement( QStringLiteral( "ows:ServiceContact" ) ); + if ( !contactPerson.isEmpty() ) + { + QDomElement contactPersonElem = doc.createElement( QStringLiteral( "ows:IndividualName" ) ); + QDomText contactPersonText = doc.createTextNode( contactPerson ); + contactPersonElem.appendChild( contactPersonText ); + serviceContactElem.appendChild( contactPersonElem ); + } + if ( !contactPosition.isEmpty() ) + { + QDomElement contactPositionElem = doc.createElement( QStringLiteral( "ows:PositionName" ) ); + QDomText contactPositionText = doc.createTextNode( contactPosition ); + contactPositionElem.appendChild( contactPositionText ); + serviceContactElem.appendChild( contactPositionElem ); + } + if ( !contactMail.isEmpty() || + !contactPhone.isEmpty() ) + { + QDomElement contactInfoElem = doc.createElement( QStringLiteral( "ows:ContactInfo" ) ); + if ( !contactMail.isEmpty() ) + { + QDomElement contactAddressElem = doc.createElement( QStringLiteral( "ows:Address" ) ); + QDomElement contactAddressMailElem = doc.createElement( QStringLiteral( "ows:ElectronicMailAddress" ) ); + QDomText contactAddressMailText = doc.createTextNode( contactMail ); + contactAddressMailElem.appendChild( contactAddressMailText ); + contactAddressElem.appendChild( contactAddressMailElem ); + contactInfoElem.appendChild( contactAddressElem ); + } + if ( !contactPhone.isEmpty() ) + { + QDomElement contactPhoneElem = doc.createElement( QStringLiteral( "ows:Phone" ) ); + QDomElement contactVoiceElem = doc.createElement( QStringLiteral( "ows:Voice" ) ); + QDomText contactVoiceText = doc.createTextNode( contactPhone ); + contactVoiceElem.appendChild( contactVoiceText ); + contactPhoneElem.appendChild( contactVoiceElem ); + contactInfoElem.appendChild( contactPhoneElem ); + } + serviceContactElem.appendChild( contactInfoElem ); + } + serviceElem.appendChild( serviceContactElem ); + } + + //End + return serviceElem; + } + + QDomElement getOperationsMetadataElement( QDomDocument &doc, const QgsProject *project, const QgsServerRequest &request ) + { + //ows:OperationsMetadata element + QDomElement operationsMetadataElement = doc.createElement( QStringLiteral( "ows:OperationsMetadata" )/*ows:OperationsMetadata*/ ); + + //ows:Operation element with name GetCapabilities + QDomElement getCapabilitiesElement = doc.createElement( QStringLiteral( "ows:Operation" )/*ows:Operation*/ ); + getCapabilitiesElement.setAttribute( QStringLiteral( "name" ), QStringLiteral( "GetCapabilities" ) ); + operationsMetadataElement.appendChild( getCapabilitiesElement ); + + //ows:DCP + QDomElement dcpElement = doc.createElement( QStringLiteral( "ows:DCP" )/*ows:DCP*/ ); + getCapabilitiesElement.appendChild( dcpElement ); + QDomElement httpElement = doc.createElement( QStringLiteral( "ows:HTTP" )/*ows:HTTP*/ ); + dcpElement.appendChild( httpElement ); + + //Prepare url + QString hrefString = serviceUrl( request, project ); + + //ows:Get + QDomElement getElement = doc.createElement( QStringLiteral( "ows:Get" )/*ows:Get*/ ); + getElement.setAttribute( QStringLiteral( "xlink:href" ), hrefString ); + httpElement.appendChild( getElement ); + + //ows:Operation element with name GetTile + QDomElement getTileElement = getCapabilitiesElement.cloneNode().toElement();//this is the same as 'GetCapabilities' + getTileElement.setAttribute( QStringLiteral( "name" ), QStringLiteral( "GetTile" ) ); + operationsMetadataElement.appendChild( getTileElement ); + + //ows:Operation element with name GetFeatureInfo + /*QDomElement getFeatureInfoElement = getCapabilitiesElement.cloneNode().toElement();//this is the same as 'GetCapabilities' + getFeatureInfoElement.setAttribute( QStringLiteral( "name" ), QStringLiteral( "GetFeatureInfo" ) ); + operationsMetadataElement.appendChild( getFeatureInfoElement );*/ + + // End + return operationsMetadataElement; + } + + QDomElement getContentsElement( QDomDocument &doc, QgsServerInterface *serverIface, const QgsProject *project ) + { +#ifdef HAVE_SERVER_PYTHON_PLUGINS + QgsAccessControl *accessControl = serverIface->accessControls(); +#endif + /* + * Adding layer list in ContentMetadata + */ + QDomElement contentsElement = doc.createElement( QStringLiteral( "Contents" )/*wmts:Contents*/ ); + + QList< tileMatrixSet > tmsList = getTileMatrixSetList( project ); + if ( !tmsList.isEmpty() ) + { + QDomElement layerParentElem = doc.createElement( QStringLiteral( "Layer" ) ); + + // Root Layer name + QString rootLayerName = QgsServerProjectUtils::wmsRootName( *project ); + if ( rootLayerName.isEmpty() && !project->title().isEmpty() ) + { + rootLayerName = project->title(); + } + + if ( !rootLayerName.isEmpty() ) + { + QDomElement layerParentNameElem = doc.createElement( QStringLiteral( "ows:Identifier" ) ); + QDomText layerParentNameText = doc.createTextNode( rootLayerName ); + layerParentNameElem.appendChild( layerParentNameText ); + layerParentElem.appendChild( layerParentNameElem ); + } + + if ( !project->title().isEmpty() ) + { + // Root Layer title + QDomElement layerParentTitleElem = doc.createElement( QStringLiteral( "ows:Title" ) ); + QDomText layerParentTitleText = doc.createTextNode( project->title() ); + layerParentTitleElem.appendChild( layerParentTitleText ); + layerParentElem.appendChild( layerParentTitleElem ); + + // Root Layer abstract + QDomElement layerParentAbstElem = doc.createElement( QStringLiteral( "ows:Abstract" ) ); + QDomText layerParentAbstText = doc.createTextNode( project->title() ); + layerParentAbstElem.appendChild( layerParentAbstText ); + layerParentElem.appendChild( layerParentAbstElem ); + } + + //transform the project native CRS into WGS84 + QgsRectangle wgs84BoundingRect; + QgsRectangle projRect = QgsServerProjectUtils::wmsExtent( *project ); + QgsCoordinateReferenceSystem projCrs = project->crs(); + QgsCoordinateReferenceSystem wgs84 = QgsCoordinateReferenceSystem::fromOgcWmsCrs( GEO_EPSG_CRS_AUTHID ); + Q_NOWARN_DEPRECATED_PUSH + QgsCoordinateTransform exGeoTransform( projCrs, wgs84 ); + Q_NOWARN_DEPRECATED_POP + try + { + wgs84BoundingRect = exGeoTransform.transformBoundingBox( projRect ); + } + catch ( const QgsCsException & ) + { + wgs84BoundingRect = QgsRectangle( -180, -90, 180, 90 ); + } + QDomElement wgs84BBoxElement = doc.createElement( QStringLiteral( "ows:WGS84BoundingBox" ) ); + QDomElement wgs84LowerCornerElement = doc.createElement( QStringLiteral( "LowerCorner" ) ); + QDomText wgs84LowerCornerText = doc.createTextNode( qgsDoubleToString( wgs84BoundingRect.xMinimum(), 6 ) + ' ' + qgsDoubleToString( wgs84BoundingRect.yMinimum(), 6 ) ); + wgs84LowerCornerElement.appendChild( wgs84LowerCornerText ); + wgs84BBoxElement.appendChild( wgs84LowerCornerElement ); + QDomElement wgs84UpperCornerElement = doc.createElement( QStringLiteral( "UpperCorner" ) ); + QDomText wgs84UpperCornerText = doc.createTextNode( qgsDoubleToString( wgs84BoundingRect.xMaximum(), 6 ) + ' ' + qgsDoubleToString( wgs84BoundingRect.yMaximum(), 6 ) ); + wgs84UpperCornerElement.appendChild( wgs84UpperCornerText ); + wgs84BBoxElement.appendChild( wgs84UpperCornerElement ); + layerParentElem.appendChild( wgs84BBoxElement ); + + // Root Layer Style + QDomElement layerParentStyleElem = doc.createElement( QStringLiteral( "Style" ) ); + layerParentStyleElem.setAttribute( QStringLiteral( "isDefault" ), QStringLiteral( "true" ) ); + QDomElement layerParentStyleIdElem = doc.createElement( QStringLiteral( "ows:Identifier" ) ); + QDomText layerParentStyleIdText = doc.createTextNode( QStringLiteral( "default" ) ); + layerParentStyleIdElem.appendChild( layerParentStyleIdText ); + layerParentStyleElem.appendChild( layerParentStyleIdElem ); + QDomElement layerParentStyleTitleElem = doc.createElement( QStringLiteral( "ows:Title" ) ); + QDomText layerParentStyleTitleText = doc.createTextNode( QStringLiteral( "default" ) ); + layerParentStyleTitleElem.appendChild( layerParentStyleTitleText ); + layerParentStyleElem.appendChild( layerParentStyleTitleElem ); + layerParentElem.appendChild( layerParentStyleElem ); + + QList::iterator tmsIt = tmsList.begin(); + for ( ; tmsIt != tmsList.end(); ++tmsIt ) + { + tileMatrixSet &tms = *tmsIt; + + //wmts:TileMatrixSetLink + QDomElement tmslElement = doc.createElement( QStringLiteral( "TileMatrixSetLink" )/*wmts:TileMatrixSetLink*/ ); + + QDomElement identifierElem = doc.createElement( QStringLiteral( "TileMatrixSet" ) ); + QDomText identifierText = doc.createTextNode( tms.ref ); + identifierElem.appendChild( identifierText ); + tmslElement.appendChild( identifierElem ); + + layerParentElem.appendChild( tmslElement ); + } + + contentsElement.appendChild( layerParentElem ); + + tmsIt = tmsList.begin(); + for ( ; tmsIt != tmsList.end(); ++tmsIt ) + { + tileMatrixSet &tms = *tmsIt; + + //wmts:TileMatrixSet + QDomElement tmsElement = doc.createElement( QStringLiteral( "TileMatrixSet" )/*wmts:TileMatrixSet*/ ); + + QDomElement identifierElem = doc.createElement( QStringLiteral( "ows:Identifier" ) ); + QDomText identifierText = doc.createTextNode( tms.ref ); + identifierElem.appendChild( identifierText ); + tmsElement.appendChild( identifierElem ); + + QDomElement crsElem = doc.createElement( QStringLiteral( "ows:SupportedCRS" ) ); + QDomText crsText = doc.createTextNode( tms.ref ); + crsElem.appendChild( crsText ); + tmsElement.appendChild( crsElem ); + + //wmts:TileMatrix + int tmIdx = 0; + QList::iterator tmIt = tms.tileMatrixList.begin(); + for ( ; tmIt != tms.tileMatrixList.end(); ++tmIt ) + { + tileMatrix &tm = *tmIt; + + QDomElement tmElement = doc.createElement( QStringLiteral( "TileMatrix" )/*wmts:TileMatrix*/ ); + + QDomElement tmIdentifierElem = doc.createElement( QStringLiteral( "ows:Identifier" ) ); + QDomText tmIdentifierText = doc.createTextNode( QString::number( tmIdx ) ); + tmIdentifierElem.appendChild( tmIdentifierText ); + tmElement.appendChild( tmIdentifierElem ); + + QDomElement tmScaleDenomElem = doc.createElement( QStringLiteral( "ScaleDenominator" ) ); + QDomText tmScaleDenomText = doc.createTextNode( qgsDoubleToString( tm.scaleDenominator, 6 ) ); + tmScaleDenomElem.appendChild( tmScaleDenomText ); + tmElement.appendChild( tmScaleDenomElem ); + + QDomElement tmTopLeftCornerElem = doc.createElement( QStringLiteral( "TopLeftCorner" ) ); + QDomText tmTopLeftCornerText = doc.createTextNode( qgsDoubleToString( tm.left, 6 ) + ' ' + qgsDoubleToString( tm.top, 6 ) ); + tmTopLeftCornerElem.appendChild( tmTopLeftCornerText ); + tmElement.appendChild( tmTopLeftCornerElem ); + + QDomElement tmTileWidthElem = doc.createElement( QStringLiteral( "TileWidth" ) ); + QDomText tmTileWidthText = doc.createTextNode( QString::number( 256 ) ); + tmTileWidthElem.appendChild( tmTileWidthText ); + tmElement.appendChild( tmTileWidthElem ); + + QDomElement tmTileHeightElem = doc.createElement( QStringLiteral( "TileHeight" ) ); + QDomText tmTileHeightText = doc.createTextNode( QString::number( 256 ) ); + tmTileHeightElem.appendChild( tmTileHeightText ); + tmElement.appendChild( tmTileHeightElem ); + + QDomElement tmColElem = doc.createElement( QStringLiteral( "MatrixWidth" ) ); + QDomText tmColText = doc.createTextNode( QString::number( tm.col ) ); + tmColElem.appendChild( tmColText ); + tmElement.appendChild( tmColElem ); + + QDomElement tmRowElem = doc.createElement( QStringLiteral( "MatrixHeight" ) ); + QDomText tmRowText = doc.createTextNode( QString::number( tm.row ) ); + tmRowElem.appendChild( tmRowText ); + tmElement.appendChild( tmRowElem ); + + tmsElement.appendChild( tmElement ); + ++tmIdx; + } + + contentsElement.appendChild( tmsElement ); + } + } + + + //End + return contentsElement; + } + +} // namespace QgsWmts + + + diff --git a/src/server/services/wmts/qgswmtsgetcapabilities.h b/src/server/services/wmts/qgswmtsgetcapabilities.h new file mode 100644 index 000000000000..d2dece425ea6 --- /dev/null +++ b/src/server/services/wmts/qgswmtsgetcapabilities.h @@ -0,0 +1,62 @@ +/*************************************************************************** + qgswmtsgecapabilities.h + ------------------------- + begin : July 23 , 2017 + copyright : (C) 2018 by René-Luc D'Hont + email : rldhont at 3liz dot 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 QGSWMTSGETCAPABILITIES_H +#define QGSWMTSGETCAPABILITIES_H + +#include + +namespace QgsWmts +{ + + /** + * Create Contents element for get capabilities document + */ + QDomElement getContentsElement( QDomDocument &doc, QgsServerInterface *serverIface, const QgsProject *project ); + + /** + * Create OperationsMetadata element for get capabilities document + */ + QDomElement getOperationsMetadataElement( QDomDocument &doc, const QgsProject *project, const QgsServerRequest &request ); + + /** + * Create ServiceProvider element for get capabilities document + */ + QDomElement getServiceProviderElement( QDomDocument &doc, const QgsProject *project ); + + /** + * Create ServiceIdentification element for get capabilities document + */ + QDomElement getServiceIdentificationElement( QDomDocument &doc, const QgsProject *project ); + + /** + * Create get capabilities document + */ + QDomDocument createGetCapabilitiesDocument( QgsServerInterface *serverIface, + const QgsProject *project, const QString &version, + const QgsServerRequest &request ); + + /** + * Output WCS GetCapabilities response + */ + void writeGetCapabilities( QgsServerInterface *serverIface, const QgsProject *project, + const QString &version, const QgsServerRequest &request, + QgsServerResponse &response ); + +} // namespace QgsWcs + +#endif + diff --git a/src/server/services/wmts/qgswmtsgettile.cpp b/src/server/services/wmts/qgswmtsgettile.cpp new file mode 100644 index 000000000000..1197c208e0f8 --- /dev/null +++ b/src/server/services/wmts/qgswmtsgettile.cpp @@ -0,0 +1,222 @@ +/*************************************************************************** + qgswmsgetmap.cpp + ------------------------- + begin : July 23 , 2017 + copyright : (C) 2018 by René-Luc D'Hont + email : rldhont at 3liz dot 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 "qgswmtsutils.h" +#include "qgswmtsgettile.h" + +#include + +namespace QgsWmts +{ + + void writeGetTile( QgsServerInterface *serverIface, const QgsProject *project, + const QString &version, const QgsServerRequest &request, + QgsServerResponse &response ) + { + Q_UNUSED( version ); + + QgsServerRequest::Parameters params = request.parameters(); + + //defining Layer + QString layer; + //read Layer + QMap::const_iterator layer_it = params.constFind( QStringLiteral( "LAYER" ) ); + if ( layer_it != params.constEnd() ) + { + layer = layer_it.value(); + } + else + { + throw QgsRequestNotWellFormedException( QStringLiteral( "Layer is mandatory" ) ); + } + + //defining Format + QString format; + //read Format + QMap::const_iterator format_it = params.constFind( QStringLiteral( "FORMAT" ) ); + if ( format_it != params.constEnd() ) + { + format = format_it.value(); + } + else + { + throw QgsRequestNotWellFormedException( QStringLiteral( "Format is mandatory" ) ); + } + + QList< tileMatrixSet > tmsList = getTileMatrixSetList( project ); + if ( tmsList.isEmpty() ) + { + throw QgsServiceException( QStringLiteral( "UnknownError" ), + QStringLiteral( "Service not well configured" ) ); + } + + //defining TileMatrixSet ref + QString tms_ref; + //read TileMatrixSet + QMap::const_iterator tms_ref_it = params.constFind( QStringLiteral( "TILEMATRIXSET" ) ); + if ( tms_ref_it != params.constEnd() ) + { + tms_ref = tms_ref_it.value(); + } + else + { + throw QgsRequestNotWellFormedException( QStringLiteral( "TileMatrixSet is mandatory" ) ); + } + + bool tms_ref_valid = false; + tileMatrixSet tms; + QList::iterator tmsIt = tmsList.begin(); + for ( ; tmsIt != tmsList.end(); ++tmsIt ) + { + tileMatrixSet &tmsi = *tmsIt; + if ( tmsi.ref == tms_ref ) + { + tms_ref_valid = true; + tms = tmsi; + break; + } + } + if ( !tms_ref_valid ) + { + throw QgsRequestNotWellFormedException( QStringLiteral( "TileMatrixSet is unknown" ) ); + } + + bool conversionSuccess = false; + + //difining TileMatrix idx + int tm_idx; + //read TileMatrix + QMap::const_iterator tm_ref_it = params.constFind( QStringLiteral( "TILEMATRIX" ) ); + if ( tm_ref_it != params.constEnd() ) + { + QString tm_ref = tm_ref_it.value(); + tm_idx = tm_ref.toInt( &conversionSuccess ); + if ( !conversionSuccess ) + { + throw QgsRequestNotWellFormedException( QStringLiteral( "TileMatrix is unknown" ) ); + } + } + else + { + throw QgsRequestNotWellFormedException( QStringLiteral( "TileMatrix is mandatory" ) ); + } + if ( tms.tileMatrixList.count() < tm_idx ) + { + throw QgsRequestNotWellFormedException( QStringLiteral( "TileMatrix is unknown" ) ); + } + tileMatrix tm = tms.tileMatrixList.at( tm_idx ); + + //defining TileRow + int tr; + //read TileRow + QMap::const_iterator tr_it = params.constFind( QStringLiteral( "TILEROW" ) ); + if ( tr_it != params.constEnd() ) + { + QString tr_str = tr_it.value(); + conversionSuccess = false; + tr = tr_str.toInt( &conversionSuccess ); + if ( !conversionSuccess ) + { + throw QgsRequestNotWellFormedException( QStringLiteral( "TileRow is unknown" ) ); + } + } + else + { + throw QgsRequestNotWellFormedException( QStringLiteral( "TileRow is mandatory" ) ); + } + if ( tm.row <= tr ) + { + throw QgsRequestNotWellFormedException( QStringLiteral( "TileRow is unknown" ) ); + } + + //defining TileCol + int tc; + //read TileCol + QMap::const_iterator tc_it = params.constFind( QStringLiteral( "TILECOL" ) ); + if ( tc_it != params.constEnd() ) + { + QString tc_str = tc_it.value(); + conversionSuccess = false; + tc = tc_str.toInt( &conversionSuccess ); + if ( !conversionSuccess ) + { + throw QgsRequestNotWellFormedException( QStringLiteral( "TileCol is unknown" ) ); + } + } + else + { + throw QgsRequestNotWellFormedException( QStringLiteral( "TileCol is mandatory" ) ); + } + if ( tm.col <= tc ) + { + throw QgsRequestNotWellFormedException( QStringLiteral( "TileCol is unknown" ) ); + } + + int tileWidth = 256; + int tileHeight = 256; + double res = tm.resolution; + double minx = tm.left + tc * ( tileWidth * res ); + double miny = tm.top - ( tr + 1 ) * ( tileHeight * res ); + double maxx = tm.left + ( tc + 1 ) * ( tileWidth * res ); + double maxy = tm.top - tr * ( tileHeight * res ); + QString bbox; + if ( tms.ref == "EPSG:4326" ) + { + bbox = qgsDoubleToString( miny, 6 ) + ',' + + qgsDoubleToString( minx, 6 ) + ',' + + qgsDoubleToString( maxy, 6 ) + ',' + + qgsDoubleToString( maxx, 6 ); + } + else + { + bbox = qgsDoubleToString( minx, 6 ) + ',' + + qgsDoubleToString( miny, 6 ) + ',' + + qgsDoubleToString( maxx, 6 ) + ',' + + qgsDoubleToString( maxy, 6 ); + } + + QUrlQuery query; + if ( !params.value( QStringLiteral( "MAP" ) ).isEmpty() ) + { + query.addQueryItem( QStringLiteral( "map" ), params.value( QStringLiteral( "MAP" ) ) ); + } + query.addQueryItem( QStringLiteral( "service" ), QStringLiteral( "WMS" ) ); + query.addQueryItem( QStringLiteral( "version" ), QStringLiteral( "1.3.0" ) ); + query.addQueryItem( QStringLiteral( "request" ), QStringLiteral( "GetMap" ) ); + query.addQueryItem( QStringLiteral( "layers" ), layer ); + query.addQueryItem( QStringLiteral( "styles" ), QString() ); + query.addQueryItem( QStringLiteral( "crs" ), tms.ref ); + query.addQueryItem( QStringLiteral( "bbox" ), bbox ); + query.addQueryItem( QStringLiteral( "width" ), QStringLiteral( "256" ) ); + query.addQueryItem( QStringLiteral( "height" ), QStringLiteral( "256" ) ); + query.addQueryItem( QStringLiteral( "format" ), format ); + if ( format.startsWith( QStringLiteral( "image/png" ) ) ) + { + query.addQueryItem( QStringLiteral( "transparent" ), QStringLiteral( "true" ) ); + } + query.addQueryItem( QStringLiteral( "dpi" ), QStringLiteral( "96" ) ); + + QgsServerParameters wmsParams( query ); + QgsServerRequest wmsRequest( "?" + query.query( QUrl::FullyDecoded ) ); + QgsService *service = serverIface->serviceRegistry()->getService( wmsParams.service(), wmsParams.version() ); + service->executeRequest( wmsRequest, response, project ); + } + +} // namespace QgsWmts + + + + diff --git a/src/server/services/wmts/qgswmtsgettile.h b/src/server/services/wmts/qgswmtsgettile.h new file mode 100644 index 000000000000..c71edc3ea398 --- /dev/null +++ b/src/server/services/wmts/qgswmtsgettile.h @@ -0,0 +1,28 @@ +/*************************************************************************** + qgswmsgettile.h + ------------------------- + begin : July 23 , 2017 + copyright : (C) 2018 by René-Luc D'Hont + email : rldhont at 3liz dot 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. * + * * + ***************************************************************************/ + +namespace QgsWmts +{ + + /** + * Output GetTile response + */ + void writeGetTile( QgsServerInterface *serverIface, const QgsProject *project, + const QString &version, const QgsServerRequest &request, + QgsServerResponse &response ); + +} // namespace QgsWmts diff --git a/src/server/services/wmts/qgswmtsserviceexception.h b/src/server/services/wmts/qgswmtsserviceexception.h new file mode 100644 index 000000000000..2d80519a1bb0 --- /dev/null +++ b/src/server/services/wmts/qgswmtsserviceexception.h @@ -0,0 +1,105 @@ +/*************************************************************************** + qgswmtsserviceexception.h + ------------------------ + begin : July 23, 2017 + copyright : (C) 2018 by René-Luc D'Hont + email : rldhont at 3liz dot 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 QGSWMTSSERVICEEXCEPTION_H +#define QGSWMTSSERVICEEXCEPTION_H + +#include + +#include "qgsserverexception.h" + +namespace QgsWmts +{ + + /** + * \ingroup server + * \class QgsWmts::QgsServiceException + * \brief Exception class for WFS services + * \since QGIS 3.0 + */ + class QgsServiceException : public QgsOgcServiceException + { + public: + + /** + * Constructor for QgsServiceException (empty locator attribute). + * \param code Error code name + * \param message Exception message to return to the client + * \param responseCode HTTP error code + */ + QgsServiceException( const QString &code, const QString &message, + int responseCode = 200 ) + : QgsOgcServiceException( code, message, QString(), responseCode, QStringLiteral( "1.0.0" ) ) + {} + + /** + * Constructor for QgsServiceException. + * \param code Error code name + * \param message Exception message to return to the client + * \param locator Locator attribute according to OGC specifications + * \param responseCode HTTP error code + */ + QgsServiceException( const QString &code, const QString &message, const QString &locator, + int responseCode = 200 ) + : QgsOgcServiceException( code, message, locator, responseCode, QStringLiteral( "1.0.0" ) ) + {} + + }; + + /** + * \ingroup server + * \class QgsWmts::QgsSecurityAccessException + * \brief Exception thrown when data access violates access controls + * \since QGIS 3.4 + */ + class QgsSecurityAccessException: public QgsServiceException + { + public: + + /** + * Constructor for QgsSecurityAccessException (Security code name). + * \param message Exception message to return to the client + * \param locator Locator attribute according to OGC specifications + */ + QgsSecurityAccessException( const QString &message, const QString &locator = QString() ) + : QgsServiceException( QStringLiteral( "Security" ), message, locator, 403 ) + {} + }; + + /** + * \ingroup server + * \class QgsWmts::QgsRequestNotWellFormedException + * \brief Exception thrown in case of malformed request + * \since QGIS 3.4 + */ + class QgsRequestNotWellFormedException: public QgsServiceException + { + public: + + /** + * Constructor for QgsRequestNotWellFormedException (RequestNotWellFormed code name). + * \param message Exception message to return to the client + * \param locator Locator attribute according to OGC specifications + */ + QgsRequestNotWellFormedException( const QString &message, const QString &locator = QString() ) + : QgsServiceException( QStringLiteral( "RequestNotWellFormed" ), message, locator, 400 ) + {} + }; +} // namespace QgsWmts + +#endif + diff --git a/src/server/services/wmts/qgswmtsutils.cpp b/src/server/services/wmts/qgswmtsutils.cpp new file mode 100644 index 000000000000..615104f42dd7 --- /dev/null +++ b/src/server/services/wmts/qgswmtsutils.cpp @@ -0,0 +1,269 @@ +/*************************************************************************** + qgswmtsutils.cpp + ------------------------- + begin : July 23, 2018 + copyright : (C) 2018 by René-Luc D'Hont + email : rldhont at 3liz dot 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 "qgswmtsutils.h" +#include "qgsconfigcache.h" +#include "qgsserverprojectutils.h" + +#include "qgsproject.h" +#include "qgsexception.h" +#include "qgsmapserviceexception.h" +#include "qgscoordinatereferencesystem.h" +#include "qgssettings.h" + + +namespace QgsWmts +{ + QString implementationVersion() + { + return QStringLiteral( "1.0.0" ); + } + + + QString serviceUrl( const QgsServerRequest &request, const QgsProject *project ) + { + QString href; + if ( project ) + { + href = QgsServerProjectUtils::wmsServiceUrl( *project ); + } + + // Build default url + if ( href.isEmpty() ) + { + QUrl url = request.url(); + QUrlQuery q( url ); + + q.removeAllQueryItems( QStringLiteral( "REQUEST" ) ); + q.removeAllQueryItems( QStringLiteral( "VERSION" ) ); + q.removeAllQueryItems( QStringLiteral( "SERVICE" ) ); + q.removeAllQueryItems( QStringLiteral( "_DC" ) ); + + url.setQuery( q ); + href = url.toString( QUrl::FullyDecoded ); + + } + + return href; + } + + QgsRectangle parseBbox( const QString &bboxStr ) + { + QStringList lst = bboxStr.split( ',' ); + if ( lst.count() != 4 ) + return QgsRectangle(); + + double d[4]; + bool ok; + for ( int i = 0; i < 4; i++ ) + { + lst[i].replace( ' ', '+' ); + d[i] = lst[i].toDouble( &ok ); + if ( !ok ) + return QgsRectangle(); + } + return QgsRectangle( d[0], d[1], d[2], d[3] ); + } + + tileMatrixSet getTileMatrixSet( tileMatrixInfo tmi, double minScale ) + { + int DOTS_PER_INCH = 72; + double METERS_PER_INCH = 0.02540005080010160020; + QMap< QString, double> INCHES_PER_UNIT; + INCHES_PER_UNIT["inches"] = 1.0; + INCHES_PER_UNIT["ft"] = 12.0; + INCHES_PER_UNIT["mi"] = 63360.0; + INCHES_PER_UNIT["m"] = 39.37; + INCHES_PER_UNIT["km"] = 39370.0; + INCHES_PER_UNIT["dd"] = 4374754.0; + INCHES_PER_UNIT["yd"] = 36.0; + INCHES_PER_UNIT["in"] = INCHES_PER_UNIT["inches"]; + INCHES_PER_UNIT["degrees"] = INCHES_PER_UNIT["dd"]; + INCHES_PER_UNIT["nmi"] = 1852.0 * INCHES_PER_UNIT["m"]; + INCHES_PER_UNIT["cm"] = INCHES_PER_UNIT["m"] / 100.0; + INCHES_PER_UNIT["mm"] = INCHES_PER_UNIT["m"] / 1000.0; + + int tileWidth = 256; + int tileHeight = 256; + + QList< tileMatrix > tileMatrixList; + double scaleDenominator = tmi.scaleDenominator; + QgsRectangle extent = tmi.extent; + QString unit = tmi.unit; + + while ( scaleDenominator >= minScale ) + { + double scale = scaleDenominator; + double res = 0.00028 * scale / METERS_PER_INCH / INCHES_PER_UNIT[ unit ]; + int col = std::round( ( extent.xMaximum() - extent.xMinimum() ) / ( tileWidth * res ) ); + int row = std::round( ( extent.yMaximum() - extent.yMinimum() ) / ( tileHeight * res ) ); + double left = ( extent.xMinimum() + ( extent.xMaximum() - extent.xMinimum() ) / 2.0 ) - ( col / 2.0 ) * ( tileWidth * res ); + double top = ( extent.yMinimum() + ( extent.yMaximum() - extent.yMinimum() ) / 2.0 ) + ( row / 2.0 ) * ( tileHeight * res ); + + tileMatrix tm; + tm.resolution = res; + tm.scaleDenominator = scale; + tm.col = col; + tm.row = row; + tm.left = std::max( left, extent.xMinimum() ); + tm.top = std::min( top, extent.yMaximum() ); + tileMatrixList.append( tm ); + + scaleDenominator = scale / 2; + } + + tileMatrixSet tms; + tms.ref = tmi.ref; + tms.extent = extent; + tms.unit = unit; + tms.tileMatrixList = tileMatrixList; + + return tms; + } + + QList< tileMatrixSet > getTileMatrixSetList( const QgsProject *project ) + { + QList< tileMatrixSet > tmsList; + + double minScale = -1.0; + double maxScale = -1.0; + + + QgsRectangle projRect = QgsServerProjectUtils::wmsExtent( *project ); + QgsCoordinateReferenceSystem projCrs = project->crs(); + + // default scales + QgsSettings settings; + QStringList scaleList = settings.value( QStringLiteral( "Map/scales" ), PROJECT_SCALES ).toString().split( ',' ); + //load project scales + bool projectScales = project->readBoolEntry( QStringLiteral( "Scales" ), QStringLiteral( "/useProjectScales" ) ); + if ( projectScales ) + { + scaleList = project->readListEntry( QStringLiteral( "Scales" ), QStringLiteral( "/ScalesList" ) ); + } + // get min and max scales + if ( !scaleList.isEmpty() ) + { + Q_FOREACH ( const QString &scaleText, scaleList ) + { + double scaleValue = scaleText.toDouble(); + if ( minScale == -1.0 && maxScale == -1.0 ) + { + minScale = scaleValue; + maxScale = scaleValue; + } + else + { + if ( scaleValue < minScale ) + { + minScale = scaleValue; + } + if ( scaleValue > maxScale ) + { + maxScale = scaleValue; + } + } + } + } + else + { + minScale = 5000.0; + maxScale = 1000000.0; + } + if ( minScale < 500.0 ) + { + minScale = 500.0; + } + if ( minScale == maxScale || minScale > maxScale ) + { + maxScale = minScale * 2.0; + } + + QStringList crsList = QgsServerProjectUtils::wmsOutputCrsList( *project ); + Q_FOREACH ( const QString &crsText, crsList ) + { + if ( crsText == "EPSG:3857" ) + { + tileMatrixInfo tmi3857; + tmi3857.ref = "EPSG:3857"; + tmi3857.extent = QgsRectangle( -20037508.3427892480, -20037508.3427892480, 20037508.3427892480, 20037508.3427892480 ); + tmi3857.scaleDenominator = 559082264.0287179; + tmi3857.unit = "m"; + + tmsList.append( getTileMatrixSet( tmi3857, minScale ) ); + } + else if ( crsText == "EPSG:4326" ) + { + tileMatrixInfo tmi4326; + tmi4326.ref = "EPSG:4326"; + tmi4326.extent = QgsRectangle( -180, -90, 180, 90 ); + tmi4326.scaleDenominator = 279541132.0143588675418869; + tmi4326.unit = "dd"; + + tmsList.append( getTileMatrixSet( tmi4326, minScale ) ); + } + else + { + tileMatrixInfo tmi; + tmi.ref = crsText; + + QgsCoordinateReferenceSystem crs = QgsCoordinateReferenceSystem::fromOgcWmsCrs( crsText ); + Q_NOWARN_DEPRECATED_PUSH + QgsCoordinateTransform crsTransform( projCrs, crs ); + Q_NOWARN_DEPRECATED_POP + try + { + tmi.extent = crsTransform.transformBoundingBox( projRect ); + } + catch ( QgsCsException &cse ) + { + Q_UNUSED( cse ); + continue; + } + + tmi.scaleDenominator = maxScale; + + QgsUnitTypes::DistanceUnit mapUnits = crs.mapUnits(); + if ( mapUnits == QgsUnitTypes::DistanceMeters ) + tmi.unit = "m"; + else if ( mapUnits == QgsUnitTypes::DistanceKilometers ) + tmi.unit = "km"; + else if ( mapUnits == QgsUnitTypes::DistanceFeet ) + tmi.unit = "ft"; + else if ( mapUnits == QgsUnitTypes::DistanceNauticalMiles ) + tmi.unit = "nmi"; + else if ( mapUnits == QgsUnitTypes::DistanceYards ) + tmi.unit = "yd"; + else if ( mapUnits == QgsUnitTypes::DistanceMiles ) + tmi.unit = "mi"; + else if ( mapUnits == QgsUnitTypes::DistanceDegrees ) + tmi.unit = "dd"; + else if ( mapUnits == QgsUnitTypes::DistanceCentimeters ) + tmi.unit = "cm"; + else if ( mapUnits == QgsUnitTypes::DistanceMillimeters ) + tmi.unit = "mm"; + + tmsList.append( getTileMatrixSet( tmi, minScale ) ); + } + } + + return tmsList; + } + +} // namespace QgsWmts + + diff --git a/src/server/services/wmts/qgswmtsutils.h b/src/server/services/wmts/qgswmtsutils.h new file mode 100644 index 000000000000..a439958f0d3e --- /dev/null +++ b/src/server/services/wmts/qgswmtsutils.h @@ -0,0 +1,101 @@ +/*************************************************************************** + qgswmtsutils.h + + Define WMTS service utility functions + ------------------------------------ + begin : July 23 , 2017 + copyright : (C) 2018 by René-Luc D'Hont + email : rldhont at 3liz dot 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 QGSWMTSUTILS_H +#define QGSWMTSUTILS_H + +#include "qgsmodule.h" +#include "qgswmtsserviceexception.h" + +#include + +/** + * \ingroup server + * WMTS implementation + */ + +//! WMTS implementation +namespace QgsWmts +{ + + struct tileMatrixInfo + { + QString ref; + + QgsRectangle extent; + + double scaleDenominator; + + QString unit; + }; + + struct tileMatrix + { + double resolution; + + double scaleDenominator; + + int col; + + int row; + + double left; + + double top; + }; + + struct tileMatrixSet + { + QString ref; + + QgsRectangle extent; + + QString unit; + + QList< tileMatrix > tileMatrixList; + }; + + /** + * Returns the highest version supported by this implementation + */ + QString implementationVersion(); + + /** + * Service URL string + */ + QString serviceUrl( const QgsServerRequest &request, const QgsProject *project ); + + /** + * Parse bounding box + */ + //XXX At some point, should be moved to common library + QgsRectangle parseBbox( const QString &bboxStr ); + + // Define namespaces used in WMTS documents + const QString WMTS_NAMESPACE = QStringLiteral( "http://www.opengis.net/wmts/1.0" ); + const QString GML_NAMESPACE = QStringLiteral( "http://www.opengis.net/gml" ); + const QString OWS_NAMESPACE = QStringLiteral( "http://www.opengis.net/ows/1.1" ); + + tileMatrixSet getTileMatrixSet( tileMatrixInfo tmi ); + QList< tileMatrixSet > getTileMatrixSetList( const QgsProject *project ); + +} // namespace QgsWmts + +#endif + + From 912effaaf9d974f8d725a76a12311a1c409b1236 Mon Sep 17 00:00:00 2001 From: rldhont Date: Fri, 27 Jul 2018 10:33:27 +0200 Subject: [PATCH 05/33] [Server][Feature][needs-docs] Create WMTS service UI The user can config the WMTS for Server by selecting: project, group or layer published through WMTS. --- src/app/qgsprojectproperties.cpp | 81 ++++++++++++++++++++++++++++++ src/app/qgsprojectproperties.h | 3 ++ src/ui/qgsprojectpropertiesbase.ui | 35 +++++++++++++ 3 files changed, 119 insertions(+) diff --git a/src/app/qgsprojectproperties.cpp b/src/app/qgsprojectproperties.cpp index 0b058fb36b11..341f3efe7fa0 100644 --- a/src/app/qgsprojectproperties.cpp +++ b/src/app/qgsprojectproperties.cpp @@ -55,6 +55,7 @@ #include "qgslayertreemodel.h" #include "qgsunittypes.h" #include "qgstablewidgetitem.h" +#include "qgstreewidgetitem.h" #include "qgslayertree.h" #include "qgsprintlayout.h" #include "qgsmetadatawidget.h" @@ -661,6 +662,22 @@ QgsProjectProperties::QgsProjectProperties( QgsMapCanvas *mapCanvas, QWidget *pa mWMSImageQualitySpinBox->setValue( imageQuality ); } + bool wmtsProject = QgsProject::instance()->readBoolEntry( QStringLiteral( "WMTSLayers" ), QStringLiteral( "Project" ) ); + QStringList wmtsGroupNameList = QgsProject::instance()->readListEntry( QStringLiteral( "WMTSLayers" ), QStringLiteral( "Group" ) ); + QStringList wmtsLayerIdList = QgsProject::instance()->readListEntry( QStringLiteral( "WMTSLayers" ), QStringLiteral( "Layer" ) ); + + QgsTreeWidgetItem *projItem = new QgsTreeWidgetItem( QStringList() << QStringLiteral( "Project" ) << QLatin1String( "" ) ); + projItem->setFlags( projItem->flags() | Qt::ItemIsUserCheckable | Qt::ItemIsSelectable ); + if ( wmtsProject ) + projItem->setCheckState( 1, Qt::Checked ); + else + projItem->setCheckState( 1, Qt::Unchecked ); + projItem->setData( 0, Qt::UserRole, "project" ); + twWmtsLayers->addTopLevelItem( projItem ); + popupulateWmtsTree( QgsProject::instance()->layerTreeRoot(), projItem, wmtsGroupNameList, wmtsLayerIdList ); + projItem->setExpanded( true ); + twWmtsLayers->header()->resizeSections( QHeaderView::ResizeToContents ); + mWFSUrlLineEdit->setText( QgsProject::instance()->readEntry( QStringLiteral( "WFSUrl" ), QStringLiteral( "/" ), QLatin1String( "" ) ) ); QStringList wfsLayerIdList = QgsProject::instance()->readListEntry( QStringLiteral( "WFSLayers" ), QStringLiteral( "/" ) ); QStringList wfstUpdateLayerIdList = QgsProject::instance()->readListEntry( QStringLiteral( "WFSTLayers" ), QStringLiteral( "Update" ) ); @@ -1221,6 +1238,32 @@ void QgsProjectProperties::apply() QgsProject::instance()->writeEntry( QStringLiteral( "WMSImageQuality" ), QStringLiteral( "/" ), imageQualityValue ); } + bool wmtsProject = false; + QStringList wmtsGroupList; + QStringList wmtsLayerList; + Q_FOREACH ( const QTreeWidgetItem *item, twWmtsLayers->findItems( "", Qt::MatchContains | Qt::MatchRecursive, 1 ) ) + { + if ( !item->checkState( 1 ) ) + continue; + + QString t = item->data( 0, Qt::UserRole ).toString(); + if ( t == "project" ) + { + wmtsProject = true; + } + else if ( t == "group" ) + { + wmtsGroupList << item->data( 0, Qt::UserRole + 1 ).toString(); + } + else if ( t == "layer" ) + { + wmtsLayerList << item->data( 0, Qt::UserRole + 1 ).toString(); + } + } + QgsProject::instance()->writeEntry( QStringLiteral( "WMTSLayers" ), QStringLiteral( "Project" ), wmtsProject ); + QgsProject::instance()->writeEntry( QStringLiteral( "WMTSLayers" ), QStringLiteral( "Group" ), wmtsGroupList ); + QgsProject::instance()->writeEntry( QStringLiteral( "WMTSLayers" ), QStringLiteral( "Layer" ), wmtsLayerList ); + QgsProject::instance()->writeEntry( QStringLiteral( "WFSUrl" ), QStringLiteral( "/" ), mWFSUrlLineEdit->text() ); QStringList wfsLayerList; QStringList wfstUpdateLayerList; @@ -1912,6 +1955,44 @@ void QgsProjectProperties::resetPythonMacros() "def closeProject():\n pass\n" ); } +void QgsProjectProperties::popupulateWmtsTree( QgsLayerTreeGroup *treeGroup, QgsTreeWidgetItem *treeItem, const QStringList &groupNames, const QStringList &layerIds ) +{ + QList< QgsLayerTreeNode * > treeGroupChildren = treeGroup->children(); + for ( int i = 0; i < treeGroupChildren.size(); ++i ) + { + QgsTreeWidgetItem *childItem; + childItem->setFlags( childItem->flags() | Qt::ItemIsUserCheckable | Qt::ItemIsSelectable ); + QgsLayerTreeNode *treeNode = treeGroupChildren.at( i ); + if ( treeNode->nodeType() == QgsLayerTreeNode::NodeGroup ) + { + QgsLayerTreeGroup *treeGroupChild = static_cast( treeNode ); + childItem = new QgsTreeWidgetItem( QStringList() << treeGroupChild->name() << QLatin1String( "" ) ); + if ( groupNames.contains( treeGroupChild->name() ) ) + childItem->setCheckState( 1, Qt::Checked ); + else + childItem->setCheckState( 1, Qt::Unchecked ); + childItem->setData( 0, Qt::UserRole, "group" ); + childItem->setData( 0, Qt::UserRole + 1, treeGroupChild->name() ); + treeItem->addChild( childItem ); + popupulateWmtsTree( treeGroupChild, childItem, groupNames, layerIds ); + treeItem->setExpanded( true ); + } + else + { + QgsLayerTreeLayer *treeLayer = static_cast( treeNode ); + QgsMapLayer *l = treeLayer->layer(); + childItem = new QgsTreeWidgetItem( QStringList() << l->name() << QLatin1String( "" ) ); + if ( layerIds.contains( l->id() ) ) + childItem->setCheckState( 1, Qt::Checked ); + else + childItem->setCheckState( 1, Qt::Unchecked ); + childItem->setData( 0, Qt::UserRole, "layer" ); + childItem->setData( 0, Qt::UserRole + 1, l->id() ); + treeItem->addChild( childItem ); + } + } +} + void QgsProjectProperties::checkOWS( QgsLayerTreeGroup *treeGroup, QStringList &owsNames, QStringList &encodingMessages ) { QList< QgsLayerTreeNode * > treeGroupChildren = treeGroup->children(); diff --git a/src/app/qgsprojectproperties.h b/src/app/qgsprojectproperties.h index f691e307639b..7eb39ff51e2f 100644 --- a/src/app/qgsprojectproperties.h +++ b/src/app/qgsprojectproperties.h @@ -31,6 +31,7 @@ class QgsStyle; class QgsExpressionContext; class QgsLayerTreeGroup; class QgsMetadataWidget; +class QgsTreeWidgetItem; /** * Dialog to set project level properties @@ -213,6 +214,8 @@ class APP_EXPORT QgsProjectProperties : public QgsOptionsDialogBase, private Ui: QList mEllipsoidList; int mEllipsoidIndex; + //! populate WMTS tree + void popupulateWmtsTree( QgsLayerTreeGroup *treeGroup, QgsTreeWidgetItem *treeItem, const QStringList &groupNames, const QStringList &layerIds ); //! Check OWS configuration void checkOWS( QgsLayerTreeGroup *treeGroup, QStringList &owsNames, QStringList &encodingMessages ); diff --git a/src/ui/qgsprojectpropertiesbase.ui b/src/ui/qgsprojectpropertiesbase.ui index f66b3794b99c..1b28468fe241 100644 --- a/src/ui/qgsprojectpropertiesbase.ui +++ b/src/ui/qgsprojectpropertiesbase.ui @@ -2460,6 +2460,41 @@ + + + + + 0 + 3 + + + + WMTS capabilities + + + + + + + 0 + 0 + + + + + Layer + + + + + Published + + + + + + + From 71c7ce1ca6bb86d63cf26df8472e7ba523db2cec Mon Sep 17 00:00:00 2001 From: rldhont Date: Fri, 27 Jul 2018 16:59:25 +0200 Subject: [PATCH 06/33] [Server][Feature][needs-docs] Update WMTS service UI: manage output format The user can choose the image format for WMTS tiles. --- src/app/qgsprojectproperties.cpp | 126 +++++++++++++++++++++++------ src/app/qgsprojectproperties.h | 7 +- src/ui/qgsprojectpropertiesbase.ui | 10 +++ 3 files changed, 117 insertions(+), 26 deletions(-) diff --git a/src/app/qgsprojectproperties.cpp b/src/app/qgsprojectproperties.cpp index 341f3efe7fa0..d579f8a4d1bb 100644 --- a/src/app/qgsprojectproperties.cpp +++ b/src/app/qgsprojectproperties.cpp @@ -663,20 +663,47 @@ QgsProjectProperties::QgsProjectProperties( QgsMapCanvas *mapCanvas, QWidget *pa } bool wmtsProject = QgsProject::instance()->readBoolEntry( QStringLiteral( "WMTSLayers" ), QStringLiteral( "Project" ) ); + bool wmtsPngProject = QgsProject::instance()->readBoolEntry( QStringLiteral( "WMTSPngLayers" ), QStringLiteral( "Project" ) ); + bool wmtsJpegProject = QgsProject::instance()->readBoolEntry( QStringLiteral( "WMTSJpegLayers" ), QStringLiteral( "Project" ) ); QStringList wmtsGroupNameList = QgsProject::instance()->readListEntry( QStringLiteral( "WMTSLayers" ), QStringLiteral( "Group" ) ); + QStringList wmtsPngGroupNameList = QgsProject::instance()->readListEntry( QStringLiteral( "WMTSPngLayers" ), QStringLiteral( "Group" ) ); + QStringList wmtsJpegGroupNameList = QgsProject::instance()->readListEntry( QStringLiteral( "WMTSJpegLayers" ), QStringLiteral( "Group" ) ); QStringList wmtsLayerIdList = QgsProject::instance()->readListEntry( QStringLiteral( "WMTSLayers" ), QStringLiteral( "Layer" ) ); + QStringList wmtsPngLayerIdList = QgsProject::instance()->readListEntry( QStringLiteral( "WMTSPngLayers" ), QStringLiteral( "Layer" ) ); + QStringList wmtsJpegLayerIdList = QgsProject::instance()->readListEntry( QStringLiteral( "WMTSJpegLayers" ), QStringLiteral( "Layer" ) ); QgsTreeWidgetItem *projItem = new QgsTreeWidgetItem( QStringList() << QStringLiteral( "Project" ) << QLatin1String( "" ) ); projItem->setFlags( projItem->flags() | Qt::ItemIsUserCheckable | Qt::ItemIsSelectable ); - if ( wmtsProject ) - projItem->setCheckState( 1, Qt::Checked ); - else - projItem->setCheckState( 1, Qt::Unchecked ); + projItem->setCheckState( 1, wmtsProject ? Qt::Checked : Qt::Unchecked ); + projItem->setCheckState( 2, wmtsPngProject ? Qt::Checked : Qt::Unchecked ); + projItem->setCheckState( 3, wmtsJpegProject ? Qt::Checked : Qt::Unchecked ); projItem->setData( 0, Qt::UserRole, "project" ); twWmtsLayers->addTopLevelItem( projItem ); - popupulateWmtsTree( QgsProject::instance()->layerTreeRoot(), projItem, wmtsGroupNameList, wmtsLayerIdList ); + populateWmtsTree( QgsProject::instance()->layerTreeRoot(), projItem ); projItem->setExpanded( true ); twWmtsLayers->header()->resizeSections( QHeaderView::ResizeToContents ); + Q_FOREACH ( QTreeWidgetItem *item, twWmtsLayers->findItems( "", Qt::MatchContains | Qt::MatchRecursive, 1 ) ) + { + /*if ( !item->checkState( 1 ) ) + continue;*/ + + QString t = item->data( 0, Qt::UserRole ).toString(); + if ( t == "group" ) + { + QString gName = item->data( 0, Qt::UserRole + 1 ).toString(); + item->setCheckState( 1, wmtsGroupNameList.contains( gName ) ? Qt::Checked : Qt::Unchecked ); + item->setCheckState( 2, wmtsPngGroupNameList.contains( gName ) ? Qt::Checked : Qt::Unchecked ); + item->setCheckState( 3, wmtsJpegGroupNameList.contains( gName ) ? Qt::Checked : Qt::Unchecked ); + } + else if ( t == "layer" ) + { + QString lId = item->data( 0, Qt::UserRole + 1 ).toString(); + item->setCheckState( 1, wmtsLayerIdList.contains( lId ) ? Qt::Checked : Qt::Unchecked ); + item->setCheckState( 2, wmtsPngLayerIdList.contains( lId ) ? Qt::Checked : Qt::Unchecked ); + item->setCheckState( 3, wmtsJpegLayerIdList.contains( lId ) ? Qt::Checked : Qt::Unchecked ); + } + } + connect( twWmtsLayers, &QTreeWidget::itemChanged, this, &QgsProjectProperties::twWmtsItemChanged ); mWFSUrlLineEdit->setText( QgsProject::instance()->readEntry( QStringLiteral( "WFSUrl" ), QStringLiteral( "/" ), QLatin1String( "" ) ) ); QStringList wfsLayerIdList = QgsProject::instance()->readListEntry( QStringLiteral( "WFSLayers" ), QStringLiteral( "/" ) ); @@ -1239,8 +1266,14 @@ void QgsProjectProperties::apply() } bool wmtsProject = false; + bool wmtsPngProject = false; + bool wmtsJpegProject = false; QStringList wmtsGroupList; + QStringList wmtsPngGroupList; + QStringList wmtsJpegGroupList; QStringList wmtsLayerList; + QStringList wmtsPngLayerList; + QStringList wmtsJpegLayerList; Q_FOREACH ( const QTreeWidgetItem *item, twWmtsLayers->findItems( "", Qt::MatchContains | Qt::MatchRecursive, 1 ) ) { if ( !item->checkState( 1 ) ) @@ -1250,19 +1283,37 @@ void QgsProjectProperties::apply() if ( t == "project" ) { wmtsProject = true; + wmtsPngProject = item->checkState( 2 ); + wmtsJpegProject = item->checkState( 3 ); } else if ( t == "group" ) { - wmtsGroupList << item->data( 0, Qt::UserRole + 1 ).toString(); + QString gName = item->data( 0, Qt::UserRole + 1 ).toString(); + wmtsGroupList << gName; + if ( item->checkState( 2 ) ) + wmtsPngGroupList << gName; + if ( item->checkState( 3 ) ) + wmtsJpegGroupList << gName; } else if ( t == "layer" ) { - wmtsLayerList << item->data( 0, Qt::UserRole + 1 ).toString(); + QString lId = item->data( 0, Qt::UserRole + 1 ).toString(); + wmtsLayerList << lId; + if ( item->checkState( 2 ) ) + wmtsPngLayerList << lId; + if ( item->checkState( 3 ) ) + wmtsJpegLayerList << lId; } } QgsProject::instance()->writeEntry( QStringLiteral( "WMTSLayers" ), QStringLiteral( "Project" ), wmtsProject ); + QgsProject::instance()->writeEntry( QStringLiteral( "WMTSPngLayers" ), QStringLiteral( "Project" ), wmtsPngProject ); + QgsProject::instance()->writeEntry( QStringLiteral( "WMTSJpegLayers" ), QStringLiteral( "Project" ), wmtsJpegProject ); QgsProject::instance()->writeEntry( QStringLiteral( "WMTSLayers" ), QStringLiteral( "Group" ), wmtsGroupList ); + QgsProject::instance()->writeEntry( QStringLiteral( "WMTSPngLayers" ), QStringLiteral( "Group" ), wmtsPngGroupList ); + QgsProject::instance()->writeEntry( QStringLiteral( "WMTSJpegLayers" ), QStringLiteral( "Group" ), wmtsJpegGroupList ); QgsProject::instance()->writeEntry( QStringLiteral( "WMTSLayers" ), QStringLiteral( "Layer" ), wmtsLayerList ); + QgsProject::instance()->writeEntry( QStringLiteral( "WMTSPngLayers" ), QStringLiteral( "Layer" ), wmtsPngLayerList ); + QgsProject::instance()->writeEntry( QStringLiteral( "WMTSJpegLayers" ), QStringLiteral( "Layer" ), wmtsJpegLayerList ); QgsProject::instance()->writeEntry( QStringLiteral( "WFSUrl" ), QStringLiteral( "/" ), mWFSUrlLineEdit->text() ); QStringList wfsLayerList; @@ -1358,6 +1409,31 @@ void QgsProjectProperties::showProjectionsTab() mOptionsListWidget->setCurrentRow( 2 ); } +void QgsProjectProperties::twWmtsItemChanged( QTreeWidgetItem *item, int column ) +{ + if ( column == 1 && !item->checkState( 1 ) ) + { + item->setCheckState( 2, Qt::Unchecked ); + item->setCheckState( 3, Qt::Unchecked ); + } + else if ( column == 1 && item->checkState( 1 ) && + !item->checkState( 2 ) && !item->checkState( 3 ) ) + { + item->setCheckState( 2, Qt::Checked ); + item->setCheckState( 3, Qt::Checked ); + } + else if ( ( column == 2 && item->checkState( 2 ) ) || + ( column == 3 && item->checkState( 3 ) ) ) + { + item->setCheckState( 1, Qt::Checked ); + } + else if ( ( column == 2 && !item->checkState( 2 ) && !item->checkState( 3 ) ) || + ( column == 3 && !item->checkState( 2 ) && !item->checkState( 3 ) ) ) + { + item->setCheckState( 1, Qt::Unchecked ); + } +} + void QgsProjectProperties::cbxWFSPubliedStateChanged( int aIdx ) { QCheckBox *cb = qobject_cast( twWFSLayers->cellWidget( aIdx, 1 ) ); @@ -1955,39 +2031,39 @@ void QgsProjectProperties::resetPythonMacros() "def closeProject():\n pass\n" ); } -void QgsProjectProperties::popupulateWmtsTree( QgsLayerTreeGroup *treeGroup, QgsTreeWidgetItem *treeItem, const QStringList &groupNames, const QStringList &layerIds ) +void QgsProjectProperties::populateWmtsTree( const QgsLayerTreeGroup *treeGroup, QgsTreeWidgetItem *treeItem ) { - QList< QgsLayerTreeNode * > treeGroupChildren = treeGroup->children(); - for ( int i = 0; i < treeGroupChildren.size(); ++i ) + Q_FOREACH ( QgsLayerTreeNode *treeNode, treeGroup->children() ) { - QgsTreeWidgetItem *childItem; - childItem->setFlags( childItem->flags() | Qt::ItemIsUserCheckable | Qt::ItemIsSelectable ); - QgsLayerTreeNode *treeNode = treeGroupChildren.at( i ); + QgsTreeWidgetItem *childItem = nullptr; if ( treeNode->nodeType() == QgsLayerTreeNode::NodeGroup ) { QgsLayerTreeGroup *treeGroupChild = static_cast( treeNode ); - childItem = new QgsTreeWidgetItem( QStringList() << treeGroupChild->name() << QLatin1String( "" ) ); - if ( groupNames.contains( treeGroupChild->name() ) ) - childItem->setCheckState( 1, Qt::Checked ); - else - childItem->setCheckState( 1, Qt::Unchecked ); + QString gName = treeGroupChild->name(); + + childItem = new QgsTreeWidgetItem( QStringList() << gName ); + childItem->setFlags( childItem->flags() | Qt::ItemIsUserCheckable | Qt::ItemIsSelectable ); + childItem->setData( 0, Qt::UserRole, "group" ); - childItem->setData( 0, Qt::UserRole + 1, treeGroupChild->name() ); + childItem->setData( 0, Qt::UserRole + 1, gName ); + treeItem->addChild( childItem ); - popupulateWmtsTree( treeGroupChild, childItem, groupNames, layerIds ); + + populateWmtsTree( treeGroupChild, childItem ); + treeItem->setExpanded( true ); } else { QgsLayerTreeLayer *treeLayer = static_cast( treeNode ); QgsMapLayer *l = treeLayer->layer(); - childItem = new QgsTreeWidgetItem( QStringList() << l->name() << QLatin1String( "" ) ); - if ( layerIds.contains( l->id() ) ) - childItem->setCheckState( 1, Qt::Checked ); - else - childItem->setCheckState( 1, Qt::Unchecked ); + + childItem = new QgsTreeWidgetItem( QStringList() << l->name() ); + childItem->setFlags( childItem->flags() | Qt::ItemIsUserCheckable | Qt::ItemIsSelectable ); + childItem->setData( 0, Qt::UserRole, "layer" ); childItem->setData( 0, Qt::UserRole + 1, l->id() ); + treeItem->addChild( childItem ); } } diff --git a/src/app/qgsprojectproperties.h b/src/app/qgsprojectproperties.h index 7eb39ff51e2f..60a51037b59b 100644 --- a/src/app/qgsprojectproperties.h +++ b/src/app/qgsprojectproperties.h @@ -136,6 +136,11 @@ class APP_EXPORT QgsProjectProperties : public QgsOptionsDialogBase, private Ui: void pbtnStyleFill_clicked(); void pbtnStyleColorRamp_clicked(); + /** + * Slot to link WMTS checkboxes in tree widget + */ + void twWmtsItemChanged( QTreeWidgetItem *item, int column ); + /** * Slot to link WFS checkboxes */ @@ -215,7 +220,7 @@ class APP_EXPORT QgsProjectProperties : public QgsOptionsDialogBase, private Ui: int mEllipsoidIndex; //! populate WMTS tree - void popupulateWmtsTree( QgsLayerTreeGroup *treeGroup, QgsTreeWidgetItem *treeItem, const QStringList &groupNames, const QStringList &layerIds ); + void populateWmtsTree( const QgsLayerTreeGroup *treeGroup, QgsTreeWidgetItem *treeItem ); //! Check OWS configuration void checkOWS( QgsLayerTreeGroup *treeGroup, QStringList &owsNames, QStringList &encodingMessages ); diff --git a/src/ui/qgsprojectpropertiesbase.ui b/src/ui/qgsprojectpropertiesbase.ui index 1b28468fe241..da42f424d332 100644 --- a/src/ui/qgsprojectpropertiesbase.ui +++ b/src/ui/qgsprojectpropertiesbase.ui @@ -2490,6 +2490,16 @@ Published + + + PNG + + + + + JPEG + + From dc7e8e4b81a83d67561abc65e8a2e24ae9d95a6b Mon Sep 17 00:00:00 2001 From: rldhont Date: Mon, 30 Jul 2018 10:55:49 +0200 Subject: [PATCH 07/33] [Server][Feature][needs-docs] Update WMTS service : use config Reuse the project configuration in the Server WMTS Service. --- .../services/wmts/qgswmtsgetcapabilities.cpp | 298 +++++++++++++----- src/server/services/wmts/qgswmtsutils.h | 13 + 2 files changed, 237 insertions(+), 74 deletions(-) diff --git a/src/server/services/wmts/qgswmtsgetcapabilities.cpp b/src/server/services/wmts/qgswmtsgetcapabilities.cpp index 96d03f7c8db2..df5c4fd6edf4 100644 --- a/src/server/services/wmts/qgswmtsgetcapabilities.cpp +++ b/src/server/services/wmts/qgswmtsgetcapabilities.cpp @@ -22,6 +22,9 @@ #include "qgsexception.h" #include "qgsmapserviceexception.h" #include "qgscoordinatereferencesystem.h" +#include "qgslayertree.h" +#include "qgslayertreemodel.h" +#include "qgslayertreemodellegendnode.h" #include @@ -320,95 +323,243 @@ namespace QgsWmts QList< tileMatrixSet > tmsList = getTileMatrixSetList( project ); if ( !tmsList.isEmpty() ) { - QDomElement layerParentElem = doc.createElement( QStringLiteral( "Layer" ) ); + QList< layerDef > wmtsLayers; + QgsCoordinateReferenceSystem wgs84 = QgsCoordinateReferenceSystem::fromOgcWmsCrs( GEO_EPSG_CRS_AUTHID ); + QList::iterator tmsIt = tmsList.begin(); - // Root Layer name - QString rootLayerName = QgsServerProjectUtils::wmsRootName( *project ); - if ( rootLayerName.isEmpty() && !project->title().isEmpty() ) - { - rootLayerName = project->title(); - } + // WMTS Project configuration + bool wmtsProject = project->readBoolEntry( QStringLiteral( "WMTSLayers" ), QStringLiteral( "Project" ) ); - if ( !rootLayerName.isEmpty() ) + if ( wmtsProject ) { - QDomElement layerParentNameElem = doc.createElement( QStringLiteral( "ows:Identifier" ) ); - QDomText layerParentNameText = doc.createTextNode( rootLayerName ); - layerParentNameElem.appendChild( layerParentNameText ); - layerParentElem.appendChild( layerParentNameElem ); - } + layerDef pLayer; - if ( !project->title().isEmpty() ) - { - // Root Layer title - QDomElement layerParentTitleElem = doc.createElement( QStringLiteral( "ows:Title" ) ); - QDomText layerParentTitleText = doc.createTextNode( project->title() ); - layerParentTitleElem.appendChild( layerParentTitleText ); - layerParentElem.appendChild( layerParentTitleElem ); - - // Root Layer abstract - QDomElement layerParentAbstElem = doc.createElement( QStringLiteral( "ows:Abstract" ) ); - QDomText layerParentAbstText = doc.createTextNode( project->title() ); - layerParentAbstElem.appendChild( layerParentAbstText ); - layerParentElem.appendChild( layerParentAbstElem ); + // Root Layer name + QString rootLayerName = QgsServerProjectUtils::wmsRootName( *project ); + if ( rootLayerName.isEmpty() && !project->title().isEmpty() ) + { + rootLayerName = project->title(); + } + pLayer.id = rootLayerName; + + if ( !project->title().isEmpty() ) + { + pLayer.title = project->title(); + pLayer.abstract = project->title(); + } + + //transform the project native CRS into WGS84 + QgsRectangle projRect = QgsServerProjectUtils::wmsExtent( *project ); + QgsCoordinateReferenceSystem projCrs = project->crs(); + Q_NOWARN_DEPRECATED_PUSH + QgsCoordinateTransform exGeoTransform( projCrs, wgs84 ); + Q_NOWARN_DEPRECATED_POP + try + { + pLayer.wgs84BoundingRect = exGeoTransform.transformBoundingBox( projRect ); + } + catch ( const QgsCsException & ) + { + pLayer.wgs84BoundingRect = QgsRectangle( -180, -90, 180, 90 ); + } + + // Formats + bool wmtsPngProject = project->readBoolEntry( QStringLiteral( "WMTSPngLayers" ), QStringLiteral( "Project" ) ); + if ( wmtsPngProject ) + pLayer.formats << QStringLiteral( "image/png" ); + bool wmtsJpegProject = project->readBoolEntry( QStringLiteral( "WMTSJpegLayers" ), QStringLiteral( "Project" ) ); + if ( wmtsJpegProject ) + pLayer.formats << QStringLiteral( "image/jpeg" ); + + wmtsLayers.append( pLayer ); } - //transform the project native CRS into WGS84 - QgsRectangle wgs84BoundingRect; - QgsRectangle projRect = QgsServerProjectUtils::wmsExtent( *project ); - QgsCoordinateReferenceSystem projCrs = project->crs(); - QgsCoordinateReferenceSystem wgs84 = QgsCoordinateReferenceSystem::fromOgcWmsCrs( GEO_EPSG_CRS_AUTHID ); - Q_NOWARN_DEPRECATED_PUSH - QgsCoordinateTransform exGeoTransform( projCrs, wgs84 ); - Q_NOWARN_DEPRECATED_POP - try + QStringList wmtsGroupNameList = project->readListEntry( QStringLiteral( "WMTSLayers" ), QStringLiteral( "Group" ) ); + if ( !wmtsGroupNameList.isEmpty() ) { - wgs84BoundingRect = exGeoTransform.transformBoundingBox( projRect ); + QgsLayerTreeGroup *treeRoot = project->layerTreeRoot(); + + QStringList wmtsPngGroupNameList = project->readListEntry( QStringLiteral( "WMTSPngLayers" ), QStringLiteral( "Group" ) ); + QStringList wmtsJpegGroupNameList = project->readListEntry( QStringLiteral( "WMTSJpegLayers" ), QStringLiteral( "Group" ) ); + + Q_FOREACH ( QString gName, wmtsGroupNameList ) + { + QgsLayerTreeGroup *treeGroup = treeRoot->findGroup( gName ); + if ( !treeGroup ) + { + continue; + } + + layerDef pLayer; + pLayer.id = treeGroup->customProperty( QStringLiteral( "wmsShortName" ) ).toString(); + if ( pLayer.id.isEmpty() ) + pLayer.id = gName; + + pLayer.title = treeGroup->customProperty( QStringLiteral( "wmsTitle" ) ).toString(); + if ( pLayer.title.isEmpty() ) + pLayer.title = gName; + + pLayer.abstract = treeGroup->customProperty( QStringLiteral( "wmsAbstract" ) ).toString(); + + for ( QgsLayerTreeLayer *layer : treeGroup->findLayers() ) + { + QgsMapLayer *l = layer->layer(); + //transform the layer native CRS into WGS84 + QgsRectangle wgs84BoundingRect; + QgsCoordinateReferenceSystem layerCrs = l->crs(); + Q_NOWARN_DEPRECATED_PUSH + QgsCoordinateTransform exGeoTransform( layerCrs, wgs84 ); + Q_NOWARN_DEPRECATED_POP + try + { + wgs84BoundingRect.combineExtentWith( exGeoTransform.transformBoundingBox( l->extent() ) ); + } + catch ( const QgsCsException & ) + { + wgs84BoundingRect.combineExtentWith( QgsRectangle( -180, -90, 180, 90 ) ); + } + } + + // Formats + if ( wmtsPngGroupNameList.contains( gName ) ) + pLayer.formats << QStringLiteral( "image/png" ); + if ( wmtsJpegGroupNameList.contains( gName ) ) + pLayer.formats << QStringLiteral( "image/jpeg" ); + + wmtsLayers.append( pLayer ); + } } - catch ( const QgsCsException & ) + + QStringList wmtsLayerIdList = project->readListEntry( QStringLiteral( "WMTSLayers" ), QStringLiteral( "Layer" ) ); + QStringList wmtsPngLayerIdList = project->readListEntry( QStringLiteral( "WMTSPngLayers" ), QStringLiteral( "Layer" ) ); + QStringList wmtsJpegLayerIdList = project->readListEntry( QStringLiteral( "WMTSJpegLayers" ), QStringLiteral( "Layer" ) ); + + Q_FOREACH ( QString lId, wmtsLayerIdList ) { - wgs84BoundingRect = QgsRectangle( -180, -90, 180, 90 ); + QgsMapLayer *l = project->mapLayer( lId ); + if ( !l ) + { + continue; + } +#ifdef HAVE_SERVER_PYTHON_PLUGINS + if ( !accessControl->layerReadPermission( l ) ) + { + continue; + } +#endif + + layerDef pLayer; + pLayer.id = l->name(); + if ( !l->shortName().isEmpty() ) + pLayer.id = l->shortName(); + pLayer.id = pLayer.id.replace( ' ', '_' ); + + pLayer.title = l->title(); + pLayer.abstract = l->abstract(); + + //transform the layer native CRS into WGS84 + QgsCoordinateReferenceSystem layerCrs = l->crs(); + Q_NOWARN_DEPRECATED_PUSH + QgsCoordinateTransform exGeoTransform( layerCrs, wgs84 ); + Q_NOWARN_DEPRECATED_POP + try + { + pLayer.wgs84BoundingRect = exGeoTransform.transformBoundingBox( l->extent() ); + } + catch ( const QgsCsException & ) + { + pLayer.wgs84BoundingRect = QgsRectangle( -180, -90, 180, 90 ); + } + + // Formats + if ( wmtsPngLayerIdList.contains( lId ) ) + pLayer.formats << QStringLiteral( "image/png" ); + if ( wmtsJpegLayerIdList.contains( lId ) ) + pLayer.formats << QStringLiteral( "image/jpeg" ); + + wmtsLayers.append( pLayer ); } - QDomElement wgs84BBoxElement = doc.createElement( QStringLiteral( "ows:WGS84BoundingBox" ) ); - QDomElement wgs84LowerCornerElement = doc.createElement( QStringLiteral( "LowerCorner" ) ); - QDomText wgs84LowerCornerText = doc.createTextNode( qgsDoubleToString( wgs84BoundingRect.xMinimum(), 6 ) + ' ' + qgsDoubleToString( wgs84BoundingRect.yMinimum(), 6 ) ); - wgs84LowerCornerElement.appendChild( wgs84LowerCornerText ); - wgs84BBoxElement.appendChild( wgs84LowerCornerElement ); - QDomElement wgs84UpperCornerElement = doc.createElement( QStringLiteral( "UpperCorner" ) ); - QDomText wgs84UpperCornerText = doc.createTextNode( qgsDoubleToString( wgs84BoundingRect.xMaximum(), 6 ) + ' ' + qgsDoubleToString( wgs84BoundingRect.yMaximum(), 6 ) ); - wgs84UpperCornerElement.appendChild( wgs84UpperCornerText ); - wgs84BBoxElement.appendChild( wgs84UpperCornerElement ); - layerParentElem.appendChild( wgs84BBoxElement ); - - // Root Layer Style - QDomElement layerParentStyleElem = doc.createElement( QStringLiteral( "Style" ) ); - layerParentStyleElem.setAttribute( QStringLiteral( "isDefault" ), QStringLiteral( "true" ) ); - QDomElement layerParentStyleIdElem = doc.createElement( QStringLiteral( "ows:Identifier" ) ); - QDomText layerParentStyleIdText = doc.createTextNode( QStringLiteral( "default" ) ); - layerParentStyleIdElem.appendChild( layerParentStyleIdText ); - layerParentStyleElem.appendChild( layerParentStyleIdElem ); - QDomElement layerParentStyleTitleElem = doc.createElement( QStringLiteral( "ows:Title" ) ); - QDomText layerParentStyleTitleText = doc.createTextNode( QStringLiteral( "default" ) ); - layerParentStyleTitleElem.appendChild( layerParentStyleTitleText ); - layerParentStyleElem.appendChild( layerParentStyleTitleElem ); - layerParentElem.appendChild( layerParentStyleElem ); - QList::iterator tmsIt = tmsList.begin(); - for ( ; tmsIt != tmsList.end(); ++tmsIt ) + Q_FOREACH ( layerDef wmtsLayer, wmtsLayers ) { - tileMatrixSet &tms = *tmsIt; + if ( wmtsLayer.id.isEmpty() ) + continue; - //wmts:TileMatrixSetLink - QDomElement tmslElement = doc.createElement( QStringLiteral( "TileMatrixSetLink" )/*wmts:TileMatrixSetLink*/ ); + QDomElement layerElem = doc.createElement( QStringLiteral( "Layer" ) ); - QDomElement identifierElem = doc.createElement( QStringLiteral( "TileMatrixSet" ) ); - QDomText identifierText = doc.createTextNode( tms.ref ); - identifierElem.appendChild( identifierText ); - tmslElement.appendChild( identifierElem ); + QDomElement layerIdElem = doc.createElement( QStringLiteral( "ows:Identifier" ) ); + QDomText layerIdText = doc.createTextNode( wmtsLayer.id ); + layerIdElem.appendChild( layerIdText ); + layerElem.appendChild( layerIdElem ); - layerParentElem.appendChild( tmslElement ); - } + if ( !wmtsLayer.title.isEmpty() ) + { + // Layer title + QDomElement layerTitleElem = doc.createElement( QStringLiteral( "ows:Title" ) ); + QDomText layerTitleText = doc.createTextNode( wmtsLayer.title ); + layerTitleElem.appendChild( layerTitleText ); + layerElem.appendChild( layerTitleElem ); + } - contentsElement.appendChild( layerParentElem ); + if ( !wmtsLayer.abstract.isEmpty() ) + { + // Layer abstract + QDomElement layerAbstElem = doc.createElement( QStringLiteral( "ows:Abstract" ) ); + QDomText layerAbstText = doc.createTextNode( project->title() ); + layerAbstElem.appendChild( layerAbstText ); + layerElem.appendChild( layerAbstElem ); + } + + QDomElement wgs84BBoxElement = doc.createElement( QStringLiteral( "ows:WGS84BoundingBox" ) ); + QDomElement wgs84LowerCornerElement = doc.createElement( QStringLiteral( "LowerCorner" ) ); + QDomText wgs84LowerCornerText = doc.createTextNode( qgsDoubleToString( wmtsLayer.wgs84BoundingRect.xMinimum(), 6 ) + ' ' + qgsDoubleToString( wmtsLayer.wgs84BoundingRect.yMinimum(), 6 ) ); + wgs84LowerCornerElement.appendChild( wgs84LowerCornerText ); + wgs84BBoxElement.appendChild( wgs84LowerCornerElement ); + QDomElement wgs84UpperCornerElement = doc.createElement( QStringLiteral( "UpperCorner" ) ); + QDomText wgs84UpperCornerText = doc.createTextNode( qgsDoubleToString( wmtsLayer.wgs84BoundingRect.xMaximum(), 6 ) + ' ' + qgsDoubleToString( wmtsLayer.wgs84BoundingRect.yMaximum(), 6 ) ); + wgs84UpperCornerElement.appendChild( wgs84UpperCornerText ); + wgs84BBoxElement.appendChild( wgs84UpperCornerElement ); + layerElem.appendChild( wgs84BBoxElement ); + + // Layer Style + QDomElement layerStyleElem = doc.createElement( QStringLiteral( "Style" ) ); + layerStyleElem.setAttribute( QStringLiteral( "isDefault" ), QStringLiteral( "true" ) ); + QDomElement layerStyleIdElem = doc.createElement( QStringLiteral( "ows:Identifier" ) ); + QDomText layerStyleIdText = doc.createTextNode( QStringLiteral( "default" ) ); + layerStyleIdElem.appendChild( layerStyleIdText ); + layerStyleElem.appendChild( layerStyleIdElem ); + QDomElement layerStyleTitleElem = doc.createElement( QStringLiteral( "ows:Title" ) ); + QDomText layerStyleTitleText = doc.createTextNode( QStringLiteral( "default" ) ); + layerStyleTitleElem.appendChild( layerStyleTitleText ); + layerStyleElem.appendChild( layerStyleTitleElem ); + layerElem.appendChild( layerStyleElem ); + + Q_FOREACH ( QString format, wmtsLayer.formats ) + { + QDomElement layerFormatElem = doc.createElement( QStringLiteral( "Format" ) ); + QDomText layerFormatText = doc.createTextNode( format ); + layerFormatElem.appendChild( layerFormatText ); + layerElem.appendChild( layerFormatElem ); + } + + tmsIt = tmsList.begin(); + for ( ; tmsIt != tmsList.end(); ++tmsIt ) + { + tileMatrixSet &tms = *tmsIt; + + //wmts:TileMatrixSetLink + QDomElement tmslElement = doc.createElement( QStringLiteral( "TileMatrixSetLink" )/*wmts:TileMatrixSetLink*/ ); + + QDomElement identifierElem = doc.createElement( QStringLiteral( "TileMatrixSet" ) ); + QDomText identifierText = doc.createTextNode( tms.ref ); + identifierElem.appendChild( identifierText ); + tmslElement.appendChild( identifierElem ); + + layerElem.appendChild( tmslElement ); + } + + contentsElement.appendChild( layerElem ); + } tmsIt = tmsList.begin(); for ( ; tmsIt != tmsList.end(); ++tmsIt ) @@ -480,7 +631,6 @@ namespace QgsWmts } } - //End return contentsElement; } diff --git a/src/server/services/wmts/qgswmtsutils.h b/src/server/services/wmts/qgswmtsutils.h index a439958f0d3e..8cd4215c1f1c 100644 --- a/src/server/services/wmts/qgswmtsutils.h +++ b/src/server/services/wmts/qgswmtsutils.h @@ -70,6 +70,19 @@ namespace QgsWmts QList< tileMatrix > tileMatrixList; }; + struct layerDef + { + QString id; + + QString title; + + QString abstract; + + QgsRectangle wgs84BoundingRect; + + QStringList formats; + }; + /** * Returns the highest version supported by this implementation */ From cff846926a8d145c92e80ea89614d7884e3be71e Mon Sep 17 00:00:00 2001 From: rldhont Date: Mon, 30 Jul 2018 14:11:15 +0200 Subject: [PATCH 08/33] [Server][Feature][needs-docs] Update WMTS service: Add GetFeatureInfo Support GetFeatureInfo Request in WMTS. --- src/server/services/wmts/CMakeLists.txt | 1 + src/server/services/wmts/qgswmts.cpp | 5 + .../services/wmts/qgswmtsgetcapabilities.cpp | 38 +++- .../services/wmts/qgswmtsgetfeatureinfo.cpp | 52 +++++ .../services/wmts/qgswmtsgetfeatureinfo.h | 28 +++ src/server/services/wmts/qgswmtsgettile.cpp | 180 +---------------- src/server/services/wmts/qgswmtsutils.cpp | 185 ++++++++++++++++++ src/server/services/wmts/qgswmtsutils.h | 7 + 8 files changed, 315 insertions(+), 181 deletions(-) create mode 100644 src/server/services/wmts/qgswmtsgetfeatureinfo.cpp create mode 100644 src/server/services/wmts/qgswmtsgetfeatureinfo.h diff --git a/src/server/services/wmts/CMakeLists.txt b/src/server/services/wmts/CMakeLists.txt index 8c237ee2eb41..18790caa531c 100644 --- a/src/server/services/wmts/CMakeLists.txt +++ b/src/server/services/wmts/CMakeLists.txt @@ -7,6 +7,7 @@ SET (wmts_SRCS qgswmtsutils.cpp qgswmtsgetcapabilities.cpp qgswmtsgettile.cpp + qgswmtsgetfeatureinfo.cpp ) ######################################################## diff --git a/src/server/services/wmts/qgswmts.cpp b/src/server/services/wmts/qgswmts.cpp index 6cc16cb596c0..080732588162 100644 --- a/src/server/services/wmts/qgswmts.cpp +++ b/src/server/services/wmts/qgswmts.cpp @@ -19,6 +19,7 @@ #include "qgswmtsutils.h" #include "qgswmtsgetcapabilities.h" #include "qgswmtsgettile.h" +#include "qgswmtsgetfeatureinfo.h" #define QSTR_COMPARE( str, lit )\ (str.compare( QStringLiteral( lit ), Qt::CaseInsensitive ) == 0) @@ -82,6 +83,10 @@ namespace QgsWmts { writeGetTile( mServerIface, project, versionString, request, response ); } + else if ( QSTR_COMPARE( req, "GetFeatureInfo" ) ) + { + writeGetFeatureInfo( mServerIface, project, versionString, request, response ); + } else { // Operation not supported diff --git a/src/server/services/wmts/qgswmtsgetcapabilities.cpp b/src/server/services/wmts/qgswmtsgetcapabilities.cpp index df5c4fd6edf4..75062429add7 100644 --- a/src/server/services/wmts/qgswmtsgetcapabilities.cpp +++ b/src/server/services/wmts/qgswmtsgetcapabilities.cpp @@ -302,9 +302,9 @@ namespace QgsWmts operationsMetadataElement.appendChild( getTileElement ); //ows:Operation element with name GetFeatureInfo - /*QDomElement getFeatureInfoElement = getCapabilitiesElement.cloneNode().toElement();//this is the same as 'GetCapabilities' + QDomElement getFeatureInfoElement = getCapabilitiesElement.cloneNode().toElement();//this is the same as 'GetCapabilities' getFeatureInfoElement.setAttribute( QStringLiteral( "name" ), QStringLiteral( "GetFeatureInfo" ) ); - operationsMetadataElement.appendChild( getFeatureInfoElement );*/ + operationsMetadataElement.appendChild( getFeatureInfoElement ); // End return operationsMetadataElement; @@ -327,6 +327,8 @@ namespace QgsWmts QgsCoordinateReferenceSystem wgs84 = QgsCoordinateReferenceSystem::fromOgcWmsCrs( GEO_EPSG_CRS_AUTHID ); QList::iterator tmsIt = tmsList.begin(); + QStringList nonIdentifiableLayers = project->nonIdentifiableLayers(); + // WMTS Project configuration bool wmtsProject = project->readBoolEntry( QStringLiteral( "WMTSLayers" ), QStringLiteral( "Project" ) ); @@ -371,6 +373,10 @@ namespace QgsWmts if ( wmtsJpegProject ) pLayer.formats << QStringLiteral( "image/jpeg" ); + // Project is not queryable in WMS + //pLayer.queryable = ( nonIdentifiableLayers.count() != project->count() ); + pLayer.queryable = false; + wmtsLayers.append( pLayer ); } @@ -401,11 +407,12 @@ namespace QgsWmts pLayer.abstract = treeGroup->customProperty( QStringLiteral( "wmsAbstract" ) ).toString(); + QgsRectangle wgs84BoundingRect; + bool queryable = false; for ( QgsLayerTreeLayer *layer : treeGroup->findLayers() ) { QgsMapLayer *l = layer->layer(); //transform the layer native CRS into WGS84 - QgsRectangle wgs84BoundingRect; QgsCoordinateReferenceSystem layerCrs = l->crs(); Q_NOWARN_DEPRECATED_PUSH QgsCoordinateTransform exGeoTransform( layerCrs, wgs84 ); @@ -418,7 +425,13 @@ namespace QgsWmts { wgs84BoundingRect.combineExtentWith( QgsRectangle( -180, -90, 180, 90 ) ); } + if ( !queryable && !nonIdentifiableLayers.contains( l->id() ) ) + { + queryable = true; + } } + pLayer.wgs84BoundingRect = wgs84BoundingRect; + pLayer.queryable = queryable; // Formats if ( wmtsPngGroupNameList.contains( gName ) ) @@ -477,9 +490,19 @@ namespace QgsWmts if ( wmtsJpegLayerIdList.contains( lId ) ) pLayer.formats << QStringLiteral( "image/jpeg" ); + pLayer.queryable = ( !nonIdentifiableLayers.contains( l->id() ) ); + wmtsLayers.append( pLayer ); } + // Append InfoFormat helper + std::function < void ( QDomElement &, const QString & ) > appendInfoFormat = [&doc]( QDomElement & elem, const QString & format ) + { + QDomElement formatElem = doc.createElement( QStringLiteral( "InfoFormat" )/*wmts:InfoFormat*/ ); + formatElem.appendChild( doc.createTextNode( format ) ); + elem.appendChild( formatElem ); + }; + Q_FOREACH ( layerDef wmtsLayer, wmtsLayers ) { if ( wmtsLayer.id.isEmpty() ) @@ -542,6 +565,15 @@ namespace QgsWmts layerElem.appendChild( layerFormatElem ); } + if ( wmtsLayer.queryable ) + { + appendInfoFormat( layerElem, QStringLiteral( "text/plain" ) ); + appendInfoFormat( layerElem, QStringLiteral( "text/html" ) ); + appendInfoFormat( layerElem, QStringLiteral( "text/xml" ) ); + appendInfoFormat( layerElem, QStringLiteral( "application/vnd.ogc.gml" ) ); + appendInfoFormat( layerElem, QStringLiteral( "application/vnd.ogc.gml/3.1.1" ) ); + } + tmsIt = tmsList.begin(); for ( ; tmsIt != tmsList.end(); ++tmsIt ) { diff --git a/src/server/services/wmts/qgswmtsgetfeatureinfo.cpp b/src/server/services/wmts/qgswmtsgetfeatureinfo.cpp new file mode 100644 index 000000000000..1212ee3b7685 --- /dev/null +++ b/src/server/services/wmts/qgswmtsgetfeatureinfo.cpp @@ -0,0 +1,52 @@ +/*************************************************************************** + qgswmsgetfeatureinfo.cpp + ------------------------- + begin : July 23 , 2017 + copyright : (C) 2018 by René-Luc D'Hont + email : rldhont at 3liz dot 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 "qgswmtsutils.h" +#include "qgswmtsgetfeatureinfo.h" + +#include + +namespace QgsWmts +{ + + void writeGetFeatureInfo( QgsServerInterface *serverIface, const QgsProject *project, + const QString &version, const QgsServerRequest &request, + QgsServerResponse &response ) + { + Q_UNUSED( version ); + + QgsServerRequest::Parameters params = request.parameters(); + + // WMS query + QUrlQuery query = translateWmtsParamToWmsQueryItem( QStringLiteral( "GetFeatureInfo" ), params, project, serverIface ); + + // GetFeatureInfo query items + query.addQueryItem( QStringLiteral( "query_layers" ), query.queryItemValue( QStringLiteral( "layers" ) ) ); + query.addQueryItem( QStringLiteral( "i" ), params.value( QStringLiteral( "I" ) ) ); + query.addQueryItem( QStringLiteral( "j" ), params.value( QStringLiteral( "J" ) ) ); + query.addQueryItem( QStringLiteral( "info_format" ), params.value( QStringLiteral( "INFOFORMAT" ) ) ); + + QgsServerParameters wmsParams( query ); + QgsServerRequest wmsRequest( "?" + query.query( QUrl::FullyDecoded ) ); + QgsService *service = serverIface->serviceRegistry()->getService( wmsParams.service(), wmsParams.version() ); + service->executeRequest( wmsRequest, response, project ); + } + +} // namespace QgsWmts + + + + diff --git a/src/server/services/wmts/qgswmtsgetfeatureinfo.h b/src/server/services/wmts/qgswmtsgetfeatureinfo.h new file mode 100644 index 000000000000..a64c6dcfd698 --- /dev/null +++ b/src/server/services/wmts/qgswmtsgetfeatureinfo.h @@ -0,0 +1,28 @@ +/*************************************************************************** + qgswmsgetfeatureinfo.h + ------------------------- + begin : July 23 , 2017 + copyright : (C) 2018 by René-Luc D'Hont + email : rldhont at 3liz dot 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. * + * * + ***************************************************************************/ + +namespace QgsWmts +{ + + /** + * Output GetFeatureInfo response + */ + void writeGetFeatureInfo( QgsServerInterface *serverIface, const QgsProject *project, + const QString &version, const QgsServerRequest &request, + QgsServerResponse &response ); + +} // namespace QgsWmts diff --git a/src/server/services/wmts/qgswmtsgettile.cpp b/src/server/services/wmts/qgswmtsgettile.cpp index 1197c208e0f8..a12992b0fe82 100644 --- a/src/server/services/wmts/qgswmtsgettile.cpp +++ b/src/server/services/wmts/qgswmtsgettile.cpp @@ -30,184 +30,8 @@ namespace QgsWmts QgsServerRequest::Parameters params = request.parameters(); - //defining Layer - QString layer; - //read Layer - QMap::const_iterator layer_it = params.constFind( QStringLiteral( "LAYER" ) ); - if ( layer_it != params.constEnd() ) - { - layer = layer_it.value(); - } - else - { - throw QgsRequestNotWellFormedException( QStringLiteral( "Layer is mandatory" ) ); - } - - //defining Format - QString format; - //read Format - QMap::const_iterator format_it = params.constFind( QStringLiteral( "FORMAT" ) ); - if ( format_it != params.constEnd() ) - { - format = format_it.value(); - } - else - { - throw QgsRequestNotWellFormedException( QStringLiteral( "Format is mandatory" ) ); - } - - QList< tileMatrixSet > tmsList = getTileMatrixSetList( project ); - if ( tmsList.isEmpty() ) - { - throw QgsServiceException( QStringLiteral( "UnknownError" ), - QStringLiteral( "Service not well configured" ) ); - } - - //defining TileMatrixSet ref - QString tms_ref; - //read TileMatrixSet - QMap::const_iterator tms_ref_it = params.constFind( QStringLiteral( "TILEMATRIXSET" ) ); - if ( tms_ref_it != params.constEnd() ) - { - tms_ref = tms_ref_it.value(); - } - else - { - throw QgsRequestNotWellFormedException( QStringLiteral( "TileMatrixSet is mandatory" ) ); - } - - bool tms_ref_valid = false; - tileMatrixSet tms; - QList::iterator tmsIt = tmsList.begin(); - for ( ; tmsIt != tmsList.end(); ++tmsIt ) - { - tileMatrixSet &tmsi = *tmsIt; - if ( tmsi.ref == tms_ref ) - { - tms_ref_valid = true; - tms = tmsi; - break; - } - } - if ( !tms_ref_valid ) - { - throw QgsRequestNotWellFormedException( QStringLiteral( "TileMatrixSet is unknown" ) ); - } - - bool conversionSuccess = false; - - //difining TileMatrix idx - int tm_idx; - //read TileMatrix - QMap::const_iterator tm_ref_it = params.constFind( QStringLiteral( "TILEMATRIX" ) ); - if ( tm_ref_it != params.constEnd() ) - { - QString tm_ref = tm_ref_it.value(); - tm_idx = tm_ref.toInt( &conversionSuccess ); - if ( !conversionSuccess ) - { - throw QgsRequestNotWellFormedException( QStringLiteral( "TileMatrix is unknown" ) ); - } - } - else - { - throw QgsRequestNotWellFormedException( QStringLiteral( "TileMatrix is mandatory" ) ); - } - if ( tms.tileMatrixList.count() < tm_idx ) - { - throw QgsRequestNotWellFormedException( QStringLiteral( "TileMatrix is unknown" ) ); - } - tileMatrix tm = tms.tileMatrixList.at( tm_idx ); - - //defining TileRow - int tr; - //read TileRow - QMap::const_iterator tr_it = params.constFind( QStringLiteral( "TILEROW" ) ); - if ( tr_it != params.constEnd() ) - { - QString tr_str = tr_it.value(); - conversionSuccess = false; - tr = tr_str.toInt( &conversionSuccess ); - if ( !conversionSuccess ) - { - throw QgsRequestNotWellFormedException( QStringLiteral( "TileRow is unknown" ) ); - } - } - else - { - throw QgsRequestNotWellFormedException( QStringLiteral( "TileRow is mandatory" ) ); - } - if ( tm.row <= tr ) - { - throw QgsRequestNotWellFormedException( QStringLiteral( "TileRow is unknown" ) ); - } - - //defining TileCol - int tc; - //read TileCol - QMap::const_iterator tc_it = params.constFind( QStringLiteral( "TILECOL" ) ); - if ( tc_it != params.constEnd() ) - { - QString tc_str = tc_it.value(); - conversionSuccess = false; - tc = tc_str.toInt( &conversionSuccess ); - if ( !conversionSuccess ) - { - throw QgsRequestNotWellFormedException( QStringLiteral( "TileCol is unknown" ) ); - } - } - else - { - throw QgsRequestNotWellFormedException( QStringLiteral( "TileCol is mandatory" ) ); - } - if ( tm.col <= tc ) - { - throw QgsRequestNotWellFormedException( QStringLiteral( "TileCol is unknown" ) ); - } - - int tileWidth = 256; - int tileHeight = 256; - double res = tm.resolution; - double minx = tm.left + tc * ( tileWidth * res ); - double miny = tm.top - ( tr + 1 ) * ( tileHeight * res ); - double maxx = tm.left + ( tc + 1 ) * ( tileWidth * res ); - double maxy = tm.top - tr * ( tileHeight * res ); - QString bbox; - if ( tms.ref == "EPSG:4326" ) - { - bbox = qgsDoubleToString( miny, 6 ) + ',' + - qgsDoubleToString( minx, 6 ) + ',' + - qgsDoubleToString( maxy, 6 ) + ',' + - qgsDoubleToString( maxx, 6 ); - } - else - { - bbox = qgsDoubleToString( minx, 6 ) + ',' + - qgsDoubleToString( miny, 6 ) + ',' + - qgsDoubleToString( maxx, 6 ) + ',' + - qgsDoubleToString( maxy, 6 ); - } - - QUrlQuery query; - if ( !params.value( QStringLiteral( "MAP" ) ).isEmpty() ) - { - query.addQueryItem( QStringLiteral( "map" ), params.value( QStringLiteral( "MAP" ) ) ); - } - query.addQueryItem( QStringLiteral( "service" ), QStringLiteral( "WMS" ) ); - query.addQueryItem( QStringLiteral( "version" ), QStringLiteral( "1.3.0" ) ); - query.addQueryItem( QStringLiteral( "request" ), QStringLiteral( "GetMap" ) ); - query.addQueryItem( QStringLiteral( "layers" ), layer ); - query.addQueryItem( QStringLiteral( "styles" ), QString() ); - query.addQueryItem( QStringLiteral( "crs" ), tms.ref ); - query.addQueryItem( QStringLiteral( "bbox" ), bbox ); - query.addQueryItem( QStringLiteral( "width" ), QStringLiteral( "256" ) ); - query.addQueryItem( QStringLiteral( "height" ), QStringLiteral( "256" ) ); - query.addQueryItem( QStringLiteral( "format" ), format ); - if ( format.startsWith( QStringLiteral( "image/png" ) ) ) - { - query.addQueryItem( QStringLiteral( "transparent" ), QStringLiteral( "true" ) ); - } - query.addQueryItem( QStringLiteral( "dpi" ), QStringLiteral( "96" ) ); + // WMS query + QUrlQuery query = translateWmtsParamToWmsQueryItem( QStringLiteral( "GetMap" ), params, project ); QgsServerParameters wmsParams( query ); QgsServerRequest wmsRequest( "?" + query.query( QUrl::FullyDecoded ) ); diff --git a/src/server/services/wmts/qgswmtsutils.cpp b/src/server/services/wmts/qgswmtsutils.cpp index 615104f42dd7..f39df2928dd1 100644 --- a/src/server/services/wmts/qgswmtsutils.cpp +++ b/src/server/services/wmts/qgswmtsutils.cpp @@ -264,6 +264,191 @@ namespace QgsWmts return tmsList; } + QUrlQuery translateWmtsParamToWmsQueryItem( const QString &request, const QgsServerRequest::Parameters ¶ms, const QgsProject *project ) + { + + //defining Layer + QString layer; + //read Layer + QMap::const_iterator layer_it = params.constFind( QStringLiteral( "LAYER" ) ); + if ( layer_it != params.constEnd() ) + { + layer = layer_it.value(); + } + else + { + throw QgsRequestNotWellFormedException( QStringLiteral( "Layer is mandatory" ) ); + } + + //defining Format + QString format; + //read Format + QMap::const_iterator format_it = params.constFind( QStringLiteral( "FORMAT" ) ); + if ( format_it != params.constEnd() ) + { + format = format_it.value(); + } + else + { + throw QgsRequestNotWellFormedException( QStringLiteral( "Format is mandatory" ) ); + } + + QList< tileMatrixSet > tmsList = getTileMatrixSetList( project ); + if ( tmsList.isEmpty() ) + { + throw QgsServiceException( QStringLiteral( "UnknownError" ), + QStringLiteral( "Service not well configured" ) ); + } + + //defining TileMatrixSet ref + QString tms_ref; + //read TileMatrixSet + QMap::const_iterator tms_ref_it = params.constFind( QStringLiteral( "TILEMATRIXSET" ) ); + if ( tms_ref_it != params.constEnd() ) + { + tms_ref = tms_ref_it.value(); + } + else + { + throw QgsRequestNotWellFormedException( QStringLiteral( "TileMatrixSet is mandatory" ) ); + } + + bool tms_ref_valid = false; + tileMatrixSet tms; + QList::iterator tmsIt = tmsList.begin(); + for ( ; tmsIt != tmsList.end(); ++tmsIt ) + { + tileMatrixSet &tmsi = *tmsIt; + if ( tmsi.ref == tms_ref ) + { + tms_ref_valid = true; + tms = tmsi; + break; + } + } + if ( !tms_ref_valid ) + { + throw QgsRequestNotWellFormedException( QStringLiteral( "TileMatrixSet is unknown" ) ); + } + + bool conversionSuccess = false; + + //difining TileMatrix idx + int tm_idx; + //read TileMatrix + QMap::const_iterator tm_ref_it = params.constFind( QStringLiteral( "TILEMATRIX" ) ); + if ( tm_ref_it != params.constEnd() ) + { + QString tm_ref = tm_ref_it.value(); + tm_idx = tm_ref.toInt( &conversionSuccess ); + if ( !conversionSuccess ) + { + throw QgsRequestNotWellFormedException( QStringLiteral( "TileMatrix is unknown" ) ); + } + } + else + { + throw QgsRequestNotWellFormedException( QStringLiteral( "TileMatrix is mandatory" ) ); + } + if ( tms.tileMatrixList.count() < tm_idx ) + { + throw QgsRequestNotWellFormedException( QStringLiteral( "TileMatrix is unknown" ) ); + } + tileMatrix tm = tms.tileMatrixList.at( tm_idx ); + + //defining TileRow + int tr; + //read TileRow + QMap::const_iterator tr_it = params.constFind( QStringLiteral( "TILEROW" ) ); + if ( tr_it != params.constEnd() ) + { + QString tr_str = tr_it.value(); + conversionSuccess = false; + tr = tr_str.toInt( &conversionSuccess ); + if ( !conversionSuccess ) + { + throw QgsRequestNotWellFormedException( QStringLiteral( "TileRow is unknown" ) ); + } + } + else + { + throw QgsRequestNotWellFormedException( QStringLiteral( "TileRow is mandatory" ) ); + } + if ( tm.row <= tr ) + { + throw QgsRequestNotWellFormedException( QStringLiteral( "TileRow is unknown" ) ); + } + + //defining TileCol + int tc; + //read TileCol + QMap::const_iterator tc_it = params.constFind( QStringLiteral( "TILECOL" ) ); + if ( tc_it != params.constEnd() ) + { + QString tc_str = tc_it.value(); + conversionSuccess = false; + tc = tc_str.toInt( &conversionSuccess ); + if ( !conversionSuccess ) + { + throw QgsRequestNotWellFormedException( QStringLiteral( "TileCol is unknown" ) ); + } + } + else + { + throw QgsRequestNotWellFormedException( QStringLiteral( "TileCol is mandatory" ) ); + } + if ( tm.col <= tc ) + { + throw QgsRequestNotWellFormedException( QStringLiteral( "TileCol is unknown" ) ); + } + + int tileWidth = 256; + int tileHeight = 256; + double res = tm.resolution; + double minx = tm.left + tc * ( tileWidth * res ); + double miny = tm.top - ( tr + 1 ) * ( tileHeight * res ); + double maxx = tm.left + ( tc + 1 ) * ( tileWidth * res ); + double maxy = tm.top - tr * ( tileHeight * res ); + QString bbox; + if ( tms.ref == "EPSG:4326" ) + { + bbox = qgsDoubleToString( miny, 6 ) + ',' + + qgsDoubleToString( minx, 6 ) + ',' + + qgsDoubleToString( maxy, 6 ) + ',' + + qgsDoubleToString( maxx, 6 ); + } + else + { + bbox = qgsDoubleToString( minx, 6 ) + ',' + + qgsDoubleToString( miny, 6 ) + ',' + + qgsDoubleToString( maxx, 6 ) + ',' + + qgsDoubleToString( maxy, 6 ); + } + + QUrlQuery query; + if ( !params.value( QStringLiteral( "MAP" ) ).isEmpty() ) + { + query.addQueryItem( QStringLiteral( "map" ), params.value( QStringLiteral( "MAP" ) ) ); + } + query.addQueryItem( QStringLiteral( "service" ), QStringLiteral( "WMS" ) ); + query.addQueryItem( QStringLiteral( "version" ), QStringLiteral( "1.3.0" ) ); + query.addQueryItem( QStringLiteral( "request" ), request ); + query.addQueryItem( QStringLiteral( "layers" ), layer ); + query.addQueryItem( QStringLiteral( "styles" ), QString() ); + query.addQueryItem( QStringLiteral( "crs" ), tms.ref ); + query.addQueryItem( QStringLiteral( "bbox" ), bbox ); + query.addQueryItem( QStringLiteral( "width" ), QStringLiteral( "256" ) ); + query.addQueryItem( QStringLiteral( "height" ), QStringLiteral( "256" ) ); + query.addQueryItem( QStringLiteral( "format" ), format ); + if ( format.startsWith( QStringLiteral( "image/png" ) ) ) + { + query.addQueryItem( QStringLiteral( "transparent" ), QStringLiteral( "true" ) ); + } + query.addQueryItem( QStringLiteral( "dpi" ), QStringLiteral( "96" ) ); + + return query; + } + } // namespace QgsWmts diff --git a/src/server/services/wmts/qgswmtsutils.h b/src/server/services/wmts/qgswmtsutils.h index 8cd4215c1f1c..2aca99912568 100644 --- a/src/server/services/wmts/qgswmtsutils.h +++ b/src/server/services/wmts/qgswmtsutils.h @@ -81,6 +81,8 @@ namespace QgsWmts QgsRectangle wgs84BoundingRect; QStringList formats; + + bool queryable; }; /** @@ -107,6 +109,11 @@ namespace QgsWmts tileMatrixSet getTileMatrixSet( tileMatrixInfo tmi ); QList< tileMatrixSet > getTileMatrixSetList( const QgsProject *project ); + /** + * Translate WMTS parameters to WMS query item + */ + QUrlQuery translateWmtsParamToWmsQueryItem( const QString &request, const QgsServerRequest::Parameters ¶ms, const QgsProject *project ); + } // namespace QgsWmts #endif From 50766ef04cb0a2248546335ae44b5b8fd55fe00b Mon Sep 17 00:00:00 2001 From: rldhont Date: Mon, 30 Jul 2018 15:41:31 +0200 Subject: [PATCH 09/33] [Server][Feature][needs-docs] Update WMTS service: Check layer param Verifying the LAYER WMTS parameter --- .../services/wmts/qgswmtsgetcapabilities.cpp | 109 +++++- src/server/services/wmts/qgswmtsgettile.cpp | 2 +- src/server/services/wmts/qgswmtsutils.cpp | 346 +++++++++++------- src/server/services/wmts/qgswmtsutils.h | 7 +- 4 files changed, 321 insertions(+), 143 deletions(-) diff --git a/src/server/services/wmts/qgswmtsgetcapabilities.cpp b/src/server/services/wmts/qgswmtsgetcapabilities.cpp index 75062429add7..8b55cbfb2390 100644 --- a/src/server/services/wmts/qgswmtsgetcapabilities.cpp +++ b/src/server/services/wmts/qgswmtsgetcapabilities.cpp @@ -332,16 +332,16 @@ namespace QgsWmts // WMTS Project configuration bool wmtsProject = project->readBoolEntry( QStringLiteral( "WMTSLayers" ), QStringLiteral( "Project" ) ); - if ( wmtsProject ) + // Root Layer name + QString rootLayerName = QgsServerProjectUtils::wmsRootName( *project ); + if ( rootLayerName.isEmpty() && !project->title().isEmpty() ) { - layerDef pLayer; + rootLayerName = project->title(); + } - // Root Layer name - QString rootLayerName = QgsServerProjectUtils::wmsRootName( *project ); - if ( rootLayerName.isEmpty() && !project->title().isEmpty() ) - { - rootLayerName = project->title(); - } + if ( wmtsProject && !rootLayerName.isEmpty() ) + { + layerDef pLayer; pLayer.id = rootLayerName; if ( !project->title().isEmpty() ) @@ -533,6 +533,7 @@ namespace QgsWmts layerElem.appendChild( layerAbstElem ); } + // WGS84 bounding box QDomElement wgs84BBoxElement = doc.createElement( QStringLiteral( "ows:WGS84BoundingBox" ) ); QDomElement wgs84LowerCornerElement = doc.createElement( QStringLiteral( "LowerCorner" ) ); QDomText wgs84LowerCornerText = doc.createTextNode( qgsDoubleToString( wmtsLayer.wgs84BoundingRect.xMinimum(), 6 ) + ' ' + qgsDoubleToString( wmtsLayer.wgs84BoundingRect.yMinimum(), 6 ) ); @@ -544,6 +545,41 @@ namespace QgsWmts wgs84BBoxElement.appendChild( wgs84UpperCornerElement ); layerElem.appendChild( wgs84BBoxElement ); + // Other bounding boxes + tmsIt = tmsList.begin(); + for ( ; tmsIt != tmsList.end(); ++tmsIt ) + { + tileMatrixSet &tms = *tmsIt; + if ( tms.ref == "EPSG:4326" ) + continue; + + QgsRectangle rect; + QgsCoordinateReferenceSystem crs = QgsCoordinateReferenceSystem::fromOgcWmsCrs( tms.ref ); + Q_NOWARN_DEPRECATED_PUSH + QgsCoordinateTransform exGeoTransform( wgs84, crs ); + Q_NOWARN_DEPRECATED_POP + try + { + rect = exGeoTransform.transformBoundingBox( wmtsLayer.wgs84BoundingRect ); + } + catch ( const QgsCsException & ) + { + continue; + } + + QDomElement bboxElement = doc.createElement( QStringLiteral( "ows:BoundingBox" ) ); + bboxElement.setAttribute( QStringLiteral( "crs" ), tms.ref ); + QDomElement lowerCornerElement = doc.createElement( QStringLiteral( "LowerCorner" ) ); + QDomText lowerCornerText = doc.createTextNode( qgsDoubleToString( rect.xMinimum(), 6 ) + ' ' + qgsDoubleToString( rect.yMinimum(), 6 ) ); + lowerCornerElement.appendChild( lowerCornerText ); + bboxElement.appendChild( lowerCornerElement ); + QDomElement upperCornerElement = doc.createElement( QStringLiteral( "UpperCorner" ) ); + QDomText upperCornerText = doc.createTextNode( qgsDoubleToString( rect.xMaximum(), 6 ) + ' ' + qgsDoubleToString( rect.yMaximum(), 6 ) ); + upperCornerElement.appendChild( upperCornerText ); + bboxElement.appendChild( upperCornerElement ); + layerElem.appendChild( bboxElement ); + } + // Layer Style QDomElement layerStyleElem = doc.createElement( QStringLiteral( "Style" ) ); layerStyleElem.setAttribute( QStringLiteral( "isDefault" ), QStringLiteral( "true" ) ); @@ -578,6 +614,22 @@ namespace QgsWmts for ( ; tmsIt != tmsList.end(); ++tmsIt ) { tileMatrixSet &tms = *tmsIt; + if ( tms.ref != "EPSG:4326" ) + { + QgsRectangle rect; + QgsCoordinateReferenceSystem crs = QgsCoordinateReferenceSystem::fromOgcWmsCrs( tms.ref ); + Q_NOWARN_DEPRECATED_PUSH + QgsCoordinateTransform exGeoTransform( wgs84, crs ); + Q_NOWARN_DEPRECATED_POP + try + { + rect = exGeoTransform.transformBoundingBox( wmtsLayer.wgs84BoundingRect ); + } + catch ( const QgsCsException & ) + { + continue; + } + } //wmts:TileMatrixSetLink QDomElement tmslElement = doc.createElement( QStringLiteral( "TileMatrixSetLink" )/*wmts:TileMatrixSetLink*/ ); @@ -587,6 +639,47 @@ namespace QgsWmts identifierElem.appendChild( identifierText ); tmslElement.appendChild( identifierElem ); + //wmts:TileMatrixSetLimits + QDomElement tmsLimitsElement = doc.createElement( QStringLiteral( "TileMatrixSetLimits" )/*wmts:TileMatrixSetLimits*/ ); + int tmIdx = 0; + QList::iterator tmIt = tms.tileMatrixList.begin(); + for ( ; tmIt != tms.tileMatrixList.end(); ++tmIt ) + { + tileMatrix &tm = *tmIt; + + QDomElement tmLimitsElement = doc.createElement( QStringLiteral( "TileMatrixLimits" )/*wmts:TileMatrixLimits*/ ); + + QDomElement tmIdentifierElem = doc.createElement( QStringLiteral( "TileMatrix" ) ); + QDomText tmIdentifierText = doc.createTextNode( QString::number( tmIdx ) ); + tmIdentifierElem.appendChild( tmIdentifierText ); + tmLimitsElement.appendChild( tmIdentifierElem ); + + QDomElement minTileColElem = doc.createElement( QStringLiteral( "MinTileCol" ) ); + QDomText minTileColText = doc.createTextNode( QString::number( 0 ) ); + minTileColElem.appendChild( minTileColText ); + tmLimitsElement.appendChild( minTileColElem ); + + QDomElement maxTileColElem = doc.createElement( QStringLiteral( "MaxTileCol" ) ); + QDomText maxTileColText = doc.createTextNode( QString::number( tm.col ) ); + maxTileColElem.appendChild( maxTileColText ); + tmLimitsElement.appendChild( maxTileColElem ); + + QDomElement minTileRowElem = doc.createElement( QStringLiteral( "MinTileRow" ) ); + QDomText minTileRowText = doc.createTextNode( QString::number( 0 ) ); + minTileRowElem.appendChild( minTileRowText ); + tmLimitsElement.appendChild( minTileRowElem ); + + QDomElement maxTileRowElem = doc.createElement( QStringLiteral( "MaxTileRow" ) ); + QDomText maxTileRowText = doc.createTextNode( QString::number( tm.row ) ); + maxTileRowElem.appendChild( maxTileRowText ); + tmLimitsElement.appendChild( maxTileRowElem ); + + tmsLimitsElement.appendChild( tmLimitsElement ); + + ++tmIdx; + } + tmslElement.appendChild( tmsLimitsElement ); + layerElem.appendChild( tmslElement ); } diff --git a/src/server/services/wmts/qgswmtsgettile.cpp b/src/server/services/wmts/qgswmtsgettile.cpp index a12992b0fe82..fa984b425f0f 100644 --- a/src/server/services/wmts/qgswmtsgettile.cpp +++ b/src/server/services/wmts/qgswmtsgettile.cpp @@ -31,7 +31,7 @@ namespace QgsWmts QgsServerRequest::Parameters params = request.parameters(); // WMS query - QUrlQuery query = translateWmtsParamToWmsQueryItem( QStringLiteral( "GetMap" ), params, project ); + QUrlQuery query = translateWmtsParamToWmsQueryItem( QStringLiteral( "GetMap" ), params, project, serverIface ); QgsServerParameters wmsParams( query ); QgsServerRequest wmsRequest( "?" + query.query( QUrl::FullyDecoded ) ); diff --git a/src/server/services/wmts/qgswmtsutils.cpp b/src/server/services/wmts/qgswmtsutils.cpp index f39df2928dd1..b325285bde6c 100644 --- a/src/server/services/wmts/qgswmtsutils.cpp +++ b/src/server/services/wmts/qgswmtsutils.cpp @@ -23,11 +23,30 @@ #include "qgsexception.h" #include "qgsmapserviceexception.h" #include "qgscoordinatereferencesystem.h" +#include "qgslayertree.h" +#include "qgslayertreemodel.h" +#include "qgslayertreemodellegendnode.h" #include "qgssettings.h" namespace QgsWmts { + namespace + { + QMap< QString, double> populateInchesPerUnit(); + QMap< QString, tileMatrixInfo> populateTileMatrixInfoMap(); + + QgsCoordinateReferenceSystem wgs84 = QgsCoordinateReferenceSystem::fromOgcWmsCrs( GEO_EPSG_CRS_AUTHID ); + + int DOTS_PER_INCH = 72; + double METERS_PER_INCH = 0.02540005080010160020; + QMap< QString, double> INCHES_PER_UNIT = populateInchesPerUnit(); + int tileWidth = 256; + int tileHeight = 256; + + QMap< QString, tileMatrixInfo> tileMatrixInfoMap = populateTileMatrixInfoMap(); + } + QString implementationVersion() { return QStringLiteral( "1.0.0" ); @@ -79,27 +98,63 @@ namespace QgsWmts return QgsRectangle( d[0], d[1], d[2], d[3] ); } - tileMatrixSet getTileMatrixSet( tileMatrixInfo tmi, double minScale ) + tileMatrixInfo getTileMatrixInfo( const QString &crsStr ) { - int DOTS_PER_INCH = 72; - double METERS_PER_INCH = 0.02540005080010160020; - QMap< QString, double> INCHES_PER_UNIT; - INCHES_PER_UNIT["inches"] = 1.0; - INCHES_PER_UNIT["ft"] = 12.0; - INCHES_PER_UNIT["mi"] = 63360.0; - INCHES_PER_UNIT["m"] = 39.37; - INCHES_PER_UNIT["km"] = 39370.0; - INCHES_PER_UNIT["dd"] = 4374754.0; - INCHES_PER_UNIT["yd"] = 36.0; - INCHES_PER_UNIT["in"] = INCHES_PER_UNIT["inches"]; - INCHES_PER_UNIT["degrees"] = INCHES_PER_UNIT["dd"]; - INCHES_PER_UNIT["nmi"] = 1852.0 * INCHES_PER_UNIT["m"]; - INCHES_PER_UNIT["cm"] = INCHES_PER_UNIT["m"] / 100.0; - INCHES_PER_UNIT["mm"] = INCHES_PER_UNIT["m"] / 1000.0; + if ( tileMatrixInfoMap.contains( crsStr ) ) + return tileMatrixInfoMap[crsStr]; + + tileMatrixInfo tmi; + tmi.ref = crsStr; + + QgsCoordinateReferenceSystem crs = QgsCoordinateReferenceSystem::fromOgcWmsCrs( crsStr ); + Q_NOWARN_DEPRECATED_PUSH + QgsCoordinateTransform crsTransform( wgs84, crs ); + Q_NOWARN_DEPRECATED_POP + try + { + tmi.extent = crsTransform.transformBoundingBox( crs.bounds() ); + } + catch ( QgsCsException &cse ) + { + Q_UNUSED( cse ); + } + + QgsUnitTypes::DistanceUnit mapUnits = crs.mapUnits(); + if ( mapUnits == QgsUnitTypes::DistanceMeters ) + tmi.unit = "m"; + else if ( mapUnits == QgsUnitTypes::DistanceKilometers ) + tmi.unit = "km"; + else if ( mapUnits == QgsUnitTypes::DistanceFeet ) + tmi.unit = "ft"; + else if ( mapUnits == QgsUnitTypes::DistanceNauticalMiles ) + tmi.unit = "nmi"; + else if ( mapUnits == QgsUnitTypes::DistanceYards ) + tmi.unit = "yd"; + else if ( mapUnits == QgsUnitTypes::DistanceMiles ) + tmi.unit = "mi"; + else if ( mapUnits == QgsUnitTypes::DistanceDegrees ) + tmi.unit = "dd"; + else if ( mapUnits == QgsUnitTypes::DistanceCentimeters ) + tmi.unit = "cm"; + else if ( mapUnits == QgsUnitTypes::DistanceMillimeters ) + tmi.unit = "mm"; + + // calculate tile matrix scale denominator + double scaleDenominator = 0.0; + int colRes = ( tmi.extent.xMaximum() - tmi.extent.xMinimum() ) / tileWidth; + int rowRes = ( tmi.extent.yMaximum() - tmi.extent.yMinimum() ) / tileHeight; + if ( colRes < rowRes ) + scaleDenominator = colRes * INCHES_PER_UNIT[ tmi.unit ] * METERS_PER_INCH / 0.00028; + else + scaleDenominator = rowRes * INCHES_PER_UNIT[ tmi.unit ] * METERS_PER_INCH / 0.00028; + tmi.scaleDenominator = scaleDenominator; - int tileWidth = 256; - int tileHeight = 256; + tileMatrixInfoMap[crsStr] = tmi; + return tmi; + } + tileMatrixSet getTileMatrixSet( tileMatrixInfo tmi, double minScale ) + { QList< tileMatrix > tileMatrixList; double scaleDenominator = tmi.scaleDenominator; QgsRectangle extent = tmi.extent; @@ -135,16 +190,9 @@ namespace QgsWmts return tms; } - QList< tileMatrixSet > getTileMatrixSetList( const QgsProject *project ) + double getProjectMinScale( const QgsProject *project ) { - QList< tileMatrixSet > tmsList; - - double minScale = -1.0; - double maxScale = -1.0; - - - QgsRectangle projRect = QgsServerProjectUtils::wmsExtent( *project ); - QgsCoordinateReferenceSystem projCrs = project->crs(); + double scale = -1.0; // default scales QgsSettings settings; @@ -161,102 +209,35 @@ namespace QgsWmts Q_FOREACH ( const QString &scaleText, scaleList ) { double scaleValue = scaleText.toDouble(); - if ( minScale == -1.0 && maxScale == -1.0 ) + if ( scale == -1.0 ) { - minScale = scaleValue; - maxScale = scaleValue; + scale = scaleValue; } - else + else if ( scaleValue < scale ) { - if ( scaleValue < minScale ) - { - minScale = scaleValue; - } - if ( scaleValue > maxScale ) - { - maxScale = scaleValue; - } + scale = scaleValue; } } } - else + if ( scale < 500.0 ) { - minScale = 5000.0; - maxScale = 1000000.0; - } - if ( minScale < 500.0 ) - { - minScale = 500.0; - } - if ( minScale == maxScale || minScale > maxScale ) - { - maxScale = minScale * 2.0; + return 500.0; } + return scale; + } - QStringList crsList = QgsServerProjectUtils::wmsOutputCrsList( *project ); - Q_FOREACH ( const QString &crsText, crsList ) - { - if ( crsText == "EPSG:3857" ) - { - tileMatrixInfo tmi3857; - tmi3857.ref = "EPSG:3857"; - tmi3857.extent = QgsRectangle( -20037508.3427892480, -20037508.3427892480, 20037508.3427892480, 20037508.3427892480 ); - tmi3857.scaleDenominator = 559082264.0287179; - tmi3857.unit = "m"; + QList< tileMatrixSet > getTileMatrixSetList( const QgsProject *project ) + { + QList< tileMatrixSet > tmsList; - tmsList.append( getTileMatrixSet( tmi3857, minScale ) ); - } - else if ( crsText == "EPSG:4326" ) - { - tileMatrixInfo tmi4326; - tmi4326.ref = "EPSG:4326"; - tmi4326.extent = QgsRectangle( -180, -90, 180, 90 ); - tmi4326.scaleDenominator = 279541132.0143588675418869; - tmi4326.unit = "dd"; + double minScale = getProjectMinScale( project ); - tmsList.append( getTileMatrixSet( tmi4326, minScale ) ); - } - else + QStringList crsList = QgsServerProjectUtils::wmsOutputCrsList( *project ); + Q_FOREACH ( const QString &crsStr, crsList ) + { + tileMatrixInfo tmi = getTileMatrixInfo( crsStr ); + if ( tmi.scaleDenominator > 0.0 ) { - tileMatrixInfo tmi; - tmi.ref = crsText; - - QgsCoordinateReferenceSystem crs = QgsCoordinateReferenceSystem::fromOgcWmsCrs( crsText ); - Q_NOWARN_DEPRECATED_PUSH - QgsCoordinateTransform crsTransform( projCrs, crs ); - Q_NOWARN_DEPRECATED_POP - try - { - tmi.extent = crsTransform.transformBoundingBox( projRect ); - } - catch ( QgsCsException &cse ) - { - Q_UNUSED( cse ); - continue; - } - - tmi.scaleDenominator = maxScale; - - QgsUnitTypes::DistanceUnit mapUnits = crs.mapUnits(); - if ( mapUnits == QgsUnitTypes::DistanceMeters ) - tmi.unit = "m"; - else if ( mapUnits == QgsUnitTypes::DistanceKilometers ) - tmi.unit = "km"; - else if ( mapUnits == QgsUnitTypes::DistanceFeet ) - tmi.unit = "ft"; - else if ( mapUnits == QgsUnitTypes::DistanceNauticalMiles ) - tmi.unit = "nmi"; - else if ( mapUnits == QgsUnitTypes::DistanceYards ) - tmi.unit = "yd"; - else if ( mapUnits == QgsUnitTypes::DistanceMiles ) - tmi.unit = "mi"; - else if ( mapUnits == QgsUnitTypes::DistanceDegrees ) - tmi.unit = "dd"; - else if ( mapUnits == QgsUnitTypes::DistanceCentimeters ) - tmi.unit = "cm"; - else if ( mapUnits == QgsUnitTypes::DistanceMillimeters ) - tmi.unit = "mm"; - tmsList.append( getTileMatrixSet( tmi, minScale ) ); } } @@ -264,9 +245,9 @@ namespace QgsWmts return tmsList; } - QUrlQuery translateWmtsParamToWmsQueryItem( const QString &request, const QgsServerRequest::Parameters ¶ms, const QgsProject *project ) + QUrlQuery translateWmtsParamToWmsQueryItem( const QString &request, const QgsServerRequest::Parameters ¶ms, + const QgsProject *project, QgsServerInterface *serverIface ) { - //defining Layer QString layer; //read Layer @@ -279,6 +260,73 @@ namespace QgsWmts { throw QgsRequestNotWellFormedException( QStringLiteral( "Layer is mandatory" ) ); } + //check layer value + bool wmtsProject = project->readBoolEntry( QStringLiteral( "WMTSLayers" ), QStringLiteral( "Project" ) ); + QStringList wmtsGroupNameList = project->readListEntry( QStringLiteral( "WMTSLayers" ), QStringLiteral( "Group" ) ); + QStringList wmtsLayerIdList = project->readListEntry( QStringLiteral( "WMTSLayers" ), QStringLiteral( "Layer" ) ); + QStringList wmtsLayerIds; + if ( wmtsProject ) + { + // Root Layer name + QString rootLayerId = QgsServerProjectUtils::wmsRootName( *project ); + if ( rootLayerId.isEmpty() ) + { + rootLayerId = project->title(); + } + if ( !rootLayerId.isEmpty() ) + { + wmtsLayerIds << rootLayerId; + } + } + if ( !wmtsGroupNameList.isEmpty() ) + { + QgsLayerTreeGroup *treeRoot = project->layerTreeRoot(); + Q_FOREACH ( QString gName, wmtsGroupNameList ) + { + QgsLayerTreeGroup *treeGroup = treeRoot->findGroup( gName ); + if ( !treeGroup ) + { + continue; + } + QString groupLayerId = treeGroup->customProperty( QStringLiteral( "wmsShortName" ) ).toString(); + if ( groupLayerId.isEmpty() ) + { + groupLayerId = gName; + } + wmtsLayerIds << groupLayerId; + } + } + if ( !wmtsLayerIdList.isEmpty() ) + { +#ifdef HAVE_SERVER_PYTHON_PLUGINS + QgsAccessControl *accessControl = serverIface->accessControls(); +#endif + Q_FOREACH ( QString lId, wmtsLayerIdList ) + { + QgsMapLayer *l = project->mapLayer( lId ); + if ( !l ) + { + continue; + } +#ifdef HAVE_SERVER_PYTHON_PLUGINS + if ( !accessControl->layerReadPermission( l ) ) + { + continue; + } +#endif + QString layerLayerId = l->shortName(); + if ( layerLayerId.isEmpty() ) + { + layerLayerId = l->name(); + } + wmtsLayerIds << layerLayerId; + } + } + if ( !wmtsLayerIds.contains( layer ) ) + { + QString msg = QObject::tr( "Layer '%1' not found" ).arg( layer ); + throw QgsBadRequestException( QStringLiteral( "LayerNotDefined" ), msg ); + } //defining Format QString format; @@ -293,13 +341,6 @@ namespace QgsWmts throw QgsRequestNotWellFormedException( QStringLiteral( "Format is mandatory" ) ); } - QList< tileMatrixSet > tmsList = getTileMatrixSetList( project ); - if ( tmsList.isEmpty() ) - { - throw QgsServiceException( QStringLiteral( "UnknownError" ), - QStringLiteral( "Service not well configured" ) ); - } - //defining TileMatrixSet ref QString tms_ref; //read TileMatrixSet @@ -313,23 +354,19 @@ namespace QgsWmts throw QgsRequestNotWellFormedException( QStringLiteral( "TileMatrixSet is mandatory" ) ); } - bool tms_ref_valid = false; - tileMatrixSet tms; - QList::iterator tmsIt = tmsList.begin(); - for ( ; tmsIt != tmsList.end(); ++tmsIt ) + // verifying TileMatricSet value + QStringList crsList = QgsServerProjectUtils::wmsOutputCrsList( *project ); + if ( !crsList.contains( tms_ref ) ) { - tileMatrixSet &tmsi = *tmsIt; - if ( tmsi.ref == tms_ref ) - { - tms_ref_valid = true; - tms = tmsi; - break; - } + throw QgsRequestNotWellFormedException( QStringLiteral( "TileMatrixSet is unknown" ) ); } - if ( !tms_ref_valid ) + + tileMatrixInfo tmi = getTileMatrixInfo( tms_ref ); + if ( tmi.scaleDenominator == 0.0 ) { throw QgsRequestNotWellFormedException( QStringLiteral( "TileMatrixSet is unknown" ) ); } + tileMatrixSet tms = getTileMatrixSet( tmi, getProjectMinScale( project ) ); bool conversionSuccess = false; @@ -449,6 +486,51 @@ namespace QgsWmts return query; } + namespace + { + + QMap< QString, double> populateInchesPerUnit() + { + QMap< QString, double> m; + m["inches"] = 1.0; + m["ft"] = 12.0; + m["mi"] = 63360.0; + m["m"] = 39.37; + m["km"] = 39370.0; + m["dd"] = 4374754.0; + m["yd"] = 36.0; + m["in"] = m["inches"]; + m["degrees"] = m["dd"]; + m["nmi"] = 1852.0 * m["m"]; + m["cm"] = m["m"] / 100.0; + m["mm"] = m["m"] / 1000.0; + return m; + } + + QMap< QString, tileMatrixInfo> populateTileMatrixInfoMap() + { + QMap< QString, tileMatrixInfo> m; + + tileMatrixInfo tmi3857; + tmi3857.ref = "EPSG:3857"; + tmi3857.extent = QgsRectangle( -20037508.3427892480, -20037508.3427892480, 20037508.3427892480, 20037508.3427892480 ); + tmi3857.scaleDenominator = 559082264.0287179; + tmi3857.unit = "m"; + m[tmi3857.ref] = tmi3857; + + + tileMatrixInfo tmi4326; + tmi4326.ref = "EPSG:4326"; + tmi4326.extent = QgsRectangle( -180, -90, 180, 90 ); + tmi4326.scaleDenominator = 279541132.0143588675418869; + tmi4326.unit = "dd"; + m[tmi4326.ref] = tmi4326; + + return m; + } + + } + } // namespace QgsWmts diff --git a/src/server/services/wmts/qgswmtsutils.h b/src/server/services/wmts/qgswmtsutils.h index 2aca99912568..b40162367e96 100644 --- a/src/server/services/wmts/qgswmtsutils.h +++ b/src/server/services/wmts/qgswmtsutils.h @@ -106,13 +106,16 @@ namespace QgsWmts const QString GML_NAMESPACE = QStringLiteral( "http://www.opengis.net/gml" ); const QString OWS_NAMESPACE = QStringLiteral( "http://www.opengis.net/ows/1.1" ); - tileMatrixSet getTileMatrixSet( tileMatrixInfo tmi ); + tileMatrixInfo getTileMatrixInfo( const QString &crsStr ); + tileMatrixSet getTileMatrixSet( tileMatrixInfo tmi, double minScale ); + double getProjectMinScale( const QgsProject *project ); QList< tileMatrixSet > getTileMatrixSetList( const QgsProject *project ); /** * Translate WMTS parameters to WMS query item */ - QUrlQuery translateWmtsParamToWmsQueryItem( const QString &request, const QgsServerRequest::Parameters ¶ms, const QgsProject *project ); + QUrlQuery translateWmtsParamToWmsQueryItem( const QString &request, const QgsServerRequest::Parameters ¶ms, + const QgsProject *project, QgsServerInterface *serverIface ); } // namespace QgsWmts From 385de9db003e42d8fd7b7852b9bb75fedef8efee Mon Sep 17 00:00:00 2001 From: rldhont Date: Wed, 1 Aug 2018 11:42:15 +0200 Subject: [PATCH 10/33] [Server][Feature][needs-docs] Create WMTS service Tests --- tests/src/python/CMakeLists.txt | 1 + tests/src/python/test_qgsserver_wmts.py | 195 + .../WMTS_GetTile_CountryGroup_3857_0.png | Bin 0 -> 25799 bytes .../WMTS_GetTile_CountryGroup_4326_0.png | Bin 0 -> 19400 bytes .../WMTS_GetTile_Hello_3857_0.png | Bin 0 -> 13157 bytes .../WMTS_GetTile_Hello_4326_0.png | Bin 0 -> 12340 bytes .../WMTS_GetTile_Project_3857_0.png | Bin 0 -> 33108 bytes .../WMTS_GetTile_Project_4326_0.png | Bin 0 -> 27852 bytes .../qgis_server/wmts_getcapabilities.txt | 1373 +++++++ .../project_groups.qgs | 3259 +++++------------ 10 files changed, 2478 insertions(+), 2350 deletions(-) create mode 100644 tests/src/python/test_qgsserver_wmts.py create mode 100644 tests/testdata/control_images/qgis_server/WMTS_GetTile_CountryGroup_3857_0/WMTS_GetTile_CountryGroup_3857_0.png create mode 100644 tests/testdata/control_images/qgis_server/WMTS_GetTile_CountryGroup_4326_0/WMTS_GetTile_CountryGroup_4326_0.png create mode 100644 tests/testdata/control_images/qgis_server/WMTS_GetTile_Hello_3857_0/WMTS_GetTile_Hello_3857_0.png create mode 100644 tests/testdata/control_images/qgis_server/WMTS_GetTile_Hello_4326_0/WMTS_GetTile_Hello_4326_0.png create mode 100644 tests/testdata/control_images/qgis_server/WMTS_GetTile_Project_3857_0/WMTS_GetTile_Project_3857_0.png create mode 100644 tests/testdata/control_images/qgis_server/WMTS_GetTile_Project_4326_0/WMTS_GetTile_Project_4326_0.png create mode 100644 tests/testdata/qgis_server/wmts_getcapabilities.txt diff --git a/tests/src/python/CMakeLists.txt b/tests/src/python/CMakeLists.txt index f198c7e4834b..c74dd3a88b03 100644 --- a/tests/src/python/CMakeLists.txt +++ b/tests/src/python/CMakeLists.txt @@ -269,6 +269,7 @@ IF (WITH_SERVER) ADD_PYTHON_TEST(PyQgsServerAccessControlWCS test_qgsserver_accesscontrol_wcs.py) ADD_PYTHON_TEST(PyQgsServerAccessControlWFSTransactional test_qgsserver_accesscontrol_wfs_transactional.py) ADD_PYTHON_TEST(PyQgsServerCacheManager test_qgsserver_cachemanager.py) + ADD_PYTHON_TEST(PyQgsServerWMTS test_qgsserver_wmts.py) ADD_PYTHON_TEST(PyQgsServerWFS test_qgsserver_wfs.py) ADD_PYTHON_TEST(PyQgsServerWFST test_qgsserver_wfst.py) ADD_PYTHON_TEST(PyQgsOfflineEditingWFS test_offline_editing_wfs.py) diff --git a/tests/src/python/test_qgsserver_wmts.py b/tests/src/python/test_qgsserver_wmts.py new file mode 100644 index 000000000000..a640c9d18b67 --- /dev/null +++ b/tests/src/python/test_qgsserver_wmts.py @@ -0,0 +1,195 @@ +# -*- coding: utf-8 -*- +"""QGIS Unit tests for QgsServer WFS. + +From build dir, run: ctest -R PyQgsServerWFS -V + + +.. note:: 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. + +""" +__author__ = 'René-Luc Dhont' +__date__ = '19/09/2017' +__copyright__ = 'Copyright 2017, The QGIS Project' +# This will get replaced with a git SHA1 when you do a git archive +__revision__ = '$Format:%H$' + +import os + +# Needed on Qt 5 so that the serialization of XML is consistent among all executions +os.environ['QT_HASH_SEED'] = '1' + +import re +import urllib.request +import urllib.parse +import urllib.error + +from qgis.server import QgsServerRequest + +from qgis.testing import unittest +from qgis.PyQt.QtCore import QSize + +import osgeo.gdal # NOQA + +from test_qgsserver import QgsServerTestBase + +# Strip path and content length because path may vary +RE_STRIP_UNCHECKABLE = b'MAP=[^"]+|Content-Length: \d+|timeStamp="[^"]+"' +RE_ATTRIBUTES = b'[^>\s]+=[^>\s]+' + + +class TestQgsServerWMTS(QgsServerTestBase): + + """QGIS Server WMTS Tests""" + + def wmts_request_compare(self, request, version='', extra_query_string='', reference_base_name=None): + #project = self.testdata_path + "test_project_wfs.qgs" + project = self.projectGroupsPath + assert os.path.exists(project), "Project file not found: " + project + + query_string = '?MAP=%s&SERVICE=WMTS&REQUEST=%s' % (urllib.parse.quote(project), request) + if version: + query_string += '&VERSION=%s' % version + + if extra_query_string: + query_string += '&%s' % extra_query_string + + header, body = self._execute_request(query_string) + self.assert_headers(header, body) + response = header + body + + if reference_base_name is not None: + reference_name = reference_base_name + else: + reference_name = 'wmts_' + request.lower() + + reference_name += '.txt' + + reference_path = self.testdata_path + reference_name + + self.store_reference(reference_path, response) + f = open(reference_path, 'rb') + expected = f.read() + f.close() + response = re.sub(RE_STRIP_UNCHECKABLE, b'', response) + expected = re.sub(RE_STRIP_UNCHECKABLE, b'', expected) + + self.assertXMLEqual(response, expected, msg="request %s failed.\n Query: %s" % (query_string, request)) + + def test_project_wmts(self): + """Test some WMTS request""" + for request in ('GetCapabilities',): + self.wmts_request_compare(request) + #self.wmts_request_compare(request, '1.0.0') + + def test_wmts_gettile(self): + # Testing project WMTS layer + qs = "?" + "&".join(["%s=%s" % i for i in list({ + "MAP": urllib.parse.quote(self.projectGroupsPath), + "SERVICE": "WMTS", + "VERSION": "1.0.0", + "REQUEST": "GetTile", + "LAYER": "QGIS Server Hello World", + "STYLE": "", + "TILEMATRIXSET": "EPSG:3857", + "TILEMATRIX": "0", + "TILEROW": "0", + "TILECOL": "0", + "FORMAT": "image/png" + }.items())]) + + r, h = self._result(self._execute_request(qs)) + self._img_diff_error(r, h, "WMTS_GetTile_Project_3857_0", 20000) + + qs = "?" + "&".join(["%s=%s" % i for i in list({ + "MAP": urllib.parse.quote(self.projectGroupsPath), + "SERVICE": "WMTS", + "VERSION": "1.0.0", + "REQUEST": "GetTile", + "LAYER": "QGIS Server Hello World", + "STYLE": "", + "TILEMATRIXSET": "EPSG:4326", + "TILEMATRIX": "0", + "TILEROW": "0", + "TILECOL": "0", + "FORMAT": "image/png" + }.items())]) + + r, h = self._result(self._execute_request(qs)) + self._img_diff_error(r, h, "WMTS_GetTile_Project_4326_0", 20000) + + # Testing group WMTS layer + qs = "?" + "&".join(["%s=%s" % i for i in list({ + "MAP": urllib.parse.quote(self.projectGroupsPath), + "SERVICE": "WMTS", + "VERSION": "1.0.0", + "REQUEST": "GetTile", + "LAYER": "CountryGroup", + "STYLE": "", + "TILEMATRIXSET": "EPSG:3857", + "TILEMATRIX": "0", + "TILEROW": "0", + "TILECOL": "0", + "FORMAT": "image/png" + }.items())]) + + r, h = self._result(self._execute_request(qs)) + self._img_diff_error(r, h, "WMTS_GetTile_CountryGroup_3857_0", 20000) + + qs = "?" + "&".join(["%s=%s" % i for i in list({ + "MAP": urllib.parse.quote(self.projectGroupsPath), + "SERVICE": "WMTS", + "VERSION": "1.0.0", + "REQUEST": "GetTile", + "LAYER": "CountryGroup", + "STYLE": "", + "TILEMATRIXSET": "EPSG:4326", + "TILEMATRIX": "0", + "TILEROW": "0", + "TILECOL": "0", + "FORMAT": "image/png" + }.items())]) + + r, h = self._result(self._execute_request(qs)) + self._img_diff_error(r, h, "WMTS_GetTile_CountryGroup_4326_0", 20000) + + # Testing QgsMapLayer WMTS layer + qs = "?" + "&".join(["%s=%s" % i for i in list({ + "MAP": urllib.parse.quote(self.projectGroupsPath), + "SERVICE": "WMTS", + "VERSION": "1.0.0", + "REQUEST": "GetTile", + "LAYER": "Hello", + "STYLE": "", + "TILEMATRIXSET": "EPSG:3857", + "TILEMATRIX": "0", + "TILEROW": "0", + "TILECOL": "0", + "FORMAT": "image/png" + }.items())]) + + r, h = self._result(self._execute_request(qs)) + self._img_diff_error(r, h, "WMTS_GetTile_Hello_3857_0", 20000) + + qs = "?" + "&".join(["%s=%s" % i for i in list({ + "MAP": urllib.parse.quote(self.projectGroupsPath), + "SERVICE": "WMTS", + "VERSION": "1.0.0", + "REQUEST": "GetTile", + "LAYER": "Hello", + "STYLE": "", + "TILEMATRIXSET": "EPSG:4326", + "TILEMATRIX": "0", + "TILEROW": "0", + "TILECOL": "0", + "FORMAT": "image/png" + }.items())]) + + r, h = self._result(self._execute_request(qs)) + self._img_diff_error(r, h, "WMTS_GetTile_Hello_4326_0", 20000) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/testdata/control_images/qgis_server/WMTS_GetTile_CountryGroup_3857_0/WMTS_GetTile_CountryGroup_3857_0.png b/tests/testdata/control_images/qgis_server/WMTS_GetTile_CountryGroup_3857_0/WMTS_GetTile_CountryGroup_3857_0.png new file mode 100644 index 0000000000000000000000000000000000000000..276c41e0400a6dbbdaa480a1ce3b9bd827308dab GIT binary patch literal 25799 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 literal 0 HcmV?d00001 diff --git a/tests/testdata/control_images/qgis_server/WMTS_GetTile_CountryGroup_4326_0/WMTS_GetTile_CountryGroup_4326_0.png b/tests/testdata/control_images/qgis_server/WMTS_GetTile_CountryGroup_4326_0/WMTS_GetTile_CountryGroup_4326_0.png new file mode 100644 index 0000000000000000000000000000000000000000..8c6486b88f6ee804b266c67857dde39e8b8db474 GIT binary patch literal 19400 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 Tm_|!~00000NkvXXu0mjfh?Tw) literal 0 HcmV?d00001 diff --git a/tests/testdata/control_images/qgis_server/WMTS_GetTile_Hello_3857_0/WMTS_GetTile_Hello_3857_0.png b/tests/testdata/control_images/qgis_server/WMTS_GetTile_Hello_3857_0/WMTS_GetTile_Hello_3857_0.png new file mode 100644 index 0000000000000000000000000000000000000000..b9a737f05372ca54c0444f10e5007704b307fdf8 GIT binary patch literal 13157 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 s5Az~>|dN@&Ni&0b+*n- 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 G$o~P{kUY@< literal 0 HcmV?d00001 diff --git a/tests/testdata/control_images/qgis_server/WMTS_GetTile_Hello_4326_0/WMTS_GetTile_Hello_4326_0.png b/tests/testdata/control_images/qgis_server/WMTS_GetTile_Hello_4326_0/WMTS_GetTile_Hello_4326_0.png new file mode 100644 index 0000000000000000000000000000000000000000..2b54a1bf8bce84d3dd72d39155bdad0774f2e8f1 GIT binary patch literal 12340 zcmd^l^;cX^@Z}ra9fC^;4nYHh1%d|;?izx-`=I$i2<}d>Ai>>zg1dW=!6mrw3#)BEoNK^=d71^`;%qtpitugv3B@6=4qEY7o2!BaKg)BUlzMIlp6eF<7S330rz zZ}hKy+QDdR_|DRdv*+6ph>J6nqtl_kq01_m_I00s zehbTdkmWF3EqS-E0ek=aetcSmWBzx-z_X-90`>6FSH8V|~ z=0+A||B@8ua$FMu#9zk&=5|j3M$bpcdAc{*Vuy;x45*z8H|Y;rUf+;6g3O>ZWZk|h zHiGxHvNN2j54uGkt1E6a&rg0|n`%ZN$T%oUUTW_0sr>H8C{avRKCp%`&B!O(v z|N1knit<-D*Xxg3#H$hE8{^=A(aMAfCPENbFRQ#uvlx|q$QTjIUsQMGPj&an!WYlN zmuM%G%8Gj)TAL>={si$OsIgS}!5M*p*wwK2cEm0XM`;=WBBWmLfXCP#yuI0eqGn}8 zH}mKWb{jW#T6Yq!sntNIl3ur&-G6+<#1w1Jkxr)!-1BB(rNuyz8B6nZ8FmJHFF8w( z2kwQe4|~cs%u>}MkmT_iW{Mp~{a9CNVp+KyXe>K?_uk7-B67pOc!i{N_YDTHb-bFj z$EHV|wYNHXoSJcWi3+jeOGh}n4loidOyR+d=Ck3W0l)1?*rp8!%v$3UA@O`o&Y zxMM40A*LH}oZPJl_4so%@Fu>e;&v&$=;O8ZT#*)a!$y3#A^s zJ=pts5dem3x&(1OJvGH?4q-sOr|`KSBjC43MY10a+#V9}A+In#>8 ztMmRW{GRs)1K7t0l&1NqyXpxW>gwci=kn<3rhXBR37SU{-|I7)DNjj909?cCk%p4K zXO3bm#RmwdF0|2BK?MGEhG6G0SVgwOLdU|?*xVwjkkFc-ID+NOH)4H$C6U{w+1(dAvV478dW}(!~0D%-oz79818_>fR+~v5Jg&hxZS& z*{}E>uK_Q^4T z4J#2R#E_*Y-)VIqvm{E*MA8w-LsjAPGygPfFX`SIrn)(SqpdENz&~FWpRPlc2hFAK zE=^4;8U+xk!N?2bIvdH%v7~K*lA75b1KyIbCsDq&3~>K7`{lQf$<& z4X}~fsOTK$a&ReLfzl+c>#x8vvNl_!~-&u@8Df(7aKQsHQ$EQt}fQ zo)Mx3?!&7SaBSk9#S#hT#3zdta>m(9G@S}tb z5HXUWIPXrIbr>LrW(_)0-X0nV!&4^)_`X|Pym+Ii^;%iU&jSYYg@CH~c(&J_UDt#1 z9+EQY4N-#pfyAJjsw#m*MB4*_TExmzL`-2k8X2h0JA;k&&A2TB*FYC38&TCCEn>-U zJAPifgS&GrYqK=18|J4?6%WJ0r){;2LAhgSw8@H|b^9$ig)?-7z(Eu(L7W@GEK!WA z1|J5ON!X|PcpM;lOBGnHN2dD2bVU7aIil9+gTf$Lm$hMMbj9w59p>bxe){Cr9wYNdNv?RuDGpf_9Q zN)Q&xA2aSo?}5u@Q_5v0FwoUtj{zV{g%Po`sGV8&Z*>uha&D-Epz<7baux!4NDjjH zIUDl|P}=X-s^0m4bUHB6{Do;Hk$U3WI8Q*y3-(Gq%o2RCkZ+ob!l(Y#VQ<2n6N^4n zs25Uk$c2Rn8*4`cCT3;|u(7Z*zy7r)P>jrF%5r{%5k^~BZZ+h_5gG*$Ogh&ml#u@@ zFURd7moYCQjI{xzz1+v0j9t9u+Dy(r=O#G@QoxM;=n#SipSbio14IMP)_QLb;!=`C zl+Df4Ap0V@J%I`#`&vCpcM8aW!jhGP&A^Uv+mAmk{QUg)U&F)Se)xp|NuzeKwX%Y! z*|55vK+z8Z-VmCDOfY1b-LkY7&=+D)5UcE4|BlplG6+pSJg7@DAa+HWutf`>fZg2O z6c6fUWn>_YLo_L4NR)?V>{^H$nZ*RJBVMzmzv2k7&{dWHz>;KmfF;kO&B73hrzT&L zsMgPPu2KUz8CGekuIzlizpuBQkDfcY?=O&L#`zAIXmDYHNpWE|K(a+4GeGpl^NJBQ zB~i$%r4hNew|8%EuWN7bCk4(|HY3^GL?>|hXy0W`;46DSMw4n~aVtfIH!Gj}=VPQ^ zd!4r5G_VvJsme2e-T-e354G8EPU3sKzPxV38gRU`*=daz+487{&*AX=+ie3fWh z1VvpYlV)=99wiXgnb(wSFD10Z@9*Gxe4>*_Vg}9n7k)ZQ97LeOVb!L!qBWVDn>TR@JsQ@C=_@|GZ? zkB*z3s{j=z2_Hi##OQBek=VPa-Ii*MNc&~v;@Ia=g%+)wwQZ9@yDqXXm>#%=>fuV$ zbyUzhe|QB|mf&>f({OtklcxyC!KbqUGGCRvaFeKLRZmUiu@*c)|4kAgH-dI~XjE`? zd8$3lC&yCEj@!koj!0V5^K)DQ(Ss6NV9|{k8!OsM=&LZ7p)aaLN=6O>yxNT_5?Mw4 zo#L&%{ei-CK$TTXiEu-ec-ws1fz13!RUQSy3t`dCiMG9RTVujas{>_X0ZalEBr?td z7>A2gG|Y=dju-VNpJ@#dxv+YB1uNYIPd~Imwmny-T*x@crcE zgapWu@hNd=4A$4(z5_$LpPuP1W^6%NmUBp4N(l-qxDkv15Xu9}ZEp5v{mT6Adx{aE z9WAlU)8z^p0EXog!Dpqa3YUN8(Qj8mJCzr>6Qz#%>iWKT<~)HT3mr_7Uyz7w3lQ+c zGfyMksvfFk(kw=;i5)@c4ZT8ZH-dF7S75Ap1}urH2o>_{LTcj7C7}d(Lt$la64L5m ziub&P@;q_WKce7_XAGrkM?7#Xm%3|r4R>OCI%IP4Q+{J(Bh`K$HYVh|TUQ!E!0NZ- z<;GrjL#g~vYg1D^u1-!|*(@2zxg^6ihcap8U28MI_2p^5>$oWDG~eX>{HuIGRcBn) zrfxgk#D0wg>-+XSkb8I^oSw)oX|7j6$OXqoF6@!G6S-zaNH_N5g=r4W&EM3}%xhv5 zqqp#AOo(T8HhsW&JgL`gdboNgCz#A>!Jv)m{&HMFkggi6z#wRuUdFv^qv$Hw)C+b6 zH;qn%dO+s<>T)atcH)JJY9G-H5(z$rki}px(swTA)EG`W;|DxG8H+PV6l6^quw5C6 z-*9WAk0wc&L;ggyl5a&1@TCL^X7YZu7#>LNC9S134Jt`QP*bf!l2T*c32nQYzmoDD znk{hqo`M4y3;*Q-pRUDZLmN#ds6PbyS&<+^ddvva7*;eUquQ+3WU!>C-vPZ`xrwFL z*tBB!%=sJZQ}J_q8XVZvuRhu@)>yd1vOL$=d%5D_MgFXPYocXCCwAcN#LY#{5|jAy z)TG_MmtEXPi?IN7)Myt|Ow{DMO}SU_RDe#=;KpV5ePitVO&~#605W8FiL=k*_|+F0 z9>qP>SoxmhXa@yoMvjgB;F15U2Dy~sc$_0eG7i(N{p^(Uk+sfC#9N;;Z8k?V<62}s%X}g9RyN>?fYfx7b54kHL!oJE? zdd()vICv{cRuC4eHt>(-)V8*?@Y9qZrma8*zn3yKchVdWG_F7O%QY8GJ^zX7E%Tnj z9Mx)rX)M8c-Tu+tv^ert8_GgNaXKO(QqB?)Cj|@O=wQUc#Wf$x6pU5V;*ar+-@XAQ z9v~s%eG_sV&)i2qO38n%2`lfQ@DOrr{y1uCC^wI`wI!U;;l_^Eiy! zyzbz9D>)`Q`ZXnvEpyc-8*F8QEftFo9~tsmB>V+4=2)+B_omhOax_cel<*m{4r_VK zJ{)YmTxCZQ1>*fO0swn0*_mR2&sdb(vo3b~v<&Q0nG1c*K>z#Atxt`YQyZv~BTBRz zRJItc6~ltCub+Ycz}$R%{JFn=VJBOdj%SOiXPadb<2$owy{EFbw|AmMh36mSmrig~ ziFOma*Lw^=`f|Vr6?k+6n(lc6Z(zN+Vt(F+hUa;fyTJv;KezNAu1ys{k5fKrGd6xq} zO!GO7LH)_X^SB4TDE8D-AE;orW71yTjR!v0&_{#+gx_7jY}4`vf|))GfxbQU8=pp> zik1m?G~U%sE4P}={g9roYiPK)Uk={_SJ_jTE{B`GS1TdSMsG>WCAmFm>G1$KF|? zx~||bDw@i&O50Br!5NAvqaITh&Xi34>+UA4i?+h5@`dZME$A^0JtHU>qoIRW&W>uF zDuHApa_g4VUd<8n;cFjkp&|eT4T)+km1Y#bQySkATlo7s>BhnPH(spKSko?V`x%9impd+8;dsA_nX?;YeA5jTMYDqY zT9T9D%`n^nwZN3uPGccz@||i!xLxC<7tDn<=3$zrKI(nT0GI3TNSrD&QfH@=hqXs( z#bJy1zRB}jAT!{8e-!@FrgHAXPe`vQO_YDR&?l)0>aQtZsrZ=If$_; zR7z@U5(%`T#6R*ZZ$l?ypZm057awWd4M}fttfFy>i%VLC$kk!GX+yJT?a|c zGcRiPyV8pc*XHLvIbbU(%@x`#JHrON`qMnM^CK(dfE!EU04ZS_=OAf9ON9c!`Vc)>}ePNEuO@pYnMoh{53tSPWDfXY1sC@t3u3tG<95Tc!^7VXtY(_bKy3O%>aJeT zH4z37=A}68yJ-3L!Nu42Aty;K(6HU-if&RRHeRc0vfSarpDMx+&}vLcg=F>*eB}Lb z(G(}59tD7}NCw<=_s0W&Mvh%!BBVEJ!tVJ7bZRw9Kd|b;35b;fQ*N%8_|5W%URd}g#D?#1Vy|Qi`%BiO z9e*lzcI@0A>k$sx1Sj?X1kuy*(3y7MtftES*OV1Y1js2ljoB+K5z{zE8DFUGw;1iX zq@zl^=Ia_?k-yL@BXzd{8(?L}h+c|=sIF;k8fR;`O!H$6>&KW}pvCDtMfXuK{p_&#uK}? z&K2+f1Rgt*l}Y#_#*>>iaFkjhRGkac5dQ=rJ*In3K%9Ht5LXd!*k|vS+P==$BFaC5k+H?wKOVLtJx1*P3 zy%$C4{e)W*gd=mMR6Kf%dCYqfz~gz89m586)Kz$m6BT zbhe6u)2>}X`xXB&Fc=Jf9}0}BQcS>Sa#ZDTbfVL!d5-`Y@MOP}-%DAh>D&s3L;V_8 zRLn}i!?7cguaayFn}hJP&A3=)=b|9U zd4NYD5CoVhKekj-yX!g-Z?`5J7K{BIxlpq7KkAJ^u;Tn1PxWE?G;K2@{N;8c>pS$l z$K-fr^KLrZX)`Q4MnjKrOh-SVcnQFhW`@&tgZXy`^Bj7JPKBw<)XX#hTx#E(VNPb( zRnzol`~eROi4VH7ZV2Rx;}srQwCxwz0FwL5Xo{y_kBm>2IMBu5I=+S5cFFvw+UtU& zam8izxN*Hh#Z^4S<676AY3F%8M+hdn7W3X|R;=sz-1$g zkA(y^l9YEnUaFU#&LDKC`PCjw-nG2>ONY0ZFrKd0?dy$s%DIE5nx zrDrvY%HEcxvKV)x)QOuhW=nK)vj6F(v2i_I9ef2>xT9rDuWgkCbQ-LP~-}rdkSh>Q+pBNwt ze>-ys=DsRt@OQL4R+ANP)lVwj+g6sp^eYLhUKY=mX`gav7A4jlmS&@Umc=U{#s&qu zc8s6MdGC&1s+9dqzm&1VB;gc77x!65UTzRIG&GFS(gaU%(U#yBN@@B7DDy5#z?h)A zJelIT{>$CZ;cvT}m7@1$xBZ-f-R_2BmODd9OgsLz=X{O{tTO?fcmJ_*->*TMePIx!r?|UIba@jyf*i=U&-0=T4R^*@u>~7`XcLCu28L3tHR(e z2AHs#d0Hj2XG&Du@O~^m-Ryn{Z2kjh&4!xWvB@&E6{X*G09y9fXwj#L%tC}!%D9y8 zn85jV#OA%feRaTr33NW^LG$|xy~=;R>G61n;BY@&9*Vm ztl5vpr%!4_z5wkBqq1Oq{iW05ru)TN;O>*k^N6|1s@uh=Kqa%2oWi%_>VtQDy$)_G zQnW1JDzOTjpQ;3d{yq$M5U&Lzds9Istu$B*jVc@L05~|&zx~X^a1?1dwG)F-?(36v z$0=+0$=T;Tj<%DX6@CUyrr~V=9JN0Owa=MYeNe}<>sXq%d_Shwiu>h!)zkN1_t~qy1IL#m z-Np5H>BBhVN<75C8jqBDJFR}wHtD6HXtS)#R-I+K3CwvGeS8ec8P%(|nU!3GiL`Ln_AhH( zRo(q09?n7god5fJ4fryfk(W3i-^tMHf%Gv>0?sWUZotC&wU{`3qUa<7kNzwxl0v9(qTuQR)> zYzzpyIJohxPS^u&7lAtuFHDkAnqeSHCjS#%pMvHq_7J$yk`8PDgWdrFfsG~P9O2w= zYc;p58dJ|Xdj-knXZzML8%~k>lZ<)ez$fR->itALn5U2?nmbHPkp%%q;Jw;n8%*&^Y~Bwkii?pJ~= zM|YBQW?po4v#Ur4G;M1=?!<7Cjcj0+VFRYZku7Zz;u2`94adRnAh9Q6E*`nRjvkzQ z&$Qg!3J}HFNkWROGfG)!kl^tAOD9?18+$|b??~jsMDEv1en)jp*mt2VO9TwM5mjal zF2HGQco)FZGX)PxySnm13{W?|X%gEYq+?0h^w*TiA-_+A7rsU!Hke!qLh@5 zP5g?$7mgG+BIhz^*E?Eh%CJk@ab&ymaSUrTF)~6m5uw(8$N;7OkLMYV8bIXBX_i9E zjM}j|aoYq-Dus1rj+hbLw{}59`e>im_*BfxR}R~W*CV6baK!15{@L&vk%b-}j%>*c z$EB|O3U{k(VrKn?Se6<9?ub(r69z8C)Ojdk2_TlWt{pdgNLZz>AH3mx9!~ zmqw^ZWp7DwPO@*C-Ce}BXQPOfri(+5lIhlj7Q0a!DO`z54O~DFX*p~jWYrrBE$OPj z`|(>3urqC@gusEzg$j~p-4D;^!XDICWGq0eUNz;u;?T?TH~-4bv*=zI!Ru-rtYH<= zyLK8o|AHUL57~v#zK`|LP$4R7h(TCxjT*r|M) z=95()S-iYlLv&D!;pGWXpD^PExhv?o+jk`YsH_zb1XlS}jdPTxqoG^!^4?ukkbmed zoUHnzg~9!sSKEVNrpNt$GZfxjmteV4U5H$JcFYJcvRU;9x@vu&qb!)-A_B~X@Eq*l z+AC^oP`mlHKYags*vf?+SZFY`Q=hnl*WK|JYj|ssVVES_)UvPR{=@q-p72RV9+|QJ zk30kJEuy92X_=1S?Vpt|8F2eVRoi>p-gn;ioxB?BG`S{29)CI9Kw(c6rg?Ad6ISg6 z#u~f30OoG==`&ttUhW5r8{djfn7!U*+~95heKJK~_*rd80`%p??jQe&H)>ZgAsE&~C3Y8q04#boz^2yi1LXh4m&3@5X`#S8r2ued#hmoio*6 zrhD)~<)%Hju$X;Vl$x44T7e}H9gr+Jn{VZR+?!;2@8@_$_`*b5o&OUv3Z9B6WvBUm zsbZU~^{Dy|QbJrUGlM$=wm*P}!q|{0_j$CNc`4HZj#xpmElx?8vPGQ+`(L6xH|0PA zz9*rue=K)13cfTER04qd)aYwYc7XKB`Nl?W9sA*Co8;V0VUO@UuDzYkoZsbpn3(99 zrGf7s_hu+u%2SlT^V;3~CcNd_YE0o%oVTPI%1B;KkEXOh$VLABgb-+3F#wD4wH`$> zTzy$2p1i@l0>!3JrtWIhe%O-=JbJ>67Cs7&-iF_1?uOuQfS?5G{* zvgkVFtoTef5fTQ^31m1Y8nwa`QZd&Ry?<*5N?+g(_;I=6G{Y+s;^p>qR@67kW&pH-KHgJe+WD-HpSMghv_dUH&A-$47*E)*1ne zZaJqo@qU3+uJcoO(>3Yrc-$;M;eU)XU~=k6(*<|MD!=}wgTNl=OqwJi z=u8w`4I7r^2SjRbh}n%@%uamMHanq_Sro4ZlRWM3Kkg)8GLm}aqeUBIn*8iCy;$<& zD0ylh_?!!0czDqAgFd2{8B8cMIpskN3`T=P=0Tv=OmyY*$u24bAhP(v6D{_?Ey8EB z>@j&kMHlc4&F<9kO_09%%MJ%{jT)wnw*9p)4FZCYj%`{LHB&5IdbDNysHIO*nP4*q z1ayJ0#3~a7)Mz)l#L%8?_6n(9NI~+ot!8eqk5SJ@Xcn510uzDEYka3MfePK)qJ_7@ zy4;(Hf?J7rp)G?U>?*C$% zHbpJD{2Asw9(HTXL+xEsTnuZuzV?9k0V34&wg zE6@H7_Qj!i{T@g|>6WD7E|jQI)dm>O*ZeO5&#sHm^r9Ubx!Hf)KalUM_4N6{(Q#+P zVL58f;_B+ky~XI>c!C&#tuw&2LevinmPD`YK+xz%$yZ?=0+ePlA6ot)ep z$C1_=ayR^BrXmiA!;$CV@}nxI4qrPdx_S?OOl5<$G5qH;H5#v&cPWCl*9-71Q#8xeng(Gj0_f0F$tN}PAIY74SyR~Rsc z{w$S^bzST9xjOi$+x_+b`Ox_PX9H|IF1#Qb+epgz{NbdA9}WaQN-IlMNSFlwFUzKg A0{{R3 literal 0 HcmV?d00001 diff --git a/tests/testdata/control_images/qgis_server/WMTS_GetTile_Project_3857_0/WMTS_GetTile_Project_3857_0.png b/tests/testdata/control_images/qgis_server/WMTS_GetTile_Project_3857_0/WMTS_GetTile_Project_3857_0.png new file mode 100644 index 0000000000000000000000000000000000000000..eae982b9ecef06958116e14c26b79c4ed11a552e GIT binary patch literal 33108 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 P2#}Xn`BEuq`s05AJ2jNt literal 0 HcmV?d00001 diff --git a/tests/testdata/control_images/qgis_server/WMTS_GetTile_Project_4326_0/WMTS_GetTile_Project_4326_0.png b/tests/testdata/control_images/qgis_server/WMTS_GetTile_Project_4326_0/WMTS_GetTile_Project_4326_0.png new file mode 100644 index 0000000000000000000000000000000000000000..843453c5c133024fb52fea9e8eaf254685e6a5cc GIT binary patch literal 27852 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 WG+UjHQ40Q^3{a3&k*Sh03Hl$%&N-6+ literal 0 HcmV?d00001 diff --git a/tests/testdata/qgis_server/wmts_getcapabilities.txt b/tests/testdata/qgis_server/wmts_getcapabilities.txt new file mode 100644 index 000000000000..4a8ae38ff319 --- /dev/null +++ b/tests/testdata/qgis_server/wmts_getcapabilities.txt @@ -0,0 +1,1373 @@ + +Content-Type: text/xml; charset=utf-8 + + + + OGC WMTS + 1.0.0 + QGIS Server test + + conditions unknown + None + + + QGIS + + Stéphane Brunner + + + + + + + + + + + + + + + + + + + + + + + + + + + + QGIS Server Hello World + QGIS Server Hello World + QGIS Server Hello World + + -174.766573 -69.957838 + 177.930819 84.307876 + + + -19454925.898459 -11055006.822989 + 19807168.136881 19143772.793601 + + + image/png + + EPSG:3857 + + + 0 + 0 + 1 + 0 + 1 + + + 1 + 0 + 2 + 0 + 2 + + + 2 + 0 + 4 + 0 + 4 + + + 3 + 0 + 8 + 0 + 8 + + + 4 + 0 + 16 + 0 + 16 + + + 5 + 0 + 32 + 0 + 32 + + + 6 + 0 + 64 + 0 + 64 + + + 7 + 0 + 128 + 0 + 128 + + + 8 + 0 + 256 + 0 + 256 + + + 9 + 0 + 512 + 0 + 512 + + + 10 + 0 + 1024 + 0 + 1024 + + + 11 + 0 + 2048 + 0 + 2048 + + + 12 + 0 + 4096 + 0 + 4096 + + + 13 + 0 + 8192 + 0 + 8192 + + + 14 + 0 + 16384 + 0 + 16384 + + + 15 + 0 + 32768 + 0 + 32768 + + + 16 + 0 + 65536 + 0 + 65536 + + + 17 + 0 + 131072 + 0 + 131072 + + + 18 + 0 + 262144 + 0 + 262144 + + + 19 + 0 + 524288 + 0 + 524288 + + + 20 + 0 + 1048576 + 0 + 1048576 + + + + + EPSG:4326 + + + 0 + 0 + 2 + 0 + 1 + + + 1 + 0 + 4 + 0 + 2 + + + 2 + 0 + 8 + 0 + 4 + + + 3 + 0 + 16 + 0 + 8 + + + 4 + 0 + 32 + 0 + 16 + + + 5 + 0 + 64 + 0 + 32 + + + 6 + 0 + 128 + 0 + 64 + + + 7 + 0 + 256 + 0 + 128 + + + 8 + 0 + 511 + 0 + 256 + + + 9 + 0 + 1022 + 0 + 511 + + + 10 + 0 + 2044 + 0 + 1022 + + + 11 + 0 + 4089 + 0 + 2044 + + + 12 + 0 + 8177 + 0 + 4089 + + + 13 + 0 + 16354 + 0 + 8177 + + + 14 + 0 + 32709 + 0 + 16354 + + + 15 + 0 + 65418 + 0 + 32709 + + + 16 + 0 + 130836 + 0 + 65418 + + + 17 + 0 + 261672 + 0 + 130836 + + + 18 + 0 + 523344 + 0 + 261672 + + + 19 + 0 + 1046687 + 0 + 523344 + + + + + + CountryGroup + CountryGroup + + -176.248495 -67.592996 + 179.412741 83.621086 + + + -19619892.68012 -10327100.342322 + 19972134.918542 18415866.312934 + + + image/png + text/plain + text/html + text/xml + application/vnd.ogc.gml + application/vnd.ogc.gml/3.1.1 + + EPSG:3857 + + + 0 + 0 + 1 + 0 + 1 + + + 1 + 0 + 2 + 0 + 2 + + + 2 + 0 + 4 + 0 + 4 + + + 3 + 0 + 8 + 0 + 8 + + + 4 + 0 + 16 + 0 + 16 + + + 5 + 0 + 32 + 0 + 32 + + + 6 + 0 + 64 + 0 + 64 + + + 7 + 0 + 128 + 0 + 128 + + + 8 + 0 + 256 + 0 + 256 + + + 9 + 0 + 512 + 0 + 512 + + + 10 + 0 + 1024 + 0 + 1024 + + + 11 + 0 + 2048 + 0 + 2048 + + + 12 + 0 + 4096 + 0 + 4096 + + + 13 + 0 + 8192 + 0 + 8192 + + + 14 + 0 + 16384 + 0 + 16384 + + + 15 + 0 + 32768 + 0 + 32768 + + + 16 + 0 + 65536 + 0 + 65536 + + + 17 + 0 + 131072 + 0 + 131072 + + + 18 + 0 + 262144 + 0 + 262144 + + + 19 + 0 + 524288 + 0 + 524288 + + + 20 + 0 + 1048576 + 0 + 1048576 + + + + + EPSG:4326 + + + 0 + 0 + 2 + 0 + 1 + + + 1 + 0 + 4 + 0 + 2 + + + 2 + 0 + 8 + 0 + 4 + + + 3 + 0 + 16 + 0 + 8 + + + 4 + 0 + 32 + 0 + 16 + + + 5 + 0 + 64 + 0 + 32 + + + 6 + 0 + 128 + 0 + 64 + + + 7 + 0 + 256 + 0 + 128 + + + 8 + 0 + 511 + 0 + 256 + + + 9 + 0 + 1022 + 0 + 511 + + + 10 + 0 + 2044 + 0 + 1022 + + + 11 + 0 + 4089 + 0 + 2044 + + + 12 + 0 + 8177 + 0 + 4089 + + + 13 + 0 + 16354 + 0 + 8177 + + + 14 + 0 + 32709 + 0 + 16354 + + + 15 + 0 + 65418 + 0 + 32709 + + + 16 + 0 + 130836 + 0 + 65418 + + + 17 + 0 + 261672 + 0 + 130836 + + + 18 + 0 + 523344 + 0 + 261672 + + + 19 + 0 + 1046687 + 0 + 523344 + + + + + + Hello + + -132.467818 -1.006739 + 101.888717 69.520496 + + + -14746250.075131 -112075.428077 + 11342200.077197 10914413.714128 + + + image/png + text/plain + text/html + text/xml + application/vnd.ogc.gml + application/vnd.ogc.gml/3.1.1 + + EPSG:3857 + + + 0 + 0 + 1 + 0 + 1 + + + 1 + 0 + 2 + 0 + 2 + + + 2 + 0 + 4 + 0 + 4 + + + 3 + 0 + 8 + 0 + 8 + + + 4 + 0 + 16 + 0 + 16 + + + 5 + 0 + 32 + 0 + 32 + + + 6 + 0 + 64 + 0 + 64 + + + 7 + 0 + 128 + 0 + 128 + + + 8 + 0 + 256 + 0 + 256 + + + 9 + 0 + 512 + 0 + 512 + + + 10 + 0 + 1024 + 0 + 1024 + + + 11 + 0 + 2048 + 0 + 2048 + + + 12 + 0 + 4096 + 0 + 4096 + + + 13 + 0 + 8192 + 0 + 8192 + + + 14 + 0 + 16384 + 0 + 16384 + + + 15 + 0 + 32768 + 0 + 32768 + + + 16 + 0 + 65536 + 0 + 65536 + + + 17 + 0 + 131072 + 0 + 131072 + + + 18 + 0 + 262144 + 0 + 262144 + + + 19 + 0 + 524288 + 0 + 524288 + + + 20 + 0 + 1048576 + 0 + 1048576 + + + + + EPSG:4326 + + + 0 + 0 + 2 + 0 + 1 + + + 1 + 0 + 4 + 0 + 2 + + + 2 + 0 + 8 + 0 + 4 + + + 3 + 0 + 16 + 0 + 8 + + + 4 + 0 + 32 + 0 + 16 + + + 5 + 0 + 64 + 0 + 32 + + + 6 + 0 + 128 + 0 + 64 + + + 7 + 0 + 256 + 0 + 128 + + + 8 + 0 + 511 + 0 + 256 + + + 9 + 0 + 1022 + 0 + 511 + + + 10 + 0 + 2044 + 0 + 1022 + + + 11 + 0 + 4089 + 0 + 2044 + + + 12 + 0 + 8177 + 0 + 4089 + + + 13 + 0 + 16354 + 0 + 8177 + + + 14 + 0 + 32709 + 0 + 16354 + + + 15 + 0 + 65418 + 0 + 32709 + + + 16 + 0 + 130836 + 0 + 65418 + + + 17 + 0 + 261672 + 0 + 130836 + + + 18 + 0 + 523344 + 0 + 261672 + + + 19 + 0 + 1046687 + 0 + 523344 + + + + + + EPSG:3857 + EPSG:3857 + + 0 + 559082264.028718 + -20037508.342789 20037508.342789 + 256 + 256 + 1 + 1 + + + 1 + 279541132.014359 + -20037508.342789 20037508.342789 + 256 + 256 + 2 + 2 + + + 2 + 139770566.007179 + -20037508.342789 20037508.342789 + 256 + 256 + 4 + 4 + + + 3 + 69885283.00359 + -20037508.342789 20037508.342789 + 256 + 256 + 8 + 8 + + + 4 + 34942641.501795 + -20037508.342789 20037508.342789 + 256 + 256 + 16 + 16 + + + 5 + 17471320.750897 + -20037508.342789 20037508.342789 + 256 + 256 + 32 + 32 + + + 6 + 8735660.375449 + -20037508.342789 20037508.342789 + 256 + 256 + 64 + 64 + + + 7 + 4367830.187724 + -20037508.342789 20037508.342789 + 256 + 256 + 128 + 128 + + + 8 + 2183915.093862 + -20037508.342789 20037508.342789 + 256 + 256 + 256 + 256 + + + 9 + 1091957.546931 + -20037508.342789 20037508.342789 + 256 + 256 + 512 + 512 + + + 10 + 545978.773466 + -20037508.342789 20037508.342789 + 256 + 256 + 1024 + 1024 + + + 11 + 272989.386733 + -20037508.342789 20037508.342789 + 256 + 256 + 2048 + 2048 + + + 12 + 136494.693366 + -20037508.342789 20037508.342789 + 256 + 256 + 4096 + 4096 + + + 13 + 68247.346683 + -20037508.342789 20037508.342789 + 256 + 256 + 8192 + 8192 + + + 14 + 34123.673342 + -20037508.342789 20037508.342789 + 256 + 256 + 16384 + 16384 + + + 15 + 17061.836671 + -20037508.342789 20037508.342789 + 256 + 256 + 32768 + 32768 + + + 16 + 8530.918335 + -20037508.342789 20037508.342789 + 256 + 256 + 65536 + 65536 + + + 17 + 4265.459168 + -20037508.342789 20037508.342789 + 256 + 256 + 131072 + 131072 + + + 18 + 2132.729584 + -20037508.342789 20037508.342789 + 256 + 256 + 262144 + 262144 + + + 19 + 1066.364792 + -20037508.342789 20037508.342789 + 256 + 256 + 524288 + 524288 + + + 20 + 533.182396 + -20037508.342789 20037508.342789 + 256 + 256 + 1048576 + 1048576 + + + + EPSG:4326 + EPSG:4326 + + 0 + 279541132.014359 + -180 90 + 256 + 256 + 2 + 1 + + + 1 + 139770566.007179 + -180 90 + 256 + 256 + 4 + 2 + + + 2 + 69885283.00359 + -180 90 + 256 + 256 + 8 + 4 + + + 3 + 34942641.501795 + -180 90 + 256 + 256 + 16 + 8 + + + 4 + 17471320.750897 + -180 90 + 256 + 256 + 32 + 16 + + + 5 + 8735660.375449 + -180 90 + 256 + 256 + 64 + 32 + + + 6 + 4367830.187724 + -180 90 + 256 + 256 + 128 + 64 + + + 7 + 2183915.093862 + -180 90 + 256 + 256 + 256 + 128 + + + 8 + 1091957.546931 + -179.972618 90 + 256 + 256 + 511 + 256 + + + 9 + 545978.773466 + -179.972618 89.986309 + 256 + 256 + 1022 + 511 + + + 10 + 272989.386733 + -179.972618 89.986309 + 256 + 256 + 2044 + 1022 + + + 11 + 136494.693366 + -180 89.986309 + 256 + 256 + 4089 + 2044 + + + 12 + 68247.346683 + -179.99463 90 + 256 + 256 + 8177 + 4089 + + + 13 + 34123.673342 + -179.99463 89.997315 + 256 + 256 + 16354 + 8177 + + + 14 + 17061.836671 + -180 89.997315 + 256 + 256 + 32709 + 16354 + + + 15 + 8530.918335 + -180 90 + 256 + 256 + 65418 + 32709 + + + 16 + 4265.459168 + -180 90 + 256 + 256 + 130836 + 65418 + + + 17 + 2132.729584 + -180 90 + 256 + 256 + 261672 + 130836 + + + 18 + 1066.364792 + -180 90 + 256 + 256 + 523344 + 261672 + + + 19 + 533.182396 + -179.999961 90 + 256 + 256 + 1046687 + 523344 + + + + diff --git a/tests/testdata/qgis_server_accesscontrol/project_groups.qgs b/tests/testdata/qgis_server_accesscontrol/project_groups.qgs index bd1f6ee625de..ef083697518d 100644 --- a/tests/testdata/qgis_server_accesscontrol/project_groups.qgs +++ b/tests/testdata/qgis_server_accesscontrol/project_groups.qgs @@ -1,67 +1,51 @@ - + QGIS Server Hello World - - - + + + - +proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs - 3857 - 3857 - EPSG:3857 - WGS 84 / Pseudo-Mercator + +proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs + 100086 + 0 + USER:100086 + * Generated CRS (+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs) merc - WGS84 + false - + - + - + - + - + - + - - - - - - - - - - - - - - - - - + - + - + @@ -75,132 +59,108 @@ country20131022151106556 Country_copy20161127151800736 country20170328164317226 - Country_07a7a712_8bf3_4cb0_abde_58338f73a4b2 - Country_Labels_67649087_4e95_4483_9c32_a9e14b8360db - Country_Diagrams_208edc6e_7828_426b_a7d8_c2383f10761d - + - - - - - - - - - - - + + + + + + + + meters - -19299402.19027414172887802 - -18237547.54576336964964867 - 19536700.87085317820310593 - 39423933.74873916804790497 + -30425236.72921397164463997 + -10846058.0813884325325489 + 29667752.81264735385775566 + 14979028.33329574391245842 0 - +proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs - 3857 - 3857 - EPSG:3857 - WGS 84 / Pseudo-Mercator + +proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs + 100086 + 0 + USER:100086 + * Generated CRS (+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs) merc - WGS84 + false 0 - - - - + -19619892.68012013286352158 -10327100.34232237376272678 19972134.91854240000247955 18415866.31293442100286484 - Country_07a7a712_8bf3_4cb0_abde_58338f73a4b2 + Country_copy20161127151800736 dbname='./helloworld.db' table="country" (geom) sql= - Country copier + Country_Labels +proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs @@ -232,7 +192,7 @@ - false + true @@ -243,56 +203,54 @@ - - - - + + + - - + + - - + + - + - - - - + + + + + + + + + + + + + + + + + - + - + - - - - - - - - - - - - - - - - - + + + @@ -305,10 +263,13 @@ + + + @@ -318,29 +279,35 @@ - - + + + + + - + - + + + - + - + + @@ -351,12 +318,16 @@ - + + + + + @@ -364,6 +335,7 @@ + @@ -380,11 +352,16 @@ + + + + + @@ -397,6 +374,8 @@ + + @@ -406,11 +385,16 @@ + + + + + @@ -419,323 +403,123 @@ + + + - - - + + + + + + + + 0 0 0 - name - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + - - - 0 - tablayout + + . + + + + + + + + + + + + . + + 0 + + + 0 + generatedlayout - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 0 - 0 - 0 - name - - - - - - - - - - - 0 - tablayout - - - - - - + + + + + + + name + + 2 + - + - + @@ -749,13 +533,13 @@ - + @@ -769,9 +553,9 @@ @@ -782,40 +566,44 @@ - - - - + + + + - - - + + + - + + + + + 0 0 1 - - + + - + - + @@ -836,38 +624,58 @@ - - + + - - + + - - + + - - + + - - + + + ../../../../../.. 0 - + 0 generatedlayout - + @@ -877,22 +685,22 @@ - "name" + name - + - -19619892.68012013286352158 - -10327100.34232237376272678 - 19972134.91854240000247955 - 18415866.31293442100286484 + -14746250.07513097859919071 + -112075.42807669920148328 + 11342200.07719692215323448 + 10914413.7141284141689539 - Country_Diagrams_208edc6e_7828_426b_a7d8_c2383f10761d - dbname='./helloworld.db' table="country" (geom) sql= + Hello_Project_SubsetString_copy20160223113949592 + dbname='./helloworld.db' table="hello" (geom) sql= - Country_Diagrams copier + Hello_Filter_SubsetString +proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs @@ -935,16 +743,36 @@ - - + + - + - + - + + + + + + + + + + + + + + + + + @@ -956,9 +784,9 @@ @@ -968,74 +796,84 @@ - - - + + 0 0 1 - - - - - + + + + - + - + - - + + - - - - + + + + + + + + + + + - - + + + - - + + + - - + + + - + - - . + ../../../../../.. 0 @@ -1050,7 +888,7 @@ Enter the name of the function in the "Python Init function" field. An example follows: """ -from qgis.PyQt.QtWidgets import QWidget +from PyQt4.QtGui import QWidget def my_form_open(dialog, layer, feature): geom = feature.geometry() @@ -1058,6 +896,16 @@ def my_form_open(dialog, layer, feature): ]]> 0 generatedlayout + + + + + + + + + + @@ -1066,22 +914,22 @@ def my_form_open(dialog, layer, feature): - name + "pkuid" - + - -19619892.68012013286352158 - -10327100.34232237376272678 - 19972134.91854240000247955 - 18415866.31293442100286484 + -2465695.66895584994927049 + 80258.53580146089370828 + 5037064.00943838991224766 + 3762589.19456820981577039 - Country_Labels_67649087_4e95_4483_9c32_a9e14b8360db - dbname='./helloworld.db' table="country" (geom) sql= + Hello_SubsetString_copy20160222085231770 + dbname='./helloworld.db' table="hello" (geom) sql="pkuid" in ( 7, 8 ) - Country_Labels copier + Hello_Project_SubsetString +proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs @@ -1124,345 +972,36 @@ def my_form_open(dialog, layer, feature): - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 0 - 0 - 0 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - . - - - - - - - - - - - - - - . - - 0 - - - 0 - generatedlayout - - - - - - - - - - name - - 2 - - - + + - + - + - + - + - + - + @@ -1474,9 +1013,9 @@ def my_form_open(dialog, layer, feature): @@ -1485,46 +1024,25 @@ def my_form_open(dialog, layer, feature): - - - - - - - - - - - - - - - - - - - + + 0 0 1 - - + + - + - + @@ -1536,7 +1054,14 @@ def my_form_open(dialog, layer, feature): - + + + + + + + - - + + + - - + + + - - + + + - - + + + - - - + + ../../../../../.. @@ -1587,7 +1112,7 @@ Enter the name of the function in the "Python Init function" field. An example follows: """ -from qgis.PyQt.QtWidgets import QWidget +from PyQt4.QtGui import QWidget def my_form_open(dialog, layer, feature): geom = feature.geometry() @@ -1596,7 +1121,14 @@ def my_form_open(dialog, layer, feature): 0 generatedlayout - + + + + + + + + @@ -1606,22 +1138,22 @@ def my_form_open(dialog, layer, feature): - name + "pkuid" - + - -19619892.68012013286352158 - -10327100.34232237376272678 - 19972134.91854240000247955 - 18415866.31293442100286484 + -14746250.07513097859919071 + -112075.42807669920148328 + 11342200.07719692215323448 + 10914413.7141284141689539 - Country_copy20161127151800736 - dbname='./helloworld.db' table="country" (geom) sql= + Hello_copy20150804164427541 + dbname='./helloworld.db' table="hello" (geom) sql= - Country_Labels + Hello_SubsetString +proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs @@ -1664,345 +1196,36 @@ def my_form_open(dialog, layer, feature): - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 0 - 0 - 0 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - . - - - - - - - - - - - - - - . - - 0 - - - 0 - generatedlayout - - - - - - - - - - name - - 2 - - - + + - + - + - + - + - + - + @@ -2014,9 +1237,9 @@ def my_form_open(dialog, layer, feature): @@ -2025,46 +1248,25 @@ def my_form_open(dialog, layer, feature): - - - - - - - - - - - - - - - - - - - + + 0 0 1 - - + + - + - + @@ -2076,7 +1278,14 @@ def my_form_open(dialog, layer, feature): - + + + + + + + - - + + + - - + + + - - + + + - - + + + - - - + + ../../../../../.. @@ -2127,7 +1336,7 @@ Enter the name of the function in the "Python Init function" field. An example follows: """ -from qgis.PyQt.QtWidgets import QWidget +from PyQt4.QtGui import QWidget def my_form_open(dialog, layer, feature): geom = feature.geometry() @@ -2136,7 +1345,14 @@ def my_form_open(dialog, layer, feature): 0 generatedlayout - + + + + + + + + @@ -2146,22 +1362,22 @@ def my_form_open(dialog, layer, feature): - name + "pkuid" - + - -14746250.07513097859919071 - -112075.42807669920148328 - 11342200.07719692215323448 - 10914413.7141284141689539 + -19619892.68012013286352158 + -10327100.34232237376272678 + 19972134.91854240000247955 + 18415866.31293442100286484 - Hello_Project_SubsetString_copy20160223113949592 - dbname='./helloworld.db' table="hello" (geom) sql= + country20131022151106556 + dbname='./helloworld.db' table="country" (geom) sql= - Hello_Filter_SubsetString + Country +proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs @@ -2204,699 +1420,22 @@ def my_form_open(dialog, layer, feature): - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 0 - 0 - 1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ../../../../../.. - - 0 - - - 0 - generatedlayout - - - - - - - - - - - - - - - - - - - "pkuid" - - - - - -2465695.66895584994927049 - 80258.53580146089370828 - 5037064.00943838991224766 - 3762589.19456820981577039 - - Hello_SubsetString_copy20160222085231770 - dbname='./helloworld.db' table="hello" (geom) sql="pkuid" in ( 7, 8 ) - - - - Hello_Project_SubsetString - - - +proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs - 3857 - 3857 - EPSG:3857 - WGS 84 / Pseudo-Mercator - merc - WGS84 - false - - - - - - - - - - - - - - - - 0 - 0 - - - - - false - - - - - spatialite - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 0 - 0 - 1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ../../../../../.. - - 0 - - - 0 - generatedlayout - - - - - - - - - - - - - - - - - - - "pkuid" - - - - - -14746250.07513097859919071 - -112075.42807669920148328 - 11342200.07719692215323448 - 10914413.7141284141689539 - - Hello_copy20150804164427541 - dbname='./helloworld.db' table="hello" (geom) sql= - - - - Hello_SubsetString - - - +proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs - 3857 - 3857 - EPSG:3857 - WGS 84 / Pseudo-Mercator - merc - WGS84 - false - - - - - - - - - - - - - - - - 0 - 0 - - - - - false - - - - - spatialite - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 0 - 0 - 1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ../../../../../.. - - 0 - - - 0 - generatedlayout - - - - - - - - - - - - - - - - - - - "pkuid" - - - - - -19619892.68012013286352158 - -10327100.34232237376272678 - 19972134.91854240000247955 - 18415866.31293442100286484 - - country20131022151106556 - dbname='./helloworld.db' table="country" (geom) sql= - - - - Country - - - +proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs - 3857 - 3857 - EPSG:3857 - WGS 84 / Pseudo-Mercator - merc - WGS84 - false - - - - - - - - - - - - - - - - 0 - 0 - - - - - false - - - - - spatialite - - - - - - - + + - + - - + + - - + + - - + + @@ -2908,7 +1447,7 @@ def my_form_open(dialog, layer, feature): - + @@ -2921,7 +1460,7 @@ def my_form_open(dialog, layer, feature): - + @@ -3086,24 +1625,24 @@ def my_form_open(dialog, layer, feature): - + - + - + - - + + - + 0 tablayout @@ -3116,19 +1655,19 @@ def my_form_open(dialog, layer, feature): - + - - + + - - + + - - + + @@ -3140,7 +1679,7 @@ def my_form_open(dialog, layer, feature): - + @@ -3149,7 +1688,7 @@ def my_form_open(dialog, layer, feature): - + @@ -3161,7 +1700,7 @@ def my_form_open(dialog, layer, feature): - + @@ -3174,7 +1713,7 @@ def my_form_open(dialog, layer, feature): - + @@ -3339,24 +1878,24 @@ def my_form_open(dialog, layer, feature): - + - + - + - - + + - + 0 tablayout @@ -3370,10 +1909,10 @@ def my_form_open(dialog, layer, feature): - + - + @@ -3387,13 +1926,13 @@ def my_form_open(dialog, layer, feature): - + @@ -3407,9 +1946,9 @@ def my_form_open(dialog, layer, feature): @@ -3420,20 +1959,20 @@ def my_form_open(dialog, layer, feature): - - - - + + + + - - - + + + @@ -3442,18 +1981,18 @@ def my_form_open(dialog, layer, feature): 0 0 1 - - + + - + - + @@ -3474,27 +2013,27 @@ def my_form_open(dialog, layer, feature): - - + + - - + + - - + + - - + + - + ../../../../../.. @@ -3505,7 +2044,7 @@ def my_form_open(dialog, layer, feature): 0 generatedlayout - + @@ -3518,7 +2057,7 @@ def my_form_open(dialog, layer, feature): "name" - + -19619892.68012013286352158 -10327100.34232237376272678 @@ -3573,14 +2112,14 @@ def my_form_open(dialog, layer, feature): - - + + - + - + @@ -3594,9 +2133,9 @@ def my_form_open(dialog, layer, feature): @@ -3613,19 +2152,19 @@ def my_form_open(dialog, layer, feature): 0 0 1 - - + + - - + + - + @@ -3646,34 +2185,34 @@ def my_form_open(dialog, layer, feature): - - + + - - + + - - + + - - + + - + - - . + 0 @@ -3707,7 +2246,7 @@ def my_form_open(dialog, layer, feature): name - + -29.99999999999666755 29.99999999999666755 @@ -3763,11 +2302,11 @@ def my_form_open(dialog, layer, feature): - - + + - + None @@ -3784,12 +2323,12 @@ def my_form_open(dialog, layer, feature): - + 0 - + -14746250.07513097859919071 -112075.42807669920148328 @@ -3844,14 +2383,14 @@ def my_form_open(dialog, layer, feature): - - + + - + - + @@ -3865,13 +2404,13 @@ def my_form_open(dialog, layer, feature): - + @@ -3885,9 +2424,9 @@ def my_form_open(dialog, layer, feature): @@ -3900,18 +2439,18 @@ def my_form_open(dialog, layer, feature): 0 0 1 - - + + - + - + @@ -3939,31 +2478,31 @@ def my_form_open(dialog, layer, feature): - - - + + + - - - + + + - - - + + + - - - + + + - + ../../../../../.. @@ -3974,12 +2513,12 @@ def my_form_open(dialog, layer, feature): 0 generatedlayout - - - + + + - - + + @@ -3994,7 +2533,7 @@ def my_form_open(dialog, layer, feature): COALESCE( "pkuid", '<NULL>' ) - + 1000 2000 @@ -4049,14 +2588,14 @@ def my_form_open(dialog, layer, feature): - - + + - + - + @@ -4077,9 +2616,9 @@ def my_form_open(dialog, layer, feature): @@ -4092,18 +2631,18 @@ def my_form_open(dialog, layer, feature): 0 0 1 - - + + - + - + @@ -4131,31 +2670,31 @@ def my_form_open(dialog, layer, feature): - - - + + + - - - + + + - - - + + + - - - + + + - + ../../../../../.. @@ -4187,14 +2726,80 @@ def my_form_open(dialog, layer, feature): - - - - - - + + true + + CountryGroup + + + hello20131022151106574 + + + + 50 + false + 0 + false + true + 30 + 16 + false + true + + + + 255 + + + true + + + 1 + + true + + 1 + 0 + 1 + 1 + 1 + + + false + + + 1 + +proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs + EPSG:3857 + 3857 + + + 255 + 255 + 255 + 246 + 108 + 255 + 128 + + conditions unknown + + dem20150730091219559 + + false + true + + true + + CountryGroup + + + hello20131022151106574 + + + Hello_SubsetString_copy20160222085231770 Hello_copy20150804164427541 @@ -4202,166 +2807,120 @@ def my_form_open(dialog, layer, feature): hello20131022151106574 points20150803121107046 + + + false + + + + + + + + + + meters + m2 + + + Stéphane Brunner + Simple test app. + + D + true + 2 + -20609693.37008669599890709 -11055006.82298868149518967 20961935.60850896313786507 19143772.79360072687268257 - false - - - - - - - true - + + 2000 + 1 + + + 1 + days + 1 + 0 + 0 + 0 + - + points20150803121107046 - + points20150803121107046 - + points20150803121107046 - + - - - - - 0 - false - false - false - 50 - true - true - 16 - 30 - false - - - false - 5000 - - 0 - 1 - 1 - 1 - 1 - - - - meters - m2 - - Stéphane Brunner + + EPSG:3857 + EPSG:4326 + 5000 - - - true - 255 - - 1 - - - - QGIS Server test - - Simple test app. - true - - - - - - false - + None - advanced - - enabled - enabled - - 40 - to vertex - - 40.000000 - 40.000000 - - - to_vertex - to_vertex - + + 1 1 1 - - 1 + to vertex country20131022151106556 hello20131022151106574 + + to_vertex + to_vertex + + + 40.000000 + 40.000000 + + 40 + + enabled + enabled + + advanced - - dem20150730091219559 - - + 5 + + false false - + + + + + + + + + + + + + 5000 90 - - - 108 - 128 - 255 - 246 - 255 - 255 - 255 - - 5 - - EPSG:3857 - EPSG:4326 - - None - conditions unknown - - - 2 - D - true - + + + + QGIS + QGIS Server test WGS84 - QGIS - - days - 0 - 2000 - 0 - 1 - 0 - 1 - - 1 - - - - EPSG:3857 - 3857 - 1 - +proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs - @@ -4387,12 +2946,12 @@ def my_form_open(dialog, layer, feature): - - - + + + - + @@ -4406,22 +2965,22 @@ def my_form_open(dialog, layer, feature): - - - + + + @@ -4429,51 +2988,51 @@ def my_form_open(dialog, layer, feature): - - - + + + - + - - - + + + - + - points20150803121107046 - hello20131022151106574 - Hello_copy20150804164427541 - Hello_SubsetString_copy20160222085231770 - Hello_Project_SubsetString_copy20160223113949592 - dem20150730091219559 - country20131022151106556 - Country_copy20161127151800736 - country20170328164317226 + points20150803121107046 + hello20131022151106574 + Hello_copy20150804164427541 + Hello_SubsetString_copy20160222085231770 + Hello_Project_SubsetString_copy20160223113949592 + dem20150730091219559 + country20131022151106556 + Country_copy20161127151800736 + country20170328164317226 - + - + @@ -4491,9 +3050,9 @@ def my_form_open(dialog, layer, feature): @@ -4501,7 +3060,7 @@ def my_form_open(dialog, layer, feature): - + @@ -4522,9 +3081,9 @@ def my_form_open(dialog, layer, feature): @@ -4532,49 +3091,49 @@ def my_form_open(dialog, layer, feature): - + - - - + + + - + - - - + + + - + - + - - - + + + - + @@ -4588,22 +3147,22 @@ def my_form_open(dialog, layer, feature): - - - + + + @@ -4611,21 +3170,21 @@ def my_form_open(dialog, layer, feature): - - - + + + - - - - + + + - + - - - + + + - + - points20150803121107046 - hello20131022151106574 - Hello_copy20150804164427541 - Hello_SubsetString_copy20160222085231770 - Hello_Project_SubsetString_copy20160223113949592 - dem20150730091219559 - country20131022151106556 - Country_copy20161127151800736 - country20170328164317226 + points20150803121107046 + hello20131022151106574 + Hello_copy20150804164427541 + Hello_SubsetString_copy20160222085231770 + Hello_Project_SubsetString_copy20160223113949592 + dem20150730091219559 + country20131022151106556 + Country_copy20161127151800736 + country20170328164317226 - + - + @@ -4704,9 +3263,9 @@ def my_form_open(dialog, layer, feature): @@ -4714,7 +3273,7 @@ def my_form_open(dialog, layer, feature): - + @@ -4735,9 +3294,9 @@ def my_form_open(dialog, layer, feature): @@ -4745,42 +3304,42 @@ def my_form_open(dialog, layer, feature): - + - - - + + + - + - - - + + + - + - + From 059232a8c4406d71a26d2d0375256afdf2ab4c0c Mon Sep 17 00:00:00 2001 From: rldhont Date: Wed, 1 Aug 2018 16:16:07 +0200 Subject: [PATCH 11/33] [Server][Feature][needs-docs] Server Cache can be used for images (tiles) Extending cache manager to save adn retrieve images. --- src/server/qgsservercachefilter.cpp | 35 +++++++++++++++++ src/server/qgsservercachefilter.h | 37 +++++++++++++++++- src/server/qgsservercachemanager.cpp | 57 ++++++++++++++++++++++++++++ src/server/qgsservercachemanager.h | 35 +++++++++++++++++ 4 files changed, 163 insertions(+), 1 deletion(-) diff --git a/src/server/qgsservercachefilter.cpp b/src/server/qgsservercachefilter.cpp index 63ff9368a0d3..cb568ea86d9e 100644 --- a/src/server/qgsservercachefilter.cpp +++ b/src/server/qgsservercachefilter.cpp @@ -61,3 +61,38 @@ bool QgsServerCacheFilter::deleteCachedDocuments( const QgsProject *project ) co Q_UNUSED( project ); return false; } + +//! Returns cached image +QByteArray QgsServerCacheFilter::getCachedImage( const QgsProject *project, const QgsServerRequest &request, const QString &key ) const +{ + Q_UNUSED( project ); + Q_UNUSED( request ); + Q_UNUSED( key ); + return QByteArray(); +} + +//! Updates or inserts the image in cache +bool QgsServerCacheFilter::setCachedImage( const QByteArray *img, const QgsProject *project, const QgsServerRequest &request, const QString &key ) const +{ + Q_UNUSED( img ); + Q_UNUSED( project ); + Q_UNUSED( request ); + Q_UNUSED( key ); + return false; +} + +//! Deletes the cached image +bool QgsServerCacheFilter::deleteCachedImage( const QgsProject *project, const QgsServerRequest &request, const QString &key ) const +{ + Q_UNUSED( project ); + Q_UNUSED( request ); + Q_UNUSED( key ); + return false; +} + +//! Deletes all cached images for a QGIS project +bool QgsServerCacheFilter::deleteCachedImages( const QgsProject *project ) const +{ + Q_UNUSED( project ); + return false; +} diff --git a/src/server/qgsservercachefilter.h b/src/server/qgsservercachefilter.h index 6665fa7cfa91..99e6dab65f1c 100644 --- a/src/server/qgsservercachefilter.h +++ b/src/server/qgsservercachefilter.h @@ -82,11 +82,46 @@ class SERVER_EXPORT QgsServerCacheFilter /** * Deletes all cached documents for a QGIS project - * \param project the project used to generate the document to provide path + * \param project the project used to generate the documents to provide path * \returns true if the documents have been deleted */ virtual bool deleteCachedDocuments( const QgsProject *project ) const; + /** + * Returns cached image (or 0 if document not in cache) like tiles + * \param project the project used to generate the image to provide path + * \param request the request used to generate the image to provider parameters or data + * \param key the key provided by the access control to identify differents images for the same request + * \returns QByteArray of the cached image or an empty one if no corresponding image found + */ + virtual QByteArray getCachedImage( const QgsProject *project, const QgsServerRequest &request, const QString &key ) const; + + /** + * Updates or inserts the image in cache like tiles + * \param img the document to cache + * \param project the project used to generate the image to provide path + * \param request the request used to generate the image to provider parameters or data + * \param key the key provided by the access control to identify differents images for the same request + * \returns true if the image has been cached + */ + virtual bool setCachedImage( const QByteArray *img, const QgsProject *project, const QgsServerRequest &request, const QString &key ) const; + + /** + * Deletes the cached image + * \param project the project used to generate the image to provide path + * \param request the request used to generate the image to provider parameters or data + * \param key the key provided by the access control to identify differents images for the same request + * \returns true if the image has been deleted + */ + virtual bool deleteCachedImage( const QgsProject *project, const QgsServerRequest &request, const QString &key ) const; + + /** + * Deletes all cached images for a QGIS project + * \param project the project used to generate the images to provide path + * \returns true if the images have been deleted + */ + virtual bool deleteCachedImages( const QgsProject *project ) const; + private: //! The server interface diff --git a/src/server/qgsservercachemanager.cpp b/src/server/qgsservercachemanager.cpp index b18cdb92aaf0..16f77995afa3 100644 --- a/src/server/qgsservercachemanager.cpp +++ b/src/server/qgsservercachemanager.cpp @@ -75,6 +75,63 @@ bool QgsServerCacheManager::deleteCachedDocuments( const QgsProject *project ) c return false; } +//! Returns cached image (or 0 if image not in cache) like tiles +QByteArray QgsServerCacheManager::getCachedImage( const QgsProject *project, const QgsServerRequest &request, const QString &key ) const +{ + QgsServerCacheFilterMap::const_iterator scIterator; + for ( scIterator = mPluginsServerCaches->constBegin(); scIterator != mPluginsServerCaches->constEnd(); ++scIterator ) + { + QByteArray content = scIterator.value()->getCachedImage( project, request, key ); + if ( !content.isEmpty() ) + { + return content; + } + } + return QByteArray(); +} + +//! Updates or inserts the image in cache like tiles +bool QgsServerCacheManager::setCachedImage( const QByteArray *img, const QgsProject *project, const QgsServerRequest &request, const QString &key ) const +{ + QgsServerCacheFilterMap::const_iterator scIterator; + for ( scIterator = mPluginsServerCaches->constBegin(); scIterator != mPluginsServerCaches->constEnd(); ++scIterator ) + { + if ( scIterator.value()->setCachedImage( img, project, request, key ) ) + { + return true; + } + } + return false; +} + +//! Deletes the cached image +bool QgsServerCacheManager::deleteCachedImage( const QgsProject *project, const QgsServerRequest &request, const QString &key ) const +{ + QgsServerCacheFilterMap::const_iterator scIterator; + for ( scIterator = mPluginsServerCaches->constBegin(); scIterator != mPluginsServerCaches->constEnd(); ++scIterator ) + { + if ( scIterator.value()->deleteCachedImage( project, request, key ) ) + { + return true; + } + } + return false; +} + +//! Deletes all cached images for a QGIS Project +bool QgsServerCacheManager::deleteCachedImages( const QgsProject *project ) const +{ + QgsServerCacheFilterMap::const_iterator scIterator; + for ( scIterator = mPluginsServerCaches->constBegin(); scIterator != mPluginsServerCaches->constEnd(); ++scIterator ) + { + if ( scIterator.value()->deleteCachedImages( project ) ) + { + return true; + } + } + return false; +} + //! Register a new access control filter void QgsServerCacheManager::registerServerCache( QgsServerCacheFilter *serverCache, int priority ) { diff --git a/src/server/qgsservercachemanager.h b/src/server/qgsservercachemanager.h index 3ccfc8566dc2..e38c0159e3b5 100644 --- a/src/server/qgsservercachemanager.h +++ b/src/server/qgsservercachemanager.h @@ -101,6 +101,41 @@ class SERVER_EXPORT QgsServerCacheManager */ bool deleteCachedDocuments( const QgsProject *project ) const; + /** + * Returns cached image (or 0 if image not in cache) like tiles + * \param project the project used to generate the image to provide path + * \param request the request used to generate the image to provider parameters or data + * \param key the key provided by the access control to identify differents images for the same request + * \returns the cached image or 0 if no corresponding image found + */ + QByteArray getCachedImage( const QgsProject *project, const QgsServerRequest &request, const QString &key ) const; + + /** + * Updates or inserts the image in cache like tiles + * \param img the image to cache + * \param project the project used to generate the image to provide path + * \param request the request used to generate the image to provider parameters or data + * \param key the key provided by the access control to identify differents images for the same request + * \returns true if the image has been cached + */ + bool setCachedImage( const QByteArray *img, const QgsProject *project, const QgsServerRequest &request, const QString &key ) const; + + /** + * Deletes the cached image + * \param project the project used to generate the image to provide path + * \param request the request used to generate the image to provider parameters or data + * \param key the key provided by the access control to identify differents images for the same request + * \returns true if the image has been deleted + */ + bool deleteCachedImage( const QgsProject *project, const QgsServerRequest &request, const QString &key ) const; + + /** + * Deletes all cached images for a QGIS project + * \param project the project used to generate the images to provide path + * \returns true if the images have been deleted + */ + bool deleteCachedImages( const QgsProject *project ) const; + /** * Register a server cache filter * \param serverCache the server cache to add From 05e9ab9e559179e04ad5e4ff15c2747ed52f5120 Mon Sep 17 00:00:00 2001 From: rldhont Date: Wed, 1 Aug 2018 16:17:22 +0200 Subject: [PATCH 12/33] [Server][Feature][needs-docs] Update WMTS service to use cache manager for tiles --- src/server/services/wmts/qgswmtsgettile.cpp | 43 ++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/src/server/services/wmts/qgswmtsgettile.cpp b/src/server/services/wmts/qgswmtsgettile.cpp index fa984b425f0f..f650faf9266c 100644 --- a/src/server/services/wmts/qgswmtsgettile.cpp +++ b/src/server/services/wmts/qgswmtsgettile.cpp @@ -27,16 +27,57 @@ namespace QgsWmts QgsServerResponse &response ) { Q_UNUSED( version ); - QgsServerRequest::Parameters params = request.parameters(); // WMS query QUrlQuery query = translateWmtsParamToWmsQueryItem( QStringLiteral( "GetMap" ), params, project, serverIface ); + // Get cached image + QStringList cacheKeyList; + bool cache = true; + + QgsAccessControl *accessControl = serverIface->accessControls(); + if ( accessControl ) + cache = accessControl->fillCacheKey( cacheKeyList ); + + QString cacheKey = cacheKeyList.join( QStringLiteral( "-" ) ); + QgsServerCacheManager *cacheManager = serverIface->cacheManager(); + if ( cacheManager && cache ) + { + QString contentType = params.value( QStringLiteral( "FORMAT" ) ); + QString saveFormat; + std::unique_ptr image; + if ( contentType == "image/jpeg" ) + { + saveFormat = "JPEG"; + image = qgis::make_unique( 256, 256, QImage::Format_RGB32 ); + } + else + { + saveFormat = "PNG"; + image = qgis::make_unique( 256, 256, QImage::Format_ARGB32_Premultiplied ); + } + + QByteArray content = cacheManager->getCachedImage( project, request, cacheKey ); + if ( !content.isEmpty() && image->loadFromData( content ) ) + { + response.setHeader( "Content-Type", contentType ); + image->save( response.io(), qPrintable( saveFormat ) ); + return; + } + } + + QgsServerParameters wmsParams( query ); QgsServerRequest wmsRequest( "?" + query.query( QUrl::FullyDecoded ) ); QgsService *service = serverIface->serviceRegistry()->getService( wmsParams.service(), wmsParams.version() ); service->executeRequest( wmsRequest, response, project ); + if ( cache && cacheManager ) + { + QByteArray content = response.data(); + if ( !content.isEmpty() ) + cacheManager->setCachedImage( &content, project, request, cacheKey ); + } } } // namespace QgsWmts From 9463d04d5452ddd151ae74479fc390437279d8c7 Mon Sep 17 00:00:00 2001 From: rldhont Date: Wed, 1 Aug 2018 16:17:52 +0200 Subject: [PATCH 13/33] [Server][Feature][needs-docs] Update cache manager tests for images (tiles) --- .../src/python/test_qgsserver_cachemanager.py | 177 +- .../qgis_server_accesscontrol/project.qgs | 2918 +++++++++-------- 2 files changed, 1667 insertions(+), 1428 deletions(-) diff --git a/tests/src/python/test_qgsserver_cachemanager.py b/tests/src/python/test_qgsserver_cachemanager.py index 8cc0dd6cb2dc..34406b96f43f 100644 --- a/tests/src/python/test_qgsserver_cachemanager.py +++ b/tests/src/python/test_qgsserver_cachemanager.py @@ -27,7 +27,8 @@ from utilities import unitTestDataPath from qgis.server import QgsServer, QgsServerCacheFilter, QgsServerRequest, QgsBufferServerRequest, QgsBufferServerResponse from qgis.core import QgsApplication, QgsFontUtils, QgsProject -from qgis.PyQt.QtCore import QFile, QByteArray +from qgis.PyQt.QtCore import QIODevice, QFile, QByteArray, QBuffer +from qgis.PyQt.QtGui import QImage from qgis.PyQt.QtXml import QDomDocument @@ -40,10 +41,15 @@ class PyServerCache(QgsServerCacheFilter): def __init__(self, server_iface): super(QgsServerCacheFilter, self).__init__(server_iface) + self._cache_dir = os.path.join(tempfile.gettempdir(), "qgs_server_cache") if not os.path.exists(self._cache_dir): os.mkdir(self._cache_dir) + self._tile_cache_dir = os.path.join(self._cache_dir, 'tiles') + if not os.path.exists(self._tile_cache_dir): + os.mkdir(self._tile_cache_dir) + def getCachedDocument(self, project, request, key): m = hashlib.md5() paramMap = request.parameters() @@ -87,6 +93,53 @@ def deleteCachedDocuments(self, project): filelist = [f for f in os.listdir(self._cache_dir) if f.endswith(".xml")] return len(filelist) == 0 + def getCachedImage(self, project, request, key): + m = hashlib.md5() + paramMap = request.parameters() + urlParam = "&".join(["%s=%s" % (k, paramMap[k]) for k in paramMap.keys()]) + m.update(urlParam.encode('utf8')) + + if not os.path.exists(os.path.join(self._tile_cache_dir, m.hexdigest() + ".png")): + return QByteArray() + + img = QImage(m.hexdigest() + ".png") + with open(os.path.join(self._tile_cache_dir, m.hexdigest() + ".png"), "rb") as f: + statusOK = img.loadFromData(f.read()) + if not statusOK: + print("Could not read or find the contents document. Error at line %d, column %d:\n%s" % (errorLine, errorColumn, errorStr)) + return QByteArray() + + ba = QByteArray() + buff = QBuffer(ba) + buff.open(QIODevice.WriteOnly) + img.save(buff, 'PNG') + return ba + + def setCachedImage(self, img, project, request, key): + m = hashlib.md5() + paramMap = request.parameters() + urlParam = "&".join(["%s=%s" % (k, paramMap[k]) for k in paramMap.keys()]) + m.update(urlParam.encode('utf8')) + with open(os.path.join(self._tile_cache_dir, m.hexdigest() + ".png"), "wb") as f: + f.write(img) + return os.path.exists(os.path.join(self._tile_cache_dir, m.hexdigest() + ".png")) + + def deleteCachedImage(self, project, request, key): + m = hashlib.md5() + paramMap = request.parameters() + urlParam = "&".join(["%s=%s" % (k, paramMap[k]) for k in paramMap.keys()]) + m.update(urlParam.encode('utf8')) + if os.path.exists(os.path.join(self._tile_cache_dir, m.hexdigest() + ".png")): + os.remove(os.path.join(self._tile_cache_dir, m.hexdigest() + ".png")) + return not os.path.exists(os.path.join(self._tile_cache_dir, m.hexdigest() + ".png")) + + def deleteCachedImages(self, project): + filelist = [f for f in os.listdir(self._tile_cache_dir) if f.endswith(".png")] + for f in filelist: + os.remove(os.path.join(self._tile_cache_dir, f)) + filelist = [f for f in os.listdir(self._tile_cache_dir) if f.endswith(".png")] + return len(filelist) == 0 + class TestQgsServerCacheManager(unittest.TestCase): @@ -186,8 +239,14 @@ def test_getcapabilities(self): # with cache header, body = self._execute_request(query_string) + # without cache + query_string = '?MAP=%s&SERVICE=WMTS&VERSION=1.0.0&REQUEST=%s' % (urllib.parse.quote(project), 'GetCapabilities') + header, body = self._execute_request(query_string) + # with cache + header, body = self._execute_request(query_string) + filelist = [f for f in os.listdir(self._servercache._cache_dir) if f.endswith(".xml")] - self.assertEqual(len(filelist), 5, 'Not enough file in cache') + self.assertEqual(len(filelist), 6, 'Not enough file in cache') cacheManager = self._server_iface.cacheManager() @@ -218,6 +277,120 @@ def test_getcapabilities(self): self.assertTrue(cacheManager.deleteCachedDocuments(None), 'deleteCachedDocuments does not retrun True') + def test_gettile(self): + project = self._project_path + assert os.path.exists(project), "Project file not found: " + project + + qs = "?" + "&".join(["%s=%s" % i for i in list({ + "MAP": urllib.parse.quote(project), + "SERVICE": "WMTS", + "VERSION": "1.0.0", + "REQUEST": "GetTile", + "LAYER": "Country", + "STYLE": "", + "TILEMATRIXSET": "EPSG:3857", + "TILEMATRIX": "0", + "TILEROW": "0", + "TILECOL": "0", + "FORMAT": "image/png" + }.items())]) + + # without cache + r, h = self._result(self._execute_request(qs)) + self.assertEqual( + h.get("Content-Type"), "image/png", + "Content type is wrong: %s\n%s" % (h.get("Content-Type"), r)) + # with cache + r, h = self._result(self._execute_request(qs)) + self.assertEqual( + h.get("Content-Type"), "image/png", + "Content type is wrong: %s\n%s" % (h.get("Content-Type"), r)) + + qs = "?" + "&".join(["%s=%s" % i for i in list({ + "MAP": urllib.parse.quote(project), + "SERVICE": "WMTS", + "VERSION": "1.0.0", + "REQUEST": "GetTile", + "LAYER": "Country", + "STYLE": "", + "TILEMATRIXSET": "EPSG:4326", + "TILEMATRIX": "0", + "TILEROW": "0", + "TILECOL": "0", + "FORMAT": "image/png" + }.items())]) + + # without cache + r, h = self._result(self._execute_request(qs)) + self.assertEqual( + h.get("Content-Type"), "image/png", + "Content type is wrong: %s\n%s" % (h.get("Content-Type"), r)) + # with cache + r, h = self._result(self._execute_request(qs)) + self.assertEqual( + h.get("Content-Type"), "image/png", + "Content type is wrong: %s\n%s" % (h.get("Content-Type"), r)) + + qs = "?" + "&".join(["%s=%s" % i for i in list({ + "MAP": urllib.parse.quote(project), + "SERVICE": "WMTS", + "VERSION": "1.0.0", + "REQUEST": "GetTile", + "LAYER": "QGIS Server Hello World", + "STYLE": "", + "TILEMATRIXSET": "EPSG:3857", + "TILEMATRIX": "0", + "TILEROW": "0", + "TILECOL": "0", + "FORMAT": "image/png" + }.items())]) + + # without cache + r, h = self._result(self._execute_request(qs)) + self.assertEqual( + h.get("Content-Type"), "image/png", + "Content type is wrong: %s\n%s" % (h.get("Content-Type"), r)) + # with cache + r, h = self._result(self._execute_request(qs)) + self.assertEqual( + h.get("Content-Type"), "image/png", + "Content type is wrong: %s\n%s" % (h.get("Content-Type"), r)) + + qs = "?" + "&".join(["%s=%s" % i for i in list({ + "MAP": urllib.parse.quote(project), + "SERVICE": "WMTS", + "VERSION": "1.0.0", + "REQUEST": "GetTile", + "LAYER": "QGIS Server Hello World", + "STYLE": "", + "TILEMATRIXSET": "EPSG:4326", + "TILEMATRIX": "0", + "TILEROW": "0", + "TILECOL": "0", + "FORMAT": "image/png" + }.items())]) + + # without cache + r, h = self._result(self._execute_request(qs)) + self.assertEqual( + h.get("Content-Type"), "image/png", + "Content type is wrong: %s\n%s" % (h.get("Content-Type"), r)) + # with cache + r, h = self._result(self._execute_request(qs)) + self.assertEqual( + h.get("Content-Type"), "image/png", + "Content type is wrong: %s\n%s" % (h.get("Content-Type"), r)) + + filelist = [f for f in os.listdir(self._servercache._tile_cache_dir) if f.endswith(".png")] + self.assertEqual(len(filelist), 4, 'Not enough image in cache') + + cacheManager = self._server_iface.cacheManager() + + self.assertTrue(cacheManager.deleteCachedImages(None), 'deleteCachedImages does not retrun True') + + filelist = [f for f in os.listdir(self._servercache._tile_cache_dir) if f.endswith(".png")] + self.assertEqual(len(filelist), 0, 'All images in cache are not deleted ') + if __name__ == "__main__": unittest.main() diff --git a/tests/testdata/qgis_server_accesscontrol/project.qgs b/tests/testdata/qgis_server_accesscontrol/project.qgs index 90863eb46bd4..2096c4afb266 100644 --- a/tests/testdata/qgis_server_accesscontrol/project.qgs +++ b/tests/testdata/qgis_server_accesscontrol/project.qgs @@ -1,16 +1,17 @@ - + + QGIS Server Hello World - - - + + + +proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs 3857 3857 EPSG:3857 - WGS 84 / Pseudo Mercator + WGS 84 / Pseudo-Mercator merc WGS84 false @@ -18,31 +19,31 @@ - + - + - + - + - + - + - + - + - + @@ -57,20 +58,20 @@ country20170328164317226 - + - - - - - - - - + + + + + + + + - + meters 11863620.20301065221428871 @@ -85,7 +86,7 @@ 3857 3857 EPSG:3857 - WGS 84 / Pseudo Mercator + WGS 84 / Pseudo-Mercator merc WGS84 false @@ -94,56 +95,55 @@ 0 - - - - + -19619892.68012013286352158 -10327100.34232237376272678 @@ -162,7 +162,7 @@ 3857 3857 EPSG:3857 - WGS 84 / Pseudo Mercator + WGS 84 / Pseudo-Mercator merc WGS84 false @@ -175,6 +175,7 @@ + @@ -186,32 +187,32 @@ - false + true - spatialite + - + - - + + - - + + - + - - + + @@ -224,7 +225,7 @@ - + @@ -244,180 +245,180 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -425,12 +426,12 @@ 0 0 - - - + + + - - + + @@ -452,22 +453,22 @@ - + . - - + + - + - + - . @@ -494,7 +495,7 @@ def my_form_open(dialog, layer, feature): 0 generatedlayout - + @@ -510,46 +511,46 @@ def my_form_open(dialog, layer, feature): - + - - - - - - - - - - - - - + + + + + + + + + + + + + - - - - - - - - - - - - + + + + + + + + + + + + @@ -560,26 +561,26 @@ def my_form_open(dialog, layer, feature): - - - - + + + + - - - + + + - + @@ -587,17 +588,17 @@ def my_form_open(dialog, layer, feature): 0 1 - - - + + + - + @@ -618,31 +619,31 @@ def my_form_open(dialog, layer, feature): - - + + - - + + - - + + - - + + - + - + - ../../../../../.. @@ -669,8 +670,10 @@ def my_form_open(dialog, layer, feature): 0 generatedlayout - + + + @@ -680,7 +683,7 @@ def my_form_open(dialog, layer, feature): name - + -14746250.07513097859919071 -112075.42807669920148328 @@ -699,7 +702,7 @@ def my_form_open(dialog, layer, feature): 3857 3857 EPSG:3857 - WGS 84 / Pseudo Mercator + WGS 84 / Pseudo-Mercator merc WGS84 false @@ -712,6 +715,7 @@ def my_form_open(dialog, layer, feature): + @@ -727,57 +731,57 @@ def my_form_open(dialog, layer, feature): - spatialite + - + - - - - - - - - - - - - - + + + + + + + + + + + + + - - - - - - - - - - - - + + + + + + + + + + + + @@ -787,24 +791,24 @@ def my_form_open(dialog, layer, feature): - - + + 0 0 1 - - - + + + - + @@ -832,36 +836,36 @@ def my_form_open(dialog, layer, feature): - - - + + + - - - + + + - - - + + + - - - + + + - + - + - ../../../../../.. @@ -888,15 +892,17 @@ def my_form_open(dialog, layer, feature): 0 generatedlayout - - - + + + - - + + + + @@ -906,12 +912,12 @@ def my_form_open(dialog, layer, feature): "pkuid" - + - -14746250.07513097859919071 - -112075.42807669920148328 - 11342200.07719692215323448 - 10914413.7141284141689539 + -2465695.66895584994927049 + 80258.53580146089370828 + 5037064.00943838991224766 + 3762589.19456820981577039 Hello_SubsetString_copy20160222085231770 dbname='./helloworld.db' table="hello" (geom) sql="pkuid" in ( 7, 8 ) @@ -925,7 +931,7 @@ def my_form_open(dialog, layer, feature): 3857 3857 EPSG:3857 - WGS 84 / Pseudo Mercator + WGS 84 / Pseudo-Mercator merc WGS84 false @@ -938,6 +944,7 @@ def my_form_open(dialog, layer, feature): + @@ -953,57 +960,57 @@ def my_form_open(dialog, layer, feature): - spatialite + - + - - - - - - - - - - - - - + + + + + + + + + + + + + - - - - - - - - - - - - + + + + + + + + + + + + @@ -1013,24 +1020,24 @@ def my_form_open(dialog, layer, feature): - - + + 0 0 1 - - - + + + - + @@ -1058,31 +1065,31 @@ def my_form_open(dialog, layer, feature): - - - + + + - - - + + + - - - + + + - - - + + + - + - + ../../../../../.. @@ -1109,15 +1116,17 @@ def my_form_open(dialog, layer, feature): 0 generatedlayout - - - + + + - - + + + + @@ -1127,7 +1136,7 @@ def my_form_open(dialog, layer, feature): "pkuid" - + -14746250.07513097859919071 -112075.42807669920148328 @@ -1146,7 +1155,7 @@ def my_form_open(dialog, layer, feature): 3857 3857 EPSG:3857 - WGS 84 / Pseudo Mercator + WGS 84 / Pseudo-Mercator merc WGS84 false @@ -1159,6 +1168,7 @@ def my_form_open(dialog, layer, feature): + @@ -1174,57 +1184,57 @@ def my_form_open(dialog, layer, feature): - spatialite + - + - - - - - - - - - - - - - + + + + + + + + + + + + + - - - - - - - - - - - - + + + + + + + + + + + + @@ -1234,24 +1244,24 @@ def my_form_open(dialog, layer, feature): - - + + 0 0 1 - - - + + + - + @@ -1279,31 +1289,31 @@ def my_form_open(dialog, layer, feature): - - - + + + - - - + + + - - - + + + - - - + + + - + - + ../../../../../.. @@ -1330,15 +1340,17 @@ def my_form_open(dialog, layer, feature): 0 generatedlayout - - - + + + - - + + + + @@ -1348,7 +1360,7 @@ def my_form_open(dialog, layer, feature): "pkuid" - + -19619892.68012013286352158 -10327100.34232237376272678 @@ -1367,7 +1379,7 @@ def my_form_open(dialog, layer, feature): 3857 3857 EPSG:3857 - WGS 84 / Pseudo Mercator + WGS 84 / Pseudo-Mercator merc WGS84 false @@ -1380,6 +1392,7 @@ def my_form_open(dialog, layer, feature): + @@ -1395,29 +1408,29 @@ def my_form_open(dialog, layer, feature): - spatialite + - + - - + + - - + + - + - - + + @@ -1452,147 +1465,147 @@ def my_form_open(dialog, layer, feature): - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 0 @@ -1600,31 +1613,31 @@ def my_form_open(dialog, layer, feature): name - - - + + - + 0 tablayout @@ -1637,19 +1650,19 @@ def my_form_open(dialog, layer, feature): - + - - + + - - + + - + - - + + @@ -1670,7 +1683,7 @@ def my_form_open(dialog, layer, feature): - + @@ -1705,147 +1718,147 @@ def my_form_open(dialog, layer, feature): - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 0 @@ -1853,31 +1866,31 @@ def my_form_open(dialog, layer, feature): name - - - + + - + 0 tablayout @@ -1891,46 +1904,46 @@ def my_form_open(dialog, layer, feature): - + - - - - - - - - - - - - - + + + + + + + + + + + + + - - - - - - - - - - - - + + + + + + + + + + + + @@ -1941,20 +1954,20 @@ def my_form_open(dialog, layer, feature): - - - - + + + + - - - + + + @@ -1964,17 +1977,17 @@ def my_form_open(dialog, layer, feature): 0 1 - - - + + + - + @@ -1995,27 +2008,27 @@ def my_form_open(dialog, layer, feature): - - + + - - + + - - + + - - + + - + - + ../../../../../.. @@ -2026,8 +2039,10 @@ def my_form_open(dialog, layer, feature): 0 generatedlayout - + + + @@ -2037,7 +2052,7 @@ def my_form_open(dialog, layer, feature): "name" - + -19619892.68012013286352158 -10327100.34232237376272678 @@ -2056,7 +2071,7 @@ def my_form_open(dialog, layer, feature): 3857 3857 EPSG:3857 - WGS 84 / Pseudo Mercator + WGS 84 / Pseudo-Mercator merc WGS84 false @@ -2069,6 +2084,7 @@ def my_form_open(dialog, layer, feature): + @@ -2084,37 +2100,37 @@ def my_form_open(dialog, layer, feature): - spatialite + - + - - - - - - - - - - - - - + + + + + + + + + + + + + @@ -2124,7 +2140,7 @@ def my_form_open(dialog, layer, feature): - + @@ -2132,18 +2148,18 @@ def my_form_open(dialog, layer, feature): 0 1 - - - - + + + + - + @@ -2164,31 +2180,31 @@ def my_form_open(dialog, layer, feature): - - + + - - + + - - + + - - + + - + - + - @@ -2214,6 +2230,8 @@ def my_form_open(dialog, layer, feature): ]]> 0 generatedlayout + + @@ -2223,7 +2241,7 @@ def my_form_open(dialog, layer, feature): name - + -29.99999999999666755 29.99999999999666755 @@ -2255,6 +2273,7 @@ def my_form_open(dialog, layer, feature): + @@ -2270,10 +2289,9 @@ def my_form_open(dialog, layer, feature): - - + gdal @@ -2283,7 +2301,7 @@ def my_form_open(dialog, layer, feature): - + None @@ -2300,12 +2318,12 @@ def my_form_open(dialog, layer, feature): - + 0 - + -14746250.07513097859919071 -112075.42807669920148328 @@ -2324,7 +2342,7 @@ def my_form_open(dialog, layer, feature): 3857 3857 EPSG:3857 - WGS 84 / Pseudo Mercator + WGS 84 / Pseudo-Mercator merc WGS84 false @@ -2337,6 +2355,7 @@ def my_form_open(dialog, layer, feature): + @@ -2352,57 +2371,57 @@ def my_form_open(dialog, layer, feature): - spatialite + - + - - - - - - - - - - - - - + + + + + + + + + + + + + - - - - - - - - - - - - + + + + + + + + + + + + @@ -2416,17 +2435,17 @@ def my_form_open(dialog, layer, feature): 0 1 - - - + + + - + @@ -2454,31 +2473,31 @@ def my_form_open(dialog, layer, feature): - - - + + + - - - + + + - - - + + + - - - + + + - + - + ../../../../../.. @@ -2489,15 +2508,17 @@ def my_form_open(dialog, layer, feature): 0 generatedlayout - - - + + + - - + + + + @@ -2507,7 +2528,7 @@ def my_form_open(dialog, layer, feature): COALESCE( "pkuid", '<NULL>' ) - + 1000 2000 @@ -2526,7 +2547,7 @@ def my_form_open(dialog, layer, feature): 3857 3857 EPSG:3857 - WGS 84 / Pseudo Mercator + WGS 84 / Pseudo-Mercator merc WGS84 false @@ -2539,6 +2560,7 @@ def my_form_open(dialog, layer, feature): + @@ -2554,44 +2576,44 @@ def my_form_open(dialog, layer, feature): - spatialite + - + - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + @@ -2605,17 +2627,17 @@ def my_form_open(dialog, layer, feature): 0 1 - - - + + + - + @@ -2643,31 +2665,31 @@ def my_form_open(dialog, layer, feature): - - - + + + - - - + + + - - - + + + - - - + + + - + - + ../../../../../.. @@ -2677,6 +2699,8 @@ def my_form_open(dialog, layer, feature): 0 generatedlayout + + @@ -2699,213 +2723,255 @@ def my_form_open(dialog, layer, feature): + true + 90 + + + + + false + + + meters + m2 + + + + 255 + true + + 1 + + + + QGIS Server test + QGIS + Stéphane Brunner + + + points20150803121107046 + + + points20150803121107046 + + + points20150803121107046 + + + + 5000 + + + + Simple test app. + + true + false + + false + + false + + + + + + 2000 + days + + 0 + 1 + 1 + + 0 + 0 + 1 + + + + + + + + country20131022151106556 + + true + + + 0 + 1 + 1 + 1 + 1 + + + None + 5 + + + + + WGS84 + + conditions unknown + + true + 2 + D + + + + 40 country20131022151106556 hello20131022151106574 - - enabled - enabled - + 1 advanced 40.000000 40.000000 + + enabled + enabled + + to vertex + 1 1 - to vertex - - 40 - 1 to_vertex to_vertex - - - 90 - - Hello_SubsetString_copy20160222085231770 - Hello_copy20150804164427541 - country20131022151106556 - hello20131022151106574 - points20150803121107046 - - - false - - - - - 2 - true - D - - true - - - - - false - QGIS - 5000 - - - + false EPSG:3857 EPSG:4326 - - WGS84 - - - meters - m2 - - QGIS Server test + + +proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs + 1 + EPSG:3857 + 3857 + + + + + country20131022151106556 + + true + + true + false + 50 0 - true + 30 false 16 - 30 - 50 - false false + true - - false - - 1 - 1 - 1 - 1 - 0 - - - false - true - - - - - - - dem20150730091219559 - None - 5000 - - false - - - Stéphane Brunner - conditions unknown - - - points20150803121107046 - - - points20150803121107046 - - - points20150803121107046 - - - - EPSG:3857 - 1 - +proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs - 3857 - + -20609693.37008669599890709 -11055006.82298868149518967 20961935.60850896313786507 19143772.79360072687268257 - Simple test app. - - 5 - - 1 - 2000 - 1 - 0 - 0 - - 0 - days - - 1 - - 128 255 + 128 + 255 255 - 246 - 108 255 - 255 + 108 + 246 - - - - 1 - - 255 - true - - + + 5000 + + + + + false + + + Hello_SubsetString_copy20160222085231770 + Hello_copy20150804164427541 + country20131022151106556 + hello20131022151106574 + points20150803121107046 + + + + + + + QGIS Server Hello World + + + + + + + + + + + + + 2000-01-01T00:00:00 + - - - + + + - - - - - - - - - - - - - + + + + + + + + + + + + + - - - + + + @@ -2913,181 +2979,181 @@ def my_form_open(dialog, layer, feature): - - - + + + - - + + - - - + + + - + - points20150803121107046 - hello20131022151106574 - Hello_copy20150804164427541 - Hello_SubsetString_copy20160222085231770 - Hello_Project_SubsetString_copy20160223113949592 - dem20150730091219559 - country20131022151106556 - Country_copy20161127151800736 - country20170328164317226 + points20150803121107046 + hello20131022151106574 + Hello_copy20150804164427541 + Hello_SubsetString_copy20160222085231770 + Hello_Project_SubsetString_copy20160223113949592 + dem20150730091219559 + country20131022151106556 + Country_copy20161127151800736 + country20170328164317226 - + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + - + - + - - - + + + - - + + - - - + + + - - + + - + - - - + + + - - - - - - - - - - - - - + + + + + + + + + + + + + - - - + + + @@ -3095,212 +3161,212 @@ def my_form_open(dialog, layer, feature): - - - + + + - - - - + + + - - + + - - - + + + - + - points20150803121107046 - hello20131022151106574 - Hello_copy20150804164427541 - Hello_SubsetString_copy20160222085231770 - Hello_Project_SubsetString_copy20160223113949592 - dem20150730091219559 - country20131022151106556 - Country_copy20161127151800736 - country20170328164317226 + points20150803121107046 + hello20131022151106574 + Hello_copy20150804164427541 + Hello_SubsetString_copy20160222085231770 + Hello_Project_SubsetString_copy20160223113949592 + dem20150730091219559 + country20131022151106556 + Country_copy20161127151800736 + country20170328164317226 - + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + - + - + - - - + + + - - + + - - - + + + - - + + - + - - - + + + - - - - - - - - - - - - - + + + + + + + + + + + + + - - - + + + @@ -3308,44 +3374,44 @@ def my_form_open(dialog, layer, feature): - - - + + + - + - + - - - + + + - + - + - + - + From 65f9c91e8ef390bcfc7874549d7844d2b3cdeb17 Mon Sep 17 00:00:00 2001 From: rldhont Date: Wed, 1 Aug 2018 16:56:05 +0200 Subject: [PATCH 14/33] [Server] Fixing spelling and doc coverage --- src/server/qgsservercachefilter.h | 13 +++++++------ src/server/qgsservercachemanager.h | 12 ++++++------ src/server/services/wmts/qgswmtsgetcapabilities.cpp | 2 +- tests/src/python/test_qgsserver_cachemanager.py | 6 +++--- 4 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/server/qgsservercachefilter.h b/src/server/qgsservercachefilter.h index 99e6dab65f1c..ac6100b165b6 100644 --- a/src/server/qgsservercachefilter.h +++ b/src/server/qgsservercachefilter.h @@ -37,6 +37,7 @@ class QgsServerInterface; * \ingroup server * \class QgsServerCacheFilter * \brief Class defining cache interface for QGIS Server plugins. + * \since QGIS 3.4 */ class SERVER_EXPORT QgsServerCacheFilter { @@ -56,7 +57,7 @@ class SERVER_EXPORT QgsServerCacheFilter * Returns cached document (or 0 if document not in cache) like capabilities * \param project the project used to generate the document to provide path * \param request the request used to generate the document to provider parameters or data - * \param key the key provided by the access control to identify differents documents for the same request + * \param key the key provided by the access control to identify different documents for the same request * \returns QByteArray of the cached document or an empty one if no corresponding document found */ virtual QByteArray getCachedDocument( const QgsProject *project, const QgsServerRequest &request, const QString &key ) const; @@ -66,7 +67,7 @@ class SERVER_EXPORT QgsServerCacheFilter * \param doc the document to cache * \param project the project used to generate the document to provide path * \param request the request used to generate the document to provider parameters or data - * \param key the key provided by the access control to identify differents documents for the same request + * \param key the key provided by the access control to identify different documents for the same request * \returns true if the document has been cached */ virtual bool setCachedDocument( const QDomDocument *doc, const QgsProject *project, const QgsServerRequest &request, const QString &key ) const; @@ -75,7 +76,7 @@ class SERVER_EXPORT QgsServerCacheFilter * Deletes the cached document * \param project the project used to generate the document to provide path * \param request the request used to generate the document to provider parameters or data - * \param key the key provided by the access control to identify differents documents for the same request + * \param key the key provided by the access control to identify different documents for the same request * \returns true if the document has been deleted */ virtual bool deleteCachedDocument( const QgsProject *project, const QgsServerRequest &request, const QString &key ) const; @@ -91,7 +92,7 @@ class SERVER_EXPORT QgsServerCacheFilter * Returns cached image (or 0 if document not in cache) like tiles * \param project the project used to generate the image to provide path * \param request the request used to generate the image to provider parameters or data - * \param key the key provided by the access control to identify differents images for the same request + * \param key the key provided by the access control to identify different images for the same request * \returns QByteArray of the cached image or an empty one if no corresponding image found */ virtual QByteArray getCachedImage( const QgsProject *project, const QgsServerRequest &request, const QString &key ) const; @@ -101,7 +102,7 @@ class SERVER_EXPORT QgsServerCacheFilter * \param img the document to cache * \param project the project used to generate the image to provide path * \param request the request used to generate the image to provider parameters or data - * \param key the key provided by the access control to identify differents images for the same request + * \param key the key provided by the access control to identify different images for the same request * \returns true if the image has been cached */ virtual bool setCachedImage( const QByteArray *img, const QgsProject *project, const QgsServerRequest &request, const QString &key ) const; @@ -110,7 +111,7 @@ class SERVER_EXPORT QgsServerCacheFilter * Deletes the cached image * \param project the project used to generate the image to provide path * \param request the request used to generate the image to provider parameters or data - * \param key the key provided by the access control to identify differents images for the same request + * \param key the key provided by the access control to identify different images for the same request * \returns true if the image has been deleted */ virtual bool deleteCachedImage( const QgsProject *project, const QgsServerRequest &request, const QString &key ) const; diff --git a/src/server/qgsservercachemanager.h b/src/server/qgsservercachemanager.h index e38c0159e3b5..da6e32a1270d 100644 --- a/src/server/qgsservercachemanager.h +++ b/src/server/qgsservercachemanager.h @@ -70,7 +70,7 @@ class SERVER_EXPORT QgsServerCacheManager * Returns cached document (or 0 if document not in cache) like capabilities * \param project the project used to generate the document to provide path * \param request the request used to generate the document to provider parameters or data - * \param key the key provided by the access control to identify differents documents for the same request + * \param key the key provided by the access control to identify different documents for the same request * \returns the cached document or 0 if no corresponding document found */ QByteArray getCachedDocument( const QgsProject *project, const QgsServerRequest &request, const QString &key ) const; @@ -80,7 +80,7 @@ class SERVER_EXPORT QgsServerCacheManager * \param doc the document to cache * \param project the project used to generate the document to provide path * \param request the request used to generate the document to provider parameters or data - * \param key the key provided by the access control to identify differents documents for the same request + * \param key the key provided by the access control to identify different documents for the same request * \returns true if the document has been cached */ bool setCachedDocument( const QDomDocument *doc, const QgsProject *project, const QgsServerRequest &request, const QString &key ) const; @@ -89,7 +89,7 @@ class SERVER_EXPORT QgsServerCacheManager * Deletes the cached document * \param project the project used to generate the document to provide path * \param request the request used to generate the document to provider parameters or data - * \param key the key provided by the access control to identify differents documents for the same request + * \param key the key provided by the access control to identify different documents for the same request * \returns true if the document has been deleted */ bool deleteCachedDocument( const QgsProject *project, const QgsServerRequest &request, const QString &key ) const; @@ -105,7 +105,7 @@ class SERVER_EXPORT QgsServerCacheManager * Returns cached image (or 0 if image not in cache) like tiles * \param project the project used to generate the image to provide path * \param request the request used to generate the image to provider parameters or data - * \param key the key provided by the access control to identify differents images for the same request + * \param key the key provided by the access control to identify different images for the same request * \returns the cached image or 0 if no corresponding image found */ QByteArray getCachedImage( const QgsProject *project, const QgsServerRequest &request, const QString &key ) const; @@ -115,7 +115,7 @@ class SERVER_EXPORT QgsServerCacheManager * \param img the image to cache * \param project the project used to generate the image to provide path * \param request the request used to generate the image to provider parameters or data - * \param key the key provided by the access control to identify differents images for the same request + * \param key the key provided by the access control to identify different images for the same request * \returns true if the image has been cached */ bool setCachedImage( const QByteArray *img, const QgsProject *project, const QgsServerRequest &request, const QString &key ) const; @@ -124,7 +124,7 @@ class SERVER_EXPORT QgsServerCacheManager * Deletes the cached image * \param project the project used to generate the image to provide path * \param request the request used to generate the image to provider parameters or data - * \param key the key provided by the access control to identify differents images for the same request + * \param key the key provided by the access control to identify different images for the same request * \returns true if the image has been deleted */ bool deleteCachedImage( const QgsProject *project, const QgsServerRequest &request, const QString &key ) const; diff --git a/src/server/services/wmts/qgswmtsgetcapabilities.cpp b/src/server/services/wmts/qgswmtsgetcapabilities.cpp index 8b55cbfb2390..587b751f9e98 100644 --- a/src/server/services/wmts/qgswmtsgetcapabilities.cpp +++ b/src/server/services/wmts/qgswmtsgetcapabilities.cpp @@ -216,7 +216,7 @@ namespace QgsWmts serviceElem.appendChild( onlineResourceElem ); } - //Contact informations + //Contact information QString contactPerson = QgsServerProjectUtils::owsServiceContactPerson( *project ); QString contactPosition = QgsServerProjectUtils::owsServiceContactPosition( *project ); QString contactMail = QgsServerProjectUtils::owsServiceContactMail( *project ); diff --git a/tests/src/python/test_qgsserver_cachemanager.py b/tests/src/python/test_qgsserver_cachemanager.py index 34406b96f43f..9c2288ff9c1f 100644 --- a/tests/src/python/test_qgsserver_cachemanager.py +++ b/tests/src/python/test_qgsserver_cachemanager.py @@ -250,7 +250,7 @@ def test_getcapabilities(self): cacheManager = self._server_iface.cacheManager() - self.assertTrue(cacheManager.deleteCachedDocuments(None), 'deleteCachedDocuments does not retrun True') + self.assertTrue(cacheManager.deleteCachedDocuments(None), 'deleteCachedDocuments does not return True') filelist = [f for f in os.listdir(self._servercache._cache_dir) if f.endswith(".xml")] self.assertEqual(len(filelist), 0, 'All files in cache are not deleted ') @@ -275,7 +275,7 @@ def test_getcapabilities(self): self.assertTrue(cDoc.setContent(cContent), 'cachedDocument not XML doc') self.assertEqual(doc.documentElement().tagName(), cDoc.documentElement().tagName(), 'cachedDocument not equal to provide document') - self.assertTrue(cacheManager.deleteCachedDocuments(None), 'deleteCachedDocuments does not retrun True') + self.assertTrue(cacheManager.deleteCachedDocuments(None), 'deleteCachedDocuments does not return True') def test_gettile(self): project = self._project_path @@ -386,7 +386,7 @@ def test_gettile(self): cacheManager = self._server_iface.cacheManager() - self.assertTrue(cacheManager.deleteCachedImages(None), 'deleteCachedImages does not retrun True') + self.assertTrue(cacheManager.deleteCachedImages(None), 'deleteCachedImages does not return True') filelist = [f for f in os.listdir(self._servercache._tile_cache_dir) if f.endswith(".png")] self.assertEqual(len(filelist), 0, 'All images in cache are not deleted ') From d0041a22e868f8b73c44b3e5a5839b3a53730b8f Mon Sep 17 00:00:00 2001 From: rldhont Date: Wed, 1 Aug 2018 16:59:23 +0200 Subject: [PATCH 15/33] [Server][Feature][needs-docs] Add Cache manager SIP files --- .../qgsservercachefilter.sip.in | 133 ++++++++++++++++ .../qgsservercachemanager.sip.in | 144 ++++++++++++++++++ 2 files changed, 277 insertions(+) create mode 100644 python/server/auto_generated/qgsservercachefilter.sip.in create mode 100644 python/server/auto_generated/qgsservercachemanager.sip.in diff --git a/python/server/auto_generated/qgsservercachefilter.sip.in b/python/server/auto_generated/qgsservercachefilter.sip.in new file mode 100644 index 000000000000..aadf3b603618 --- /dev/null +++ b/python/server/auto_generated/qgsservercachefilter.sip.in @@ -0,0 +1,133 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/server/qgsservercachefilter.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + + + + +class QgsServerCacheFilter +{ +%Docstring +Class defining cache interface for QGIS Server plugins. + +.. versionadded:: 3.4 +%End + +%TypeHeaderCode +#include "qgsservercachefilter.h" +%End + public: + + QgsServerCacheFilter( const QgsServerInterface *serverInterface ); +%Docstring +Constructor +QgsServerInterface passed to plugins constructors +and must be passed to QgsServerCacheFilter instances. +%End + + virtual ~QgsServerCacheFilter(); + + virtual QByteArray getCachedDocument( const QgsProject *project, const QgsServerRequest &request, const QString &key ) const; +%Docstring +Returns cached document (or 0 if document not in cache) like capabilities + +:param project: the project used to generate the document to provide path +:param request: the request used to generate the document to provider parameters or data +:param key: the key provided by the access control to identify different documents for the same request + +:return: QByteArray of the cached document or an empty one if no corresponding document found +%End + + virtual bool setCachedDocument( const QDomDocument *doc, const QgsProject *project, const QgsServerRequest &request, const QString &key ) const; +%Docstring +Updates or inserts the document in cache like capabilities + +:param doc: the document to cache +:param project: the project used to generate the document to provide path +:param request: the request used to generate the document to provider parameters or data +:param key: the key provided by the access control to identify different documents for the same request + +:return: true if the document has been cached +%End + + virtual bool deleteCachedDocument( const QgsProject *project, const QgsServerRequest &request, const QString &key ) const; +%Docstring +Deletes the cached document + +:param project: the project used to generate the document to provide path +:param request: the request used to generate the document to provider parameters or data +:param key: the key provided by the access control to identify different documents for the same request + +:return: true if the document has been deleted +%End + + virtual bool deleteCachedDocuments( const QgsProject *project ) const; +%Docstring +Deletes all cached documents for a QGIS project + +:param project: the project used to generate the documents to provide path + +:return: true if the documents have been deleted +%End + + virtual QByteArray getCachedImage( const QgsProject *project, const QgsServerRequest &request, const QString &key ) const; +%Docstring +Returns cached image (or 0 if document not in cache) like tiles + +:param project: the project used to generate the image to provide path +:param request: the request used to generate the image to provider parameters or data +:param key: the key provided by the access control to identify different images for the same request + +:return: QByteArray of the cached image or an empty one if no corresponding image found +%End + + virtual bool setCachedImage( const QByteArray *img, const QgsProject *project, const QgsServerRequest &request, const QString &key ) const; +%Docstring +Updates or inserts the image in cache like tiles + +:param img: the document to cache +:param project: the project used to generate the image to provide path +:param request: the request used to generate the image to provider parameters or data +:param key: the key provided by the access control to identify different images for the same request + +:return: true if the image has been cached +%End + + virtual bool deleteCachedImage( const QgsProject *project, const QgsServerRequest &request, const QString &key ) const; +%Docstring +Deletes the cached image + +:param project: the project used to generate the image to provide path +:param request: the request used to generate the image to provider parameters or data +:param key: the key provided by the access control to identify different images for the same request + +:return: true if the image has been deleted +%End + + virtual bool deleteCachedImages( const QgsProject *project ) const; +%Docstring +Deletes all cached images for a QGIS project + +:param project: the project used to generate the images to provide path + +:return: true if the images have been deleted +%End + +}; + +typedef QMultiMap QgsServerCacheFilterMap; + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/server/qgsservercachefilter.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/server/auto_generated/qgsservercachemanager.sip.in b/python/server/auto_generated/qgsservercachemanager.sip.in new file mode 100644 index 000000000000..047b0b33d31f --- /dev/null +++ b/python/server/auto_generated/qgsservercachemanager.sip.in @@ -0,0 +1,144 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/server/qgsservercachemanager.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + + + + + +class QgsServerCacheManager +{ +%Docstring +A helper class that centralizes caches accesses given by all the server cache filter plugins. + +.. versionadded:: 3.4 +%End + +%TypeHeaderCode +#include "qgsservercachemanager.h" +#include "qgsservercachefilter.h" +%End + public: + QgsServerCacheManager(); +%Docstring +Constructor +%End + + QgsServerCacheManager( const QgsServerCacheManager © ); +%Docstring +Constructor +%End + + + ~QgsServerCacheManager(); + + QByteArray getCachedDocument( const QgsProject *project, const QgsServerRequest &request, const QString &key ) const; +%Docstring +Returns cached document (or 0 if document not in cache) like capabilities + +:param project: the project used to generate the document to provide path +:param request: the request used to generate the document to provider parameters or data +:param key: the key provided by the access control to identify different documents for the same request + +:return: the cached document or 0 if no corresponding document found +%End + + bool setCachedDocument( const QDomDocument *doc, const QgsProject *project, const QgsServerRequest &request, const QString &key ) const; +%Docstring +Updates or inserts the document in cache like capabilities + +:param doc: the document to cache +:param project: the project used to generate the document to provide path +:param request: the request used to generate the document to provider parameters or data +:param key: the key provided by the access control to identify different documents for the same request + +:return: true if the document has been cached +%End + + bool deleteCachedDocument( const QgsProject *project, const QgsServerRequest &request, const QString &key ) const; +%Docstring +Deletes the cached document + +:param project: the project used to generate the document to provide path +:param request: the request used to generate the document to provider parameters or data +:param key: the key provided by the access control to identify different documents for the same request + +:return: true if the document has been deleted +%End + + bool deleteCachedDocuments( const QgsProject *project ) const; +%Docstring +Deletes all cached documents for a QGIS project + +:param project: the project used to generate the document to provide path + +:return: true if the document has been deleted +%End + + QByteArray getCachedImage( const QgsProject *project, const QgsServerRequest &request, const QString &key ) const; +%Docstring +Returns cached image (or 0 if image not in cache) like tiles + +:param project: the project used to generate the image to provide path +:param request: the request used to generate the image to provider parameters or data +:param key: the key provided by the access control to identify different images for the same request + +:return: the cached image or 0 if no corresponding image found +%End + + bool setCachedImage( const QByteArray *img, const QgsProject *project, const QgsServerRequest &request, const QString &key ) const; +%Docstring +Updates or inserts the image in cache like tiles + +:param img: the image to cache +:param project: the project used to generate the image to provide path +:param request: the request used to generate the image to provider parameters or data +:param key: the key provided by the access control to identify different images for the same request + +:return: true if the image has been cached +%End + + bool deleteCachedImage( const QgsProject *project, const QgsServerRequest &request, const QString &key ) const; +%Docstring +Deletes the cached image + +:param project: the project used to generate the image to provide path +:param request: the request used to generate the image to provider parameters or data +:param key: the key provided by the access control to identify different images for the same request + +:return: true if the image has been deleted +%End + + bool deleteCachedImages( const QgsProject *project ) const; +%Docstring +Deletes all cached images for a QGIS project + +:param project: the project used to generate the images to provide path + +:return: true if the images have been deleted +%End + + void registerServerCache( QgsServerCacheFilter *serverCache, int priority = 0 ); +%Docstring +Register a server cache filter + +:param serverCache: the server cache to add +:param priority: the priority used to define the order +%End + +}; + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/server/qgsservercachemanager.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ From 7633d2ab21e9c225939ba5d0a4fbf557be673dc0 Mon Sep 17 00:00:00 2001 From: rldhont Date: Fri, 3 Aug 2018 14:09:34 +0200 Subject: [PATCH 16/33] [Server] Various code cleaning for server cache manager and WMTS service --- .../qgsservercachemanager.sip.in | 2 +- .../auto_generated/qgsserverinterface.sip.in | 4 + src/app/qgsprojectproperties.cpp | 36 +++++---- src/server/qgsservercachefilter.cpp | 13 +-- src/server/qgsservercachefilter.h | 2 +- src/server/qgsservercachemanager.cpp | 9 --- src/server/qgsservercachemanager.h | 34 +++++--- src/server/qgsserverinterface.h | 6 +- src/server/qgsserverinterfaceimpl.cpp | 6 +- src/server/qgsserverinterfaceimpl.h | 14 +++- .../services/wcs/qgswcsgetcapabilities.cpp | 4 +- .../services/wfs/qgswfsgetcapabilities.cpp | 4 +- .../wfs/qgswfsgetcapabilities_1_0_0.cpp | 4 +- .../services/wms/qgswmsgetcapabilities.cpp | 8 +- src/server/services/wmts/qgswmts.cpp | 4 +- .../services/wmts/qgswmtsgetcapabilities.cpp | 42 +++++----- src/server/services/wmts/qgswmtsgettile.cpp | 10 +-- src/server/services/wmts/qgswmtsutils.cpp | 81 +++++++------------ src/server/services/wmts/qgswmtsutils.h | 22 ++--- 19 files changed, 145 insertions(+), 160 deletions(-) diff --git a/python/server/auto_generated/qgsservercachemanager.sip.in b/python/server/auto_generated/qgsservercachemanager.sip.in index 047b0b33d31f..f872e12ff30d 100644 --- a/python/server/auto_generated/qgsservercachemanager.sip.in +++ b/python/server/auto_generated/qgsservercachemanager.sip.in @@ -33,7 +33,7 @@ Constructor QgsServerCacheManager( const QgsServerCacheManager © ); %Docstring -Constructor +Copy constructor %End diff --git a/python/server/auto_generated/qgsserverinterface.sip.in b/python/server/auto_generated/qgsserverinterface.sip.in index a14c4d67fb3d..53c82e353db5 100644 --- a/python/server/auto_generated/qgsserverinterface.sip.in +++ b/python/server/auto_generated/qgsserverinterface.sip.in @@ -91,11 +91,15 @@ Register a server cache filter :param serverCache: the server cache to register :param priority: the priority used to order them + +.. versionadded:: 3.4 %End virtual QgsServerCacheManager *cacheManager() const = 0; %Docstring Gets the registered server cache filters + +.. versionadded:: 3.4 %End virtual QString getEnv( const QString &name ) const = 0; diff --git a/src/app/qgsprojectproperties.cpp b/src/app/qgsprojectproperties.cpp index d579f8a4d1bb..0ad40c06dd4f 100644 --- a/src/app/qgsprojectproperties.cpp +++ b/src/app/qgsprojectproperties.cpp @@ -672,30 +672,27 @@ QgsProjectProperties::QgsProjectProperties( QgsMapCanvas *mapCanvas, QWidget *pa QStringList wmtsPngLayerIdList = QgsProject::instance()->readListEntry( QStringLiteral( "WMTSPngLayers" ), QStringLiteral( "Layer" ) ); QStringList wmtsJpegLayerIdList = QgsProject::instance()->readListEntry( QStringLiteral( "WMTSJpegLayers" ), QStringLiteral( "Layer" ) ); - QgsTreeWidgetItem *projItem = new QgsTreeWidgetItem( QStringList() << QStringLiteral( "Project" ) << QLatin1String( "" ) ); + QgsTreeWidgetItem *projItem = new QgsTreeWidgetItem( QStringList() << QStringLiteral( "Project" ) ); projItem->setFlags( projItem->flags() | Qt::ItemIsUserCheckable | Qt::ItemIsSelectable ); projItem->setCheckState( 1, wmtsProject ? Qt::Checked : Qt::Unchecked ); projItem->setCheckState( 2, wmtsPngProject ? Qt::Checked : Qt::Unchecked ); projItem->setCheckState( 3, wmtsJpegProject ? Qt::Checked : Qt::Unchecked ); - projItem->setData( 0, Qt::UserRole, "project" ); + projItem->setData( 0, Qt::UserRole, QStringLiteral( "project" ) ); twWmtsLayers->addTopLevelItem( projItem ); populateWmtsTree( QgsProject::instance()->layerTreeRoot(), projItem ); projItem->setExpanded( true ); twWmtsLayers->header()->resizeSections( QHeaderView::ResizeToContents ); - Q_FOREACH ( QTreeWidgetItem *item, twWmtsLayers->findItems( "", Qt::MatchContains | Qt::MatchRecursive, 1 ) ) + for ( QTreeWidgetItem *item : twWmtsLayers->findItems( QString(), Qt::MatchContains | Qt::MatchRecursive, 1 ) ) { - /*if ( !item->checkState( 1 ) ) - continue;*/ - - QString t = item->data( 0, Qt::UserRole ).toString(); - if ( t == "group" ) + QString itemType = item->data( 0, Qt::UserRole ).toString(); + if ( itemType == QLatin1String( "group" ) ) { QString gName = item->data( 0, Qt::UserRole + 1 ).toString(); item->setCheckState( 1, wmtsGroupNameList.contains( gName ) ? Qt::Checked : Qt::Unchecked ); item->setCheckState( 2, wmtsPngGroupNameList.contains( gName ) ? Qt::Checked : Qt::Unchecked ); item->setCheckState( 3, wmtsJpegGroupNameList.contains( gName ) ? Qt::Checked : Qt::Unchecked ); } - else if ( t == "layer" ) + else if ( itemType == QLatin1String( "layer" ) ) { QString lId = item->data( 0, Qt::UserRole + 1 ).toString(); item->setCheckState( 1, wmtsLayerIdList.contains( lId ) ? Qt::Checked : Qt::Unchecked ); @@ -1274,19 +1271,19 @@ void QgsProjectProperties::apply() QStringList wmtsLayerList; QStringList wmtsPngLayerList; QStringList wmtsJpegLayerList; - Q_FOREACH ( const QTreeWidgetItem *item, twWmtsLayers->findItems( "", Qt::MatchContains | Qt::MatchRecursive, 1 ) ) + for ( const QTreeWidgetItem *item : twWmtsLayers->findItems( QString(), Qt::MatchContains | Qt::MatchRecursive, 1 ) ) { if ( !item->checkState( 1 ) ) continue; - QString t = item->data( 0, Qt::UserRole ).toString(); - if ( t == "project" ) + QString itemType = item->data( 0, Qt::UserRole ).toString(); + if ( itemType == QLatin1String( "project" ) ) { wmtsProject = true; wmtsPngProject = item->checkState( 2 ); wmtsJpegProject = item->checkState( 3 ); } - else if ( t == "group" ) + else if ( itemType == QLatin1String( "group" ) ) { QString gName = item->data( 0, Qt::UserRole + 1 ).toString(); wmtsGroupList << gName; @@ -1295,7 +1292,7 @@ void QgsProjectProperties::apply() if ( item->checkState( 3 ) ) wmtsJpegGroupList << gName; } - else if ( t == "layer" ) + else if ( itemType == QLatin1String( "layer" ) ) { QString lId = item->data( 0, Qt::UserRole + 1 ).toString(); wmtsLayerList << lId; @@ -2033,7 +2030,7 @@ void QgsProjectProperties::resetPythonMacros() void QgsProjectProperties::populateWmtsTree( const QgsLayerTreeGroup *treeGroup, QgsTreeWidgetItem *treeItem ) { - Q_FOREACH ( QgsLayerTreeNode *treeNode, treeGroup->children() ) + for ( QgsLayerTreeNode *treeNode : treeGroup->children() ) { QgsTreeWidgetItem *childItem = nullptr; if ( treeNode->nodeType() == QgsLayerTreeNode::NodeGroup ) @@ -2044,7 +2041,7 @@ void QgsProjectProperties::populateWmtsTree( const QgsLayerTreeGroup *treeGroup, childItem = new QgsTreeWidgetItem( QStringList() << gName ); childItem->setFlags( childItem->flags() | Qt::ItemIsUserCheckable | Qt::ItemIsSelectable ); - childItem->setData( 0, Qt::UserRole, "group" ); + childItem->setData( 0, Qt::UserRole, QStringLiteral( "group" ) ); childItem->setData( 0, Qt::UserRole + 1, gName ); treeItem->addChild( childItem ); @@ -2058,10 +2055,15 @@ void QgsProjectProperties::populateWmtsTree( const QgsLayerTreeGroup *treeGroup, QgsLayerTreeLayer *treeLayer = static_cast( treeNode ); QgsMapLayer *l = treeLayer->layer(); + if ( !l ) + { + continue; + } + childItem = new QgsTreeWidgetItem( QStringList() << l->name() ); childItem->setFlags( childItem->flags() | Qt::ItemIsUserCheckable | Qt::ItemIsSelectable ); - childItem->setData( 0, Qt::UserRole, "layer" ); + childItem->setData( 0, Qt::UserRole, QStringLiteral( "layer" ) ); childItem->setData( 0, Qt::UserRole + 1, l->id() ); treeItem->addChild( childItem ); diff --git a/src/server/qgsservercachefilter.cpp b/src/server/qgsservercachefilter.cpp index cb568ea86d9e..81c446431c5a 100644 --- a/src/server/qgsservercachefilter.cpp +++ b/src/server/qgsservercachefilter.cpp @@ -21,13 +21,11 @@ #include -//! Constructor -QgsServerCacheFilter::QgsServerCacheFilter( const QgsServerInterface *serverInterface ): - mServerInterface( serverInterface ) +QgsServerCacheFilter::QgsServerCacheFilter( const QgsServerInterface *serverInterface ) + : mServerInterface( serverInterface ) { } -//! Returns cached document QByteArray QgsServerCacheFilter::getCachedDocument( const QgsProject *project, const QgsServerRequest &request, const QString &key ) const { Q_UNUSED( project ); @@ -36,7 +34,6 @@ QByteArray QgsServerCacheFilter::getCachedDocument( const QgsProject *project, c return QByteArray(); } -//! Updates or inserts the document in cache bool QgsServerCacheFilter::setCachedDocument( const QDomDocument *doc, const QgsProject *project, const QgsServerRequest &request, const QString &key ) const { Q_UNUSED( doc ); @@ -46,7 +43,6 @@ bool QgsServerCacheFilter::setCachedDocument( const QDomDocument *doc, const Qgs return false; } -//! Deletes the cached document bool QgsServerCacheFilter::deleteCachedDocument( const QgsProject *project, const QgsServerRequest &request, const QString &key ) const { Q_UNUSED( project ); @@ -55,14 +51,12 @@ bool QgsServerCacheFilter::deleteCachedDocument( const QgsProject *project, cons return false; } -//! Deletes all cached documents for a QGIS project bool QgsServerCacheFilter::deleteCachedDocuments( const QgsProject *project ) const { Q_UNUSED( project ); return false; } -//! Returns cached image QByteArray QgsServerCacheFilter::getCachedImage( const QgsProject *project, const QgsServerRequest &request, const QString &key ) const { Q_UNUSED( project ); @@ -71,7 +65,6 @@ QByteArray QgsServerCacheFilter::getCachedImage( const QgsProject *project, cons return QByteArray(); } -//! Updates or inserts the image in cache bool QgsServerCacheFilter::setCachedImage( const QByteArray *img, const QgsProject *project, const QgsServerRequest &request, const QString &key ) const { Q_UNUSED( img ); @@ -81,7 +74,6 @@ bool QgsServerCacheFilter::setCachedImage( const QByteArray *img, const QgsProje return false; } -//! Deletes the cached image bool QgsServerCacheFilter::deleteCachedImage( const QgsProject *project, const QgsServerRequest &request, const QString &key ) const { Q_UNUSED( project ); @@ -90,7 +82,6 @@ bool QgsServerCacheFilter::deleteCachedImage( const QgsProject *project, const Q return false; } -//! Deletes all cached images for a QGIS project bool QgsServerCacheFilter::deleteCachedImages( const QgsProject *project ) const { Q_UNUSED( project ); diff --git a/src/server/qgsservercachefilter.h b/src/server/qgsservercachefilter.h index ac6100b165b6..e6efd86edc96 100644 --- a/src/server/qgsservercachefilter.h +++ b/src/server/qgsservercachefilter.h @@ -133,4 +133,4 @@ class SERVER_EXPORT QgsServerCacheFilter //! The registry definition typedef QMultiMap QgsServerCacheFilterMap; -#endif // QGSSERVERSECURITY_H +#endif // QGSSERVERCACHEPLUGIN_H diff --git a/src/server/qgsservercachemanager.cpp b/src/server/qgsservercachemanager.cpp index 16f77995afa3..98d7bd46005a 100644 --- a/src/server/qgsservercachemanager.cpp +++ b/src/server/qgsservercachemanager.cpp @@ -18,7 +18,6 @@ #include "qgsservercachemanager.h" -//! Returns cached document (or 0 if document not in cache) like capabilities QByteArray QgsServerCacheManager::getCachedDocument( const QgsProject *project, const QgsServerRequest &request, const QString &key ) const { QgsServerCacheFilterMap::const_iterator scIterator; @@ -33,7 +32,6 @@ QByteArray QgsServerCacheManager::getCachedDocument( const QgsProject *project, return QByteArray(); } -//! Updates or inserts the document in cache like capabilities bool QgsServerCacheManager::setCachedDocument( const QDomDocument *doc, const QgsProject *project, const QgsServerRequest &request, const QString &key ) const { QgsServerCacheFilterMap::const_iterator scIterator; @@ -47,7 +45,6 @@ bool QgsServerCacheManager::setCachedDocument( const QDomDocument *doc, const Qg return false; } -//! Deletes the cached document bool QgsServerCacheManager::deleteCachedDocument( const QgsProject *project, const QgsServerRequest &request, const QString &key ) const { QgsServerCacheFilterMap::const_iterator scIterator; @@ -61,7 +58,6 @@ bool QgsServerCacheManager::deleteCachedDocument( const QgsProject *project, con return false; } -//! Deletes all cached documents for a QGIS Project bool QgsServerCacheManager::deleteCachedDocuments( const QgsProject *project ) const { QgsServerCacheFilterMap::const_iterator scIterator; @@ -75,7 +71,6 @@ bool QgsServerCacheManager::deleteCachedDocuments( const QgsProject *project ) c return false; } -//! Returns cached image (or 0 if image not in cache) like tiles QByteArray QgsServerCacheManager::getCachedImage( const QgsProject *project, const QgsServerRequest &request, const QString &key ) const { QgsServerCacheFilterMap::const_iterator scIterator; @@ -90,7 +85,6 @@ QByteArray QgsServerCacheManager::getCachedImage( const QgsProject *project, con return QByteArray(); } -//! Updates or inserts the image in cache like tiles bool QgsServerCacheManager::setCachedImage( const QByteArray *img, const QgsProject *project, const QgsServerRequest &request, const QString &key ) const { QgsServerCacheFilterMap::const_iterator scIterator; @@ -104,7 +98,6 @@ bool QgsServerCacheManager::setCachedImage( const QByteArray *img, const QgsProj return false; } -//! Deletes the cached image bool QgsServerCacheManager::deleteCachedImage( const QgsProject *project, const QgsServerRequest &request, const QString &key ) const { QgsServerCacheFilterMap::const_iterator scIterator; @@ -118,7 +111,6 @@ bool QgsServerCacheManager::deleteCachedImage( const QgsProject *project, const return false; } -//! Deletes all cached images for a QGIS Project bool QgsServerCacheManager::deleteCachedImages( const QgsProject *project ) const { QgsServerCacheFilterMap::const_iterator scIterator; @@ -132,7 +124,6 @@ bool QgsServerCacheManager::deleteCachedImages( const QgsProject *project ) cons return false; } -//! Register a new access control filter void QgsServerCacheManager::registerServerCache( QgsServerCacheFilter *serverCache, int priority ) { mPluginsServerCaches->insert( priority, serverCache ); diff --git a/src/server/qgsservercachemanager.h b/src/server/qgsservercachemanager.h index da6e32a1270d..812e7babe8f7 100644 --- a/src/server/qgsservercachemanager.h +++ b/src/server/qgsservercachemanager.h @@ -49,21 +49,39 @@ class SERVER_EXPORT QgsServerCacheManager //! Constructor QgsServerCacheManager() { - mPluginsServerCaches = new QgsServerCacheFilterMap(); - mResolved = false; + mPluginsServerCaches.reset( new QgsServerCacheFilterMap() ); } - //! Constructor + //! Copy constructor QgsServerCacheManager( const QgsServerCacheManager © ) { - mPluginsServerCaches = new QgsServerCacheFilterMap( *copy.mPluginsServerCaches ); - mResolved = copy.mResolved; + if ( copy.mPluginsServerCaches ) + { + mPluginsServerCaches.reset( new QgsServerCacheFilterMap( *copy.mPluginsServerCaches ) ); + } + else + { + mPluginsServerCaches.reset( nullptr ); + } + } + //! Assignment operator + QgsServerCacheManager &operator=( const QgsServerCacheManager © ) + { + if ( copy.mPluginsServerCaches ) + { + mPluginsServerCaches.reset( new QgsServerCacheFilterMap( *copy.mPluginsServerCaches ) ); + } + else + { + mPluginsServerCaches.reset( nullptr ); + } + return *this; } ~QgsServerCacheManager() { - delete mPluginsServerCaches; + mPluginsServerCaches.reset(); } /** @@ -145,9 +163,7 @@ class SERVER_EXPORT QgsServerCacheManager private: //! The ServerCache plugins registry - QgsServerCacheFilterMap *mPluginsServerCaches = nullptr; - - bool mResolved; + std::unique_ptr mPluginsServerCaches = nullptr; }; #endif diff --git a/src/server/qgsserverinterface.h b/src/server/qgsserverinterface.h index a2bfee8ebaea..89e5e6ff6908 100644 --- a/src/server/qgsserverinterface.h +++ b/src/server/qgsserverinterface.h @@ -126,10 +126,14 @@ class SERVER_EXPORT QgsServerInterface * Register a server cache filter * \param serverCache the server cache to register * \param priority the priority used to order them + * \since QGIS 3.4 */ virtual void registerServerCache( QgsServerCacheFilter *serverCache SIP_TRANSFER, int priority = 0 ) = 0; - //! Gets the registered server cache filters + /** + * Gets the registered server cache filters + * \since QGIS 3.4 + */ virtual QgsServerCacheManager *cacheManager() const = 0; //! Returns an enrironment variable, used to pass environment variables to Python diff --git a/src/server/qgsserverinterfaceimpl.cpp b/src/server/qgsserverinterfaceimpl.cpp index ddbcdb8622ac..29aca9ac5318 100644 --- a/src/server/qgsserverinterfaceimpl.cpp +++ b/src/server/qgsserverinterfaceimpl.cpp @@ -29,10 +29,7 @@ QgsServerInterfaceImpl::QgsServerInterfaceImpl( QgsCapabilitiesCache *capCache, mRequestHandler = nullptr; #ifdef HAVE_SERVER_PYTHON_PLUGINS mAccessControls = new QgsAccessControl(); - mCacheManager = new QgsServerCacheManager(); -#else - mAccessControls = nullptr; - mCacheManager = nullptr; + mCacheManager.reset( new QgsServerCacheManager() ); #endif } @@ -46,6 +43,7 @@ QgsServerInterfaceImpl::~QgsServerInterfaceImpl() { #ifdef HAVE_SERVER_PYTHON_PLUGINS delete mAccessControls; + mCacheManager.reset(); #endif } diff --git a/src/server/qgsserverinterfaceimpl.h b/src/server/qgsserverinterfaceimpl.h index a8a7e4b973f5..3b31e2bc349e 100644 --- a/src/server/qgsserverinterfaceimpl.h +++ b/src/server/qgsserverinterfaceimpl.h @@ -60,14 +60,20 @@ class QgsServerInterfaceImpl : public QgsServerInterface */ QgsAccessControl *accessControls() const override { return mAccessControls; } - //! Register a server cache filter - void registerServerCache( QgsServerCacheFilter *serverCache, int priority = 0 ) override; + + /** + * Register a server cache filter + * \param serverCache the server cache to register + * \param priority the priority used to order them + * \since QGIS 3.4 + */ + void registerServerCache( QgsServerCacheFilter *serverCache SIP_TRANSFER, int priority = 0 ) override; /** * Gets the helper over all the registered server cache filters * \returns the server cache helper */ - QgsServerCacheManager *cacheManager() const override { return mCacheManager; } + QgsServerCacheManager *cacheManager() const override { return mCacheManager.get(); } QString getEnv( const QString &name ) const override; QString configFilePath() override { return mConfigFilePath; } @@ -84,7 +90,7 @@ class QgsServerInterfaceImpl : public QgsServerInterface QString mConfigFilePath; QgsServerFiltersMap mFilters; QgsAccessControl *mAccessControls = nullptr; - QgsServerCacheManager *mCacheManager = nullptr; + std::unique_ptr mCacheManager = nullptr; QgsCapabilitiesCache *mCapabilitiesCache = nullptr; QgsRequestHandler *mRequestHandler = nullptr; QgsServiceRegistry *mServiceRegistry = nullptr; diff --git a/src/server/services/wcs/qgswcsgetcapabilities.cpp b/src/server/services/wcs/qgswcsgetcapabilities.cpp index 74fe35db1c7d..105d0b4b0bde 100644 --- a/src/server/services/wcs/qgswcsgetcapabilities.cpp +++ b/src/server/services/wcs/qgswcsgetcapabilities.cpp @@ -45,7 +45,7 @@ namespace QgsWcs cache = accessControl->fillCacheKey( cacheKeyList ); QDomDocument doc; - QString cacheKey = cacheKeyList.join( QStringLiteral( "-" ) ); + QString cacheKey = cacheKeyList.join( '-' ); const QDomDocument *capabilitiesDocument = nullptr; QgsServerCacheManager *cacheManager = serverIface->cacheManager(); @@ -82,7 +82,7 @@ namespace QgsWcs } } - response.setHeader( "Content-Type", "text/xml; charset=utf-8" ); + response.setHeader( QStringLiteral( "Content-Type" ), QStringLiteral( "text/xml; charset=utf-8" ) ); response.write( capabilitiesDocument->toByteArray() ); } diff --git a/src/server/services/wfs/qgswfsgetcapabilities.cpp b/src/server/services/wfs/qgswfsgetcapabilities.cpp index 653c41e91b83..0dfa8cd371c6 100644 --- a/src/server/services/wfs/qgswfsgetcapabilities.cpp +++ b/src/server/services/wfs/qgswfsgetcapabilities.cpp @@ -49,7 +49,7 @@ namespace QgsWfs cache = accessControl->fillCacheKey( cacheKeyList ); QDomDocument doc; - QString cacheKey = cacheKeyList.join( QStringLiteral( "-" ) ); + QString cacheKey = cacheKeyList.join( '-' ); const QDomDocument *capabilitiesDocument = nullptr; QgsServerCacheManager *cacheManager = serverIface->cacheManager(); @@ -86,7 +86,7 @@ namespace QgsWfs } } - response.setHeader( "Content-Type", "text/xml; charset=utf-8" ); + response.setHeader( QStringLiteral( "Content-Type" ), QStringLiteral( "text/xml; charset=utf-8" ) ); response.write( capabilitiesDocument->toByteArray() ); } diff --git a/src/server/services/wfs/qgswfsgetcapabilities_1_0_0.cpp b/src/server/services/wfs/qgswfsgetcapabilities_1_0_0.cpp index 8ddb306e1877..474d53e2a29e 100644 --- a/src/server/services/wfs/qgswfsgetcapabilities_1_0_0.cpp +++ b/src/server/services/wfs/qgswfsgetcapabilities_1_0_0.cpp @@ -51,7 +51,7 @@ namespace QgsWfs cache = accessControl->fillCacheKey( cacheKeyList ); QDomDocument doc; - QString cacheKey = cacheKeyList.join( QStringLiteral( "-" ) ); + QString cacheKey = cacheKeyList.join( '-' ); const QDomDocument *capabilitiesDocument = nullptr; QgsServerCacheManager *cacheManager = serverIface->cacheManager(); @@ -88,7 +88,7 @@ namespace QgsWfs } } - response.setHeader( "Content-Type", "text/xml; charset=utf-8" ); + response.setHeader( QStringLiteral( "Content-Type" ), QStringLiteral( "text/xml; charset=utf-8" ) ); response.write( capabilitiesDocument->toByteArray() ); } diff --git a/src/server/services/wms/qgswmsgetcapabilities.cpp b/src/server/services/wms/qgswmsgetcapabilities.cpp index 7d6c2cc5d2e1..408fb6a504ff 100644 --- a/src/server/services/wms/qgswmsgetcapabilities.cpp +++ b/src/server/services/wms/qgswmsgetcapabilities.cpp @@ -106,7 +106,7 @@ namespace QgsWms QDomDocument doc; - QString cacheKey = cacheKeyList.join( QStringLiteral( "-" ) ); + QString cacheKey = cacheKeyList.join( '-' ); const QDomDocument *capabilitiesDocument = nullptr; QgsServerCacheManager *cacheManager = serverIface->cacheManager(); @@ -114,7 +114,7 @@ namespace QgsWms { QByteArray content; if ( cacheKeyList.count() == 2 ) - content = cacheManager->getCachedDocument( project, request, QStringLiteral( "" ) ); + content = cacheManager->getCachedDocument( project, request, QString() ); else if ( cacheKeyList.count() > 2 ) content = cacheManager->getCachedDocument( project, request, cacheKeyList.at( 3 ) ); @@ -144,9 +144,9 @@ namespace QgsWms { QByteArray content; if ( cacheKeyList.count() == 2 && - cacheManager->setCachedDocument( &doc, project, request, QStringLiteral( "" ) ) ) + cacheManager->setCachedDocument( &doc, project, request, QString() ) ) { - content = cacheManager->getCachedDocument( project, request, QStringLiteral( "" ) ); + content = cacheManager->getCachedDocument( project, request, QString() ); } else if ( cacheKeyList.count() > 2 && cacheManager->setCachedDocument( &doc, project, request, cacheKeyList.at( 3 ) ) ) diff --git a/src/server/services/wmts/qgswmts.cpp b/src/server/services/wmts/qgswmts.cpp index 080732588162..465efcee36ea 100644 --- a/src/server/services/wmts/qgswmts.cpp +++ b/src/server/services/wmts/qgswmts.cpp @@ -59,7 +59,7 @@ namespace QgsWmts Q_UNUSED( project ); QgsServerRequest::Parameters params = request.parameters(); - QString versionString = params.value( "VERSION" ); + QString versionString = params.value( QStringLiteral( "VERSION" ) ); // Set the default version if ( versionString.isEmpty() ) @@ -113,7 +113,7 @@ class QgsWmtsModule: public QgsServiceModule public: void registerSelf( QgsServiceRegistry ®istry, QgsServerInterface *serverIface ) override { - QgsDebugMsg( "WMTSModule::registerSelf called" ); + QgsDebugMsg( QStringLiteral( "WMTSModule::registerSelf called" ) ); registry.registerService( new QgsWmts::Service( serverIface ) ); } }; diff --git a/src/server/services/wmts/qgswmtsgetcapabilities.cpp b/src/server/services/wmts/qgswmtsgetcapabilities.cpp index 587b751f9e98..1f5630f65fb0 100644 --- a/src/server/services/wmts/qgswmtsgetcapabilities.cpp +++ b/src/server/services/wmts/qgswmtsgetcapabilities.cpp @@ -45,7 +45,7 @@ namespace QgsWmts cache = accessControl->fillCacheKey( cacheKeyList ); QDomDocument doc; - QString cacheKey = cacheKeyList.join( QStringLiteral( "-" ) ); + QString cacheKey = cacheKeyList.join( '-' ); const QDomDocument *capabilitiesDocument = nullptr; QgsServerCacheManager *cacheManager = serverIface->cacheManager(); @@ -82,7 +82,7 @@ namespace QgsWmts } } - response.setHeader( "Content-Type", "text/xml; charset=utf-8" ); + response.setHeader( QStringLiteral( "Content-Type" ), QStringLiteral( "text/xml; charset=utf-8" ) ); response.write( capabilitiesDocument->toByteArray() ); } @@ -128,7 +128,7 @@ namespace QgsWmts //Service type QDomElement typeElem = doc.createElement( QStringLiteral( "ows:ServiceType" ) ); - QDomText typeText = doc.createTextNode( "OGC WMTS" ); + QDomText typeText = doc.createTextNode( QStringLiteral( "OGC WMTS" ) ); typeElem.appendChild( typeText ); serviceElem.appendChild( typeElem ); @@ -353,9 +353,7 @@ namespace QgsWmts //transform the project native CRS into WGS84 QgsRectangle projRect = QgsServerProjectUtils::wmsExtent( *project ); QgsCoordinateReferenceSystem projCrs = project->crs(); - Q_NOWARN_DEPRECATED_PUSH - QgsCoordinateTransform exGeoTransform( projCrs, wgs84 ); - Q_NOWARN_DEPRECATED_POP + QgsCoordinateTransform exGeoTransform( projCrs, wgs84, project ); try { pLayer.wgs84BoundingRect = exGeoTransform.transformBoundingBox( projRect ); @@ -388,7 +386,7 @@ namespace QgsWmts QStringList wmtsPngGroupNameList = project->readListEntry( QStringLiteral( "WMTSPngLayers" ), QStringLiteral( "Group" ) ); QStringList wmtsJpegGroupNameList = project->readListEntry( QStringLiteral( "WMTSJpegLayers" ), QStringLiteral( "Group" ) ); - Q_FOREACH ( QString gName, wmtsGroupNameList ) + for ( QString gName : wmtsGroupNameList ) { QgsLayerTreeGroup *treeGroup = treeRoot->findGroup( gName ); if ( !treeGroup ) @@ -412,11 +410,13 @@ namespace QgsWmts for ( QgsLayerTreeLayer *layer : treeGroup->findLayers() ) { QgsMapLayer *l = layer->layer(); + if ( !l ) + { + continue; + } //transform the layer native CRS into WGS84 QgsCoordinateReferenceSystem layerCrs = l->crs(); - Q_NOWARN_DEPRECATED_PUSH - QgsCoordinateTransform exGeoTransform( layerCrs, wgs84 ); - Q_NOWARN_DEPRECATED_POP + QgsCoordinateTransform exGeoTransform( layerCrs, wgs84, project ); try { wgs84BoundingRect.combineExtentWith( exGeoTransform.transformBoundingBox( l->extent() ) ); @@ -447,7 +447,7 @@ namespace QgsWmts QStringList wmtsPngLayerIdList = project->readListEntry( QStringLiteral( "WMTSPngLayers" ), QStringLiteral( "Layer" ) ); QStringList wmtsJpegLayerIdList = project->readListEntry( QStringLiteral( "WMTSJpegLayers" ), QStringLiteral( "Layer" ) ); - Q_FOREACH ( QString lId, wmtsLayerIdList ) + for ( QString lId : wmtsLayerIdList ) { QgsMapLayer *l = project->mapLayer( lId ); if ( !l ) @@ -472,9 +472,7 @@ namespace QgsWmts //transform the layer native CRS into WGS84 QgsCoordinateReferenceSystem layerCrs = l->crs(); - Q_NOWARN_DEPRECATED_PUSH - QgsCoordinateTransform exGeoTransform( layerCrs, wgs84 ); - Q_NOWARN_DEPRECATED_POP + QgsCoordinateTransform exGeoTransform( layerCrs, wgs84, project ); try { pLayer.wgs84BoundingRect = exGeoTransform.transformBoundingBox( l->extent() ); @@ -503,7 +501,7 @@ namespace QgsWmts elem.appendChild( formatElem ); }; - Q_FOREACH ( layerDef wmtsLayer, wmtsLayers ) + for ( layerDef wmtsLayer : wmtsLayers ) { if ( wmtsLayer.id.isEmpty() ) continue; @@ -550,14 +548,12 @@ namespace QgsWmts for ( ; tmsIt != tmsList.end(); ++tmsIt ) { tileMatrixSet &tms = *tmsIt; - if ( tms.ref == "EPSG:4326" ) + if ( tms.ref == QLatin1String( "EPSG:4326" ) ) continue; QgsRectangle rect; QgsCoordinateReferenceSystem crs = QgsCoordinateReferenceSystem::fromOgcWmsCrs( tms.ref ); - Q_NOWARN_DEPRECATED_PUSH - QgsCoordinateTransform exGeoTransform( wgs84, crs ); - Q_NOWARN_DEPRECATED_POP + QgsCoordinateTransform exGeoTransform( wgs84, crs, project ); try { rect = exGeoTransform.transformBoundingBox( wmtsLayer.wgs84BoundingRect ); @@ -593,7 +589,7 @@ namespace QgsWmts layerStyleElem.appendChild( layerStyleTitleElem ); layerElem.appendChild( layerStyleElem ); - Q_FOREACH ( QString format, wmtsLayer.formats ) + for ( QString format : wmtsLayer.formats ) { QDomElement layerFormatElem = doc.createElement( QStringLiteral( "Format" ) ); QDomText layerFormatText = doc.createTextNode( format ); @@ -614,13 +610,11 @@ namespace QgsWmts for ( ; tmsIt != tmsList.end(); ++tmsIt ) { tileMatrixSet &tms = *tmsIt; - if ( tms.ref != "EPSG:4326" ) + if ( tms.ref != QLatin1String( "EPSG:4326" ) ) { QgsRectangle rect; QgsCoordinateReferenceSystem crs = QgsCoordinateReferenceSystem::fromOgcWmsCrs( tms.ref ); - Q_NOWARN_DEPRECATED_PUSH - QgsCoordinateTransform exGeoTransform( wgs84, crs ); - Q_NOWARN_DEPRECATED_POP + QgsCoordinateTransform exGeoTransform( wgs84, crs, project ); try { rect = exGeoTransform.transformBoundingBox( wmtsLayer.wgs84BoundingRect ); diff --git a/src/server/services/wmts/qgswmtsgettile.cpp b/src/server/services/wmts/qgswmtsgettile.cpp index f650faf9266c..d1ba058e1012 100644 --- a/src/server/services/wmts/qgswmtsgettile.cpp +++ b/src/server/services/wmts/qgswmtsgettile.cpp @@ -40,28 +40,28 @@ namespace QgsWmts if ( accessControl ) cache = accessControl->fillCacheKey( cacheKeyList ); - QString cacheKey = cacheKeyList.join( QStringLiteral( "-" ) ); + QString cacheKey = cacheKeyList.join( '-' ); QgsServerCacheManager *cacheManager = serverIface->cacheManager(); if ( cacheManager && cache ) { QString contentType = params.value( QStringLiteral( "FORMAT" ) ); QString saveFormat; std::unique_ptr image; - if ( contentType == "image/jpeg" ) + if ( contentType == QLatin1String( "image/jpeg" ) ) { - saveFormat = "JPEG"; + saveFormat = QStringLiteral( "JPEG" ); image = qgis::make_unique( 256, 256, QImage::Format_RGB32 ); } else { - saveFormat = "PNG"; + saveFormat = QStringLiteral( "PNG" ); image = qgis::make_unique( 256, 256, QImage::Format_ARGB32_Premultiplied ); } QByteArray content = cacheManager->getCachedImage( project, request, cacheKey ); if ( !content.isEmpty() && image->loadFromData( content ) ) { - response.setHeader( "Content-Type", contentType ); + response.setHeader( QStringLiteral( "Content-Type" ), contentType ); image->save( response.io(), qPrintable( saveFormat ) ); return; } diff --git a/src/server/services/wmts/qgswmtsutils.cpp b/src/server/services/wmts/qgswmtsutils.cpp index b325285bde6c..3aaea8875078 100644 --- a/src/server/services/wmts/qgswmtsutils.cpp +++ b/src/server/services/wmts/qgswmtsutils.cpp @@ -33,14 +33,14 @@ namespace QgsWmts { namespace { - QMap< QString, double> populateInchesPerUnit(); + QMap< QgsUnitTypes::DistanceUnit, double> populateInchesPerUnit(); QMap< QString, tileMatrixInfo> populateTileMatrixInfoMap(); QgsCoordinateReferenceSystem wgs84 = QgsCoordinateReferenceSystem::fromOgcWmsCrs( GEO_EPSG_CRS_AUTHID ); int DOTS_PER_INCH = 72; double METERS_PER_INCH = 0.02540005080010160020; - QMap< QString, double> INCHES_PER_UNIT = populateInchesPerUnit(); + QMap< QgsUnitTypes::DistanceUnit, double> INCHES_PER_UNIT = populateInchesPerUnit(); int tileWidth = 256; int tileHeight = 256; @@ -98,7 +98,7 @@ namespace QgsWmts return QgsRectangle( d[0], d[1], d[2], d[3] ); } - tileMatrixInfo getTileMatrixInfo( const QString &crsStr ) + tileMatrixInfo getTileMatrixInfo( const QString &crsStr, const QgsProject *project ) { if ( tileMatrixInfoMap.contains( crsStr ) ) return tileMatrixInfoMap[crsStr]; @@ -107,9 +107,7 @@ namespace QgsWmts tmi.ref = crsStr; QgsCoordinateReferenceSystem crs = QgsCoordinateReferenceSystem::fromOgcWmsCrs( crsStr ); - Q_NOWARN_DEPRECATED_PUSH - QgsCoordinateTransform crsTransform( wgs84, crs ); - Q_NOWARN_DEPRECATED_POP + QgsCoordinateTransform crsTransform( wgs84, crs, project ); try { tmi.extent = crsTransform.transformBoundingBox( crs.bounds() ); @@ -119,25 +117,7 @@ namespace QgsWmts Q_UNUSED( cse ); } - QgsUnitTypes::DistanceUnit mapUnits = crs.mapUnits(); - if ( mapUnits == QgsUnitTypes::DistanceMeters ) - tmi.unit = "m"; - else if ( mapUnits == QgsUnitTypes::DistanceKilometers ) - tmi.unit = "km"; - else if ( mapUnits == QgsUnitTypes::DistanceFeet ) - tmi.unit = "ft"; - else if ( mapUnits == QgsUnitTypes::DistanceNauticalMiles ) - tmi.unit = "nmi"; - else if ( mapUnits == QgsUnitTypes::DistanceYards ) - tmi.unit = "yd"; - else if ( mapUnits == QgsUnitTypes::DistanceMiles ) - tmi.unit = "mi"; - else if ( mapUnits == QgsUnitTypes::DistanceDegrees ) - tmi.unit = "dd"; - else if ( mapUnits == QgsUnitTypes::DistanceCentimeters ) - tmi.unit = "cm"; - else if ( mapUnits == QgsUnitTypes::DistanceMillimeters ) - tmi.unit = "mm"; + tmi.unit = crs.mapUnits(); // calculate tile matrix scale denominator double scaleDenominator = 0.0; @@ -158,7 +138,7 @@ namespace QgsWmts QList< tileMatrix > tileMatrixList; double scaleDenominator = tmi.scaleDenominator; QgsRectangle extent = tmi.extent; - QString unit = tmi.unit; + QgsUnitTypes::DistanceUnit unit = tmi.unit; while ( scaleDenominator >= minScale ) { @@ -206,7 +186,7 @@ namespace QgsWmts // get min and max scales if ( !scaleList.isEmpty() ) { - Q_FOREACH ( const QString &scaleText, scaleList ) + for ( const QString &scaleText : scaleList ) { double scaleValue = scaleText.toDouble(); if ( scale == -1.0 ) @@ -233,9 +213,9 @@ namespace QgsWmts double minScale = getProjectMinScale( project ); QStringList crsList = QgsServerProjectUtils::wmsOutputCrsList( *project ); - Q_FOREACH ( const QString &crsStr, crsList ) + for ( const QString &crsStr : crsList ) { - tileMatrixInfo tmi = getTileMatrixInfo( crsStr ); + tileMatrixInfo tmi = getTileMatrixInfo( crsStr, project ); if ( tmi.scaleDenominator > 0.0 ) { tmsList.append( getTileMatrixSet( tmi, minScale ) ); @@ -281,7 +261,7 @@ namespace QgsWmts if ( !wmtsGroupNameList.isEmpty() ) { QgsLayerTreeGroup *treeRoot = project->layerTreeRoot(); - Q_FOREACH ( QString gName, wmtsGroupNameList ) + for ( QString gName : wmtsGroupNameList ) { QgsLayerTreeGroup *treeGroup = treeRoot->findGroup( gName ); if ( !treeGroup ) @@ -301,7 +281,7 @@ namespace QgsWmts #ifdef HAVE_SERVER_PYTHON_PLUGINS QgsAccessControl *accessControl = serverIface->accessControls(); #endif - Q_FOREACH ( QString lId, wmtsLayerIdList ) + for ( QString lId : wmtsLayerIdList ) { QgsMapLayer *l = project->mapLayer( lId ); if ( !l ) @@ -361,7 +341,7 @@ namespace QgsWmts throw QgsRequestNotWellFormedException( QStringLiteral( "TileMatrixSet is unknown" ) ); } - tileMatrixInfo tmi = getTileMatrixInfo( tms_ref ); + tileMatrixInfo tmi = getTileMatrixInfo( tms_ref, project ); if ( tmi.scaleDenominator == 0.0 ) { throw QgsRequestNotWellFormedException( QStringLiteral( "TileMatrixSet is unknown" ) ); @@ -489,21 +469,18 @@ namespace QgsWmts namespace { - QMap< QString, double> populateInchesPerUnit() - { - QMap< QString, double> m; - m["inches"] = 1.0; - m["ft"] = 12.0; - m["mi"] = 63360.0; - m["m"] = 39.37; - m["km"] = 39370.0; - m["dd"] = 4374754.0; - m["yd"] = 36.0; - m["in"] = m["inches"]; - m["degrees"] = m["dd"]; - m["nmi"] = 1852.0 * m["m"]; - m["cm"] = m["m"] / 100.0; - m["mm"] = m["m"] / 1000.0; + QMap< QgsUnitTypes::DistanceUnit, double> populateInchesPerUnit() + { + QMap< QgsUnitTypes::DistanceUnit, double> m; + m[ QgsUnitTypes::DistanceMeters ] = 39.37; + m[ QgsUnitTypes::DistanceFeet ] = 12.0; + m[ QgsUnitTypes::DistanceYards ] = 36.0; + m[ QgsUnitTypes::DistanceMiles ] = 63360.0; + m[ QgsUnitTypes::DistanceDegrees ] = 4374754.0; + m[ QgsUnitTypes::DistanceKilometers ] = m[ QgsUnitTypes::DistanceMeters ] * 1000.0; + m[ QgsUnitTypes::DistanceNauticalMiles ] = m[ QgsUnitTypes::DistanceMeters ] * 1852.0; + m[ QgsUnitTypes::DistanceCentimeters ] = m[ QgsUnitTypes::DistanceMeters ] / 100.0; + m[ QgsUnitTypes::DistanceMillimeters ] = m[ QgsUnitTypes::DistanceMeters ] / 1000.0; return m; } @@ -511,19 +488,21 @@ namespace QgsWmts { QMap< QString, tileMatrixInfo> m; + // Tile matrix information + // to build tile matrix set like Google Mercator or TMS tileMatrixInfo tmi3857; - tmi3857.ref = "EPSG:3857"; + tmi3857.ref = QStringLiteral( "EPSG:3857" ); tmi3857.extent = QgsRectangle( -20037508.3427892480, -20037508.3427892480, 20037508.3427892480, 20037508.3427892480 ); tmi3857.scaleDenominator = 559082264.0287179; - tmi3857.unit = "m"; + tmi3857.unit = QgsUnitTypes::DistanceMeters; m[tmi3857.ref] = tmi3857; tileMatrixInfo tmi4326; - tmi4326.ref = "EPSG:4326"; + tmi4326.ref = QStringLiteral( "EPSG:4326" ); tmi4326.extent = QgsRectangle( -180, -90, 180, 90 ); tmi4326.scaleDenominator = 279541132.0143588675418869; - tmi4326.unit = "dd"; + tmi4326.unit = QgsUnitTypes::DistanceDegrees; m[tmi4326.ref] = tmi4326; return m; diff --git a/src/server/services/wmts/qgswmtsutils.h b/src/server/services/wmts/qgswmtsutils.h index b40162367e96..c856d4e564ba 100644 --- a/src/server/services/wmts/qgswmtsutils.h +++ b/src/server/services/wmts/qgswmtsutils.h @@ -39,24 +39,24 @@ namespace QgsWmts QgsRectangle extent; - double scaleDenominator; + double scaleDenominator = 0.0; - QString unit; + QgsUnitTypes::DistanceUnit unit; }; struct tileMatrix { - double resolution; + double resolution = 0.0; - double scaleDenominator; + double scaleDenominator = 0.0; - int col; + int col = 0; - int row; + int row = 0; - double left; + double left = 0.0; - double top; + double top = 0.0; }; struct tileMatrixSet @@ -65,7 +65,7 @@ namespace QgsWmts QgsRectangle extent; - QString unit; + QgsUnitTypes::DistanceUnit unit; QList< tileMatrix > tileMatrixList; }; @@ -82,7 +82,7 @@ namespace QgsWmts QStringList formats; - bool queryable; + bool queryable = false; }; /** @@ -106,7 +106,7 @@ namespace QgsWmts const QString GML_NAMESPACE = QStringLiteral( "http://www.opengis.net/gml" ); const QString OWS_NAMESPACE = QStringLiteral( "http://www.opengis.net/ows/1.1" ); - tileMatrixInfo getTileMatrixInfo( const QString &crsStr ); + tileMatrixInfo getTileMatrixInfo( const QString &crsStr, const QgsProject *project ); tileMatrixSet getTileMatrixSet( tileMatrixInfo tmi, double minScale ); double getProjectMinScale( const QgsProject *project ); QList< tileMatrixSet > getTileMatrixSetList( const QgsProject *project ); From 2075141af90833da4ec90aae2c55dcd7e8fbb21a Mon Sep 17 00:00:00 2001 From: rldhont Date: Fri, 3 Aug 2018 15:54:00 +0200 Subject: [PATCH 17/33] [Server] Fixing tests --- tests/src/python/test_qgsserver_wms.py | 2 + .../WMS_GetMap_ContextRendering_mask.png | Bin 0 -> 4440 bytes .../WMS_GetMap_LabelingSettings_mask.png | Bin 0 -> 2684 bytes .../WMS_GetPrint_Highlight_mask.png | Bin 95546 -> 96480 bytes .../getcapabilities_without_map_param.txt | 246 ++ .../project_groups.qgs | 3166 ++++++++++++----- 6 files changed, 2563 insertions(+), 851 deletions(-) create mode 100644 tests/testdata/control_images/qgis_server/WMS_GetMap_ContextRendering/WMS_GetMap_ContextRendering_mask.png create mode 100644 tests/testdata/control_images/qgis_server/WMS_GetMap_LabelingSettings/WMS_GetMap_LabelingSettings_mask.png create mode 100644 tests/testdata/qgis_server/getcapabilities_without_map_param.txt diff --git a/tests/src/python/test_qgsserver_wms.py b/tests/src/python/test_qgsserver_wms.py index 0360039a7560..0681eaf3f6ae 100644 --- a/tests/src/python/test_qgsserver_wms.py +++ b/tests/src/python/test_qgsserver_wms.py @@ -123,7 +123,9 @@ def wms_request_compare_project(self, request, extra=None, reference_file=None): self.assertXMLEqual(response, expected, msg="request %s failed.\nQuery: %s\nExpected file: %s\nResponse:\n%s" % (query_string, request, reference_path, response.decode('utf-8'))) def test_wms_getcapabilities_project(self): + """WMS GetCapabilities without map parameter""" self.wms_request_compare_project('GetCapabilities') + # reference_file='getcapabilities_without_map_param' could be the right response def wms_inspire_request_compare(self, request): """WMS INSPIRE tests""" diff --git a/tests/testdata/control_images/qgis_server/WMS_GetMap_ContextRendering/WMS_GetMap_ContextRendering_mask.png b/tests/testdata/control_images/qgis_server/WMS_GetMap_ContextRendering/WMS_GetMap_ContextRendering_mask.png new file mode 100644 index 0000000000000000000000000000000000000000..53f44ed121baaab844e8367ddc952abc0af53055 GIT binary patch literal 4440 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�!l`YvV-+&C@XcK{1}_Uu^($ikNLQfRa1IYu_0 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 aI%BP++6r}zntXc5B~~jJa_9g2+y4Mz1DX8* literal 95546 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 + + + WMS + QGIS TestProject + + + infoMapAccessService + + + + + Alessandro Pasotti + QGIS dev team + + elpaso@itopen.it + + conditions unknown + None + + + + + text/xml + + + + + + + + + + image/jpeg + image/png + image/png; mode=16bit + image/png; mode=8bit + image/png; mode=1bit + application/dxf + + + + + + + + + + text/plain + text/html + text/xml + application/vnd.ogc.gml + application/vnd.ogc.gml/3.1.1 + + + + + + + + + + image/jpeg + image/png + + + + + + + + + + text/xml + + + + + + + + + + text/xml + + + + + + + + + + + XML + + + + QGIS Test Project + QGIS Test Project + CRS:84 + EPSG:4326 + EPSG:3857 + + 8.20315 + 8.20416 + 44.9012 + 44.9016 + + + + QGIS Test Project + + infoMapAccessService + + + layer_with_short_name + A Layer with a short name + A Layer with an abstract + CRS:84 + EPSG:4326 + EPSG:3857 + + 8.20346 + 8.20355 + 44.9014 + 44.9015 + + + + + + + testlayer èé + A test vector layer + A test vector layer with unicode òà + CRS:84 + EPSG:4326 + EPSG:3857 + + 8.20346 + 8.20355 + 44.9014 + 44.9015 + + + + + + + group_name + Group title + Group abstract + CRS:84 + EPSG:4326 + EPSG:3857 + + 8.20346 + 8.20355 + 44.9014 + 44.9015 + + + + + testlayer2 + testlayer2 + CRS:84 + EPSG:4326 + EPSG:3857 + + 8.20346 + 8.20355 + 44.9014 + 44.9015 + + + + + + + + groupwithoutshortname + groupwithoutshortname + CRS:84 + EPSG:4326 + EPSG:3857 + + 8.20346 + 8.20355 + 44.9014 + 44.9015 + + + + + testlayer3 + testlayer3 + CRS:84 + EPSG:4326 + EPSG:3857 + + 8.20346 + 8.20355 + 44.9014 + 44.9015 + + + + + + + + + diff --git a/tests/testdata/qgis_server_accesscontrol/project_groups.qgs b/tests/testdata/qgis_server_accesscontrol/project_groups.qgs index ef083697518d..00a255d0d18b 100644 --- a/tests/testdata/qgis_server_accesscontrol/project_groups.qgs +++ b/tests/testdata/qgis_server_accesscontrol/project_groups.qgs @@ -2,50 +2,66 @@ QGIS Server Hello World - - - + + + - +proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs - 100086 - 0 - USER:100086 - * Generated CRS (+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs) + +proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs + 3857 + 3857 + EPSG:3857 + WGS 84 / Pseudo-Mercator merc - + WGS84 false - + - + - + - + - + - + + + + + + + + + + + + + + + + + - + - + - + @@ -59,108 +75,132 @@ country20131022151106556 Country_copy20161127151800736 country20170328164317226 + Country_07a7a712_8bf3_4cb0_abde_58338f73a4b2 + Country_Labels_67649087_4e95_4483_9c32_a9e14b8360db + Country_Diagrams_208edc6e_7828_426b_a7d8_c2383f10761d - + - - - - - - - - + + + + + + + + + + + meters - -30425236.72921397164463997 - -10846058.0813884325325489 - 29667752.81264735385775566 - 14979028.33329574391245842 + -19299402.19027414172887802 + -18237547.54576336964964867 + 19536700.87085317820310593 + 39423933.74873916804790497 0 - +proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs - 100086 - 0 - USER:100086 - * Generated CRS (+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs) + +proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs + 3857 + 3857 + EPSG:3857 + WGS 84 / Pseudo-Mercator merc - + WGS84 false 0 - - + - + -19619892.68012013286352158 -10327100.34232237376272678 19972134.91854240000247955 18415866.31293442100286484 - Country_copy20161127151800736 + Country_07a7a712_8bf3_4cb0_abde_58338f73a4b2 dbname='./helloworld.db' table="country" (geom) sql= - Country_Labels + Country copier +proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs @@ -192,7 +232,7 @@ - true + false @@ -203,54 +243,56 @@ - - - + + + + - - + + - - + + - + - - - - - - - - - - - - - - - - - + + + + - + - + + + + + + + + + - + + + + + + + + + - - @@ -263,13 +305,10 @@ - - - @@ -279,35 +318,29 @@ - - - - - + + - + - + - - - + - + - @@ -318,16 +351,12 @@ - + - - - - @@ -335,7 +364,6 @@ - @@ -352,16 +380,11 @@ - - - - - @@ -374,8 +397,6 @@ - - @@ -385,16 +406,11 @@ - - - - - @@ -403,188 +419,1633 @@ - - - - - - - + + + - - - - 0 0 0 - - - - + name + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - . - - - - - - - - - - - - - - . + - 0 - - 0 - generatedlayout + tablayout + + - + - - - - - - - name - - 2 + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 0 + 0 + name + + + + + + + + + + + 0 + tablayout + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 0 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ../../../../../.. + + 0 + + + 0 + generatedlayout + + + + + + + + + + + + "name" + + + + + -19619892.68012013286352158 + -10327100.34232237376272678 + 19972134.91854240000247955 + 18415866.31293442100286484 + + Country_Diagrams_208edc6e_7828_426b_a7d8_c2383f10761d + dbname='./helloworld.db' table="country" (geom) sql= + + + + Country_Diagrams copier + + + +proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs + 3857 + 3857 + EPSG:3857 + WGS 84 / Pseudo-Mercator + merc + WGS84 + false + + + + + + + + + + + + + + + + 0 + 0 + + + + + false + + + + + spatialite + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 0 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + . + + 0 + + + 0 + generatedlayout + + + + + + + + + name + + + + + -19619892.68012013286352158 + -10327100.34232237376272678 + 19972134.91854240000247955 + 18415866.31293442100286484 + + Country_Labels_67649087_4e95_4483_9c32_a9e14b8360db + dbname='./helloworld.db' table="country" (geom) sql= + + + + Country_Labels copier + + + +proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs + 3857 + 3857 + EPSG:3857 + WGS 84 / Pseudo-Mercator + merc + WGS84 + false + + + + + + + + + + + + + + + + 0 + 0 + + + + + false + + + + + spatialite + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 0 + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + . + + + + + + + + + + + + + + . + + 0 + + + 0 + generatedlayout + + + + + + + + + + name + + 2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 0 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ../../../../../.. + + 0 + + + 0 + generatedlayout + + + + + + + + + + + + name + + + + + -19619892.68012013286352158 + -10327100.34232237376272678 + 19972134.91854240000247955 + 18415866.31293442100286484 + + Country_copy20161127151800736 + dbname='./helloworld.db' table="country" (geom) sql= + + + + Country_Labels + + + +proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs + 3857 + 3857 + EPSG:3857 + WGS 84 / Pseudo-Mercator + merc + WGS84 + false + + + + + + + + + + + + + + + + 0 + 0 + + + + + false + + + + + spatialite + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 0 + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + . + + + + + + + + + + + + + + . + + 0 + + + 0 + generatedlayout + + + + + + + + + + name + + 2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -592,18 +2053,18 @@ def my_form_open(dialog, layer, feature): 0 0 1 - - + + - + - + @@ -624,31 +2085,31 @@ def my_form_open(dialog, layer, feature): - - + + - - + + - - + + - - + + - + - ../../../../../.. @@ -675,7 +2136,7 @@ def my_form_open(dialog, layer, feature): 0 generatedlayout - + @@ -688,7 +2149,7 @@ def my_form_open(dialog, layer, feature): name - + -14746250.07513097859919071 -112075.42807669920148328 @@ -743,14 +2204,14 @@ def my_form_open(dialog, layer, feature): - - + + - + - + @@ -764,13 +2225,13 @@ def my_form_open(dialog, layer, feature): - + @@ -784,9 +2245,9 @@ def my_form_open(dialog, layer, feature): @@ -802,18 +2263,18 @@ def my_form_open(dialog, layer, feature): 0 0 1 - - + + - + - + @@ -841,36 +2302,36 @@ def my_form_open(dialog, layer, feature): - - - + + + - - - + + + - - - + + + - - - + + + - + - ../../../../../.. @@ -897,12 +2358,12 @@ def my_form_open(dialog, layer, feature): 0 generatedlayout - - - + + + - - + + @@ -917,7 +2378,7 @@ def my_form_open(dialog, layer, feature): "pkuid" - + -2465695.66895584994927049 80258.53580146089370828 @@ -972,14 +2433,14 @@ def my_form_open(dialog, layer, feature): - - + + - + - + @@ -993,13 +2454,13 @@ def my_form_open(dialog, layer, feature): - + @@ -1013,9 +2474,9 @@ def my_form_open(dialog, layer, feature): @@ -1031,18 +2492,18 @@ def my_form_open(dialog, layer, feature): 0 0 1 - - + + - + - + @@ -1070,31 +2531,31 @@ def my_form_open(dialog, layer, feature): - - - + + + - - - + + + - - - + + + - - - + + + - + ../../../../../.. @@ -1121,12 +2582,12 @@ def my_form_open(dialog, layer, feature): 0 generatedlayout - - - + + + - - + + @@ -1141,7 +2602,7 @@ def my_form_open(dialog, layer, feature): "pkuid" - + -14746250.07513097859919071 -112075.42807669920148328 @@ -1196,14 +2657,14 @@ def my_form_open(dialog, layer, feature): - - + + - + - + @@ -1217,13 +2678,13 @@ def my_form_open(dialog, layer, feature): - + @@ -1237,9 +2698,9 @@ def my_form_open(dialog, layer, feature): @@ -1255,18 +2716,18 @@ def my_form_open(dialog, layer, feature): 0 0 1 - - + + - + - + @@ -1294,31 +2755,31 @@ def my_form_open(dialog, layer, feature): - - - + + + - - - + + + - - - + + + - - - + + + - + ../../../../../.. @@ -1345,12 +2806,12 @@ def my_form_open(dialog, layer, feature): 0 generatedlayout - - - + + + - - + + @@ -1365,7 +2826,7 @@ def my_form_open(dialog, layer, feature): "pkuid" - + -19619892.68012013286352158 -10327100.34232237376272678 @@ -1420,22 +2881,22 @@ def my_form_open(dialog, layer, feature): - - + + - + - - + + - - + + - - + + @@ -1447,7 +2908,7 @@ def my_form_open(dialog, layer, feature): - + @@ -1460,7 +2921,7 @@ def my_form_open(dialog, layer, feature): - + @@ -1625,24 +3086,24 @@ def my_form_open(dialog, layer, feature): - + - + - + - - + + - + 0 tablayout @@ -1655,19 +3116,19 @@ def my_form_open(dialog, layer, feature): - + - - + + - - + + - - + + @@ -1679,7 +3140,7 @@ def my_form_open(dialog, layer, feature): - + @@ -1688,7 +3149,7 @@ def my_form_open(dialog, layer, feature): - + @@ -1700,7 +3161,7 @@ def my_form_open(dialog, layer, feature): - + @@ -1713,7 +3174,7 @@ def my_form_open(dialog, layer, feature): - + @@ -1878,24 +3339,24 @@ def my_form_open(dialog, layer, feature): - + - + - + - - + + - + 0 tablayout @@ -1909,10 +3370,10 @@ def my_form_open(dialog, layer, feature): - + - + @@ -1926,13 +3387,13 @@ def my_form_open(dialog, layer, feature): - + @@ -1946,9 +3407,9 @@ def my_form_open(dialog, layer, feature): @@ -1959,20 +3420,20 @@ def my_form_open(dialog, layer, feature): - - - - + + + + - - - + + + @@ -1981,18 +3442,18 @@ def my_form_open(dialog, layer, feature): 0 0 1 - - + + - + - + @@ -2013,27 +3474,27 @@ def my_form_open(dialog, layer, feature): - - + + - - + + - - + + - - + + - + ../../../../../.. @@ -2044,7 +3505,7 @@ def my_form_open(dialog, layer, feature): 0 generatedlayout - + @@ -2057,7 +3518,7 @@ def my_form_open(dialog, layer, feature): "name" - + -19619892.68012013286352158 -10327100.34232237376272678 @@ -2112,14 +3573,14 @@ def my_form_open(dialog, layer, feature): - - + + - + - + @@ -2133,9 +3594,9 @@ def my_form_open(dialog, layer, feature): @@ -2152,19 +3613,19 @@ def my_form_open(dialog, layer, feature): 0 0 1 - - + + - - + + - + @@ -2185,34 +3646,34 @@ def my_form_open(dialog, layer, feature): - - + + - - + + - - + + - - + + - + - - + . 0 @@ -2246,7 +3707,7 @@ def my_form_open(dialog, layer, feature): name - + -29.99999999999666755 29.99999999999666755 @@ -2302,11 +3763,11 @@ def my_form_open(dialog, layer, feature): - - + + - + None @@ -2323,12 +3784,12 @@ def my_form_open(dialog, layer, feature): - + 0 - + -14746250.07513097859919071 -112075.42807669920148328 @@ -2383,14 +3844,14 @@ def my_form_open(dialog, layer, feature): - - + + - + - + @@ -2404,13 +3865,13 @@ def my_form_open(dialog, layer, feature): - + @@ -2424,9 +3885,9 @@ def my_form_open(dialog, layer, feature): @@ -2439,18 +3900,18 @@ def my_form_open(dialog, layer, feature): 0 0 1 - - + + - + - + @@ -2478,31 +3939,31 @@ def my_form_open(dialog, layer, feature): - - - + + + - - - + + + - - - + + + - - - + + + - + ../../../../../.. @@ -2513,12 +3974,12 @@ def my_form_open(dialog, layer, feature): 0 generatedlayout - - - + + + - - + + @@ -2533,7 +3994,7 @@ def my_form_open(dialog, layer, feature): COALESCE( "pkuid", '<NULL>' ) - + 1000 2000 @@ -2588,14 +4049,14 @@ def my_form_open(dialog, layer, feature): - - + + - + - + @@ -2616,9 +4077,9 @@ def my_form_open(dialog, layer, feature): @@ -2631,18 +4092,18 @@ def my_form_open(dialog, layer, feature): 0 0 1 - - + + - + - + @@ -2670,31 +4131,31 @@ def my_form_open(dialog, layer, feature): - - - + + + - - - + + + - - - + + + - - - + + + - + ../../../../../.. @@ -2708,98 +4169,32 @@ def my_form_open(dialog, layer, feature): - - - - - "name" - - - - - - - - - - - - - - - - - true - - CountryGroup - - - hello20131022151106574 - - - - 50 - false - 0 - false - true - 30 - 16 - false - true - - - - 255 - - - true - - - 1 - - true - - 1 - 0 - 1 - 1 - 1 - - - false - - - 1 - +proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs - EPSG:3857 - 3857 - - - 255 - 255 - 255 - 246 - 108 - 255 - 128 - - conditions unknown - - dem20150730091219559 - - false - true - - true - - CountryGroup - - - hello20131022151106574 - - - + + + + + "name" + + + + + + + + + + + + + + + + + + + + + Hello_SubsetString_copy20160222085231770 Hello_copy20150804164427541 @@ -2807,120 +4202,189 @@ def my_form_open(dialog, layer, feature): hello20131022151106574 points20150803121107046 - - - false - - - - - - - - - - meters - m2 - - - Stéphane Brunner - Simple test app. - - D - true - 2 - -20609693.37008669599890709 -11055006.82298868149518967 20961935.60850896313786507 19143772.79360072687268257 - - 2000 - 1 - - - 1 - days - 1 - 0 - 0 - 0 - + false + + + + + + + true + - + points20150803121107046 - + points20150803121107046 - + points20150803121107046 - + + + + + + 0 + false + false + false + 50 + true + true + 16 + 30 + false - - EPSG:3857 - EPSG:4326 - + + + false + 5000 + + 0 + 1 + 1 + 1 + 1 + + + + meters + m2 + + Stéphane Brunner 5000 - None + + + true + 255 + + 1 + + + + QGIS Server test + + Simple test app. + true + + + + + + false + - - 1 + advanced + + enabled + enabled + + 40 + to vertex + + 40.000000 + 40.000000 + + + to_vertex + to_vertex + 1 1 - to vertex + + 1 country20131022151106556 hello20131022151106574 - - to_vertex - to_vertex - - - 40.000000 - 40.000000 - - 40 - - enabled - enabled - - advanced - 5 - - false + + dem20150730091219559 + + false - - - - - - - - - - - - - 5000 + 90 - - - - QGIS - QGIS Server test + + + 108 + 128 + 255 + 246 + 255 + 255 + 255 + + 5 + + EPSG:3857 + EPSG:4326 + + None + conditions unknown + + + 2 + D + true + WGS84 + QGIS + + days + 0 + 2000 + 0 + 1 + 0 + 1 + + 1 + + + + EPSG:3857 + 3857 + 1 + +proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs + + + true + + CountryGroup + + + hello20131022151106574 + + + + true + + CountryGroup + + + hello20131022151106574 + + + + false + + + @@ -2946,12 +4410,12 @@ def my_form_open(dialog, layer, feature): - - - + + + - + @@ -2965,22 +4429,22 @@ def my_form_open(dialog, layer, feature): - - - + + + @@ -2988,51 +4452,51 @@ def my_form_open(dialog, layer, feature): - - - + + + - + - - - + + + - + - points20150803121107046 - hello20131022151106574 - Hello_copy20150804164427541 - Hello_SubsetString_copy20160222085231770 - Hello_Project_SubsetString_copy20160223113949592 - dem20150730091219559 - country20131022151106556 - Country_copy20161127151800736 - country20170328164317226 + points20150803121107046 + hello20131022151106574 + Hello_copy20150804164427541 + Hello_SubsetString_copy20160222085231770 + Hello_Project_SubsetString_copy20160223113949592 + dem20150730091219559 + country20131022151106556 + Country_copy20161127151800736 + country20170328164317226 - + - + @@ -3050,9 +4514,9 @@ def my_form_open(dialog, layer, feature): @@ -3060,7 +4524,7 @@ def my_form_open(dialog, layer, feature): - + @@ -3081,9 +4545,9 @@ def my_form_open(dialog, layer, feature): @@ -3091,49 +4555,49 @@ def my_form_open(dialog, layer, feature): - + - - - + + + - + - - - + + + - + - + - - - + + + - + @@ -3147,22 +4611,22 @@ def my_form_open(dialog, layer, feature): - - - + + + @@ -3170,21 +4634,21 @@ def my_form_open(dialog, layer, feature): - - - + + + - - - - + + + - + - - - + + + - + - points20150803121107046 - hello20131022151106574 - Hello_copy20150804164427541 - Hello_SubsetString_copy20160222085231770 - Hello_Project_SubsetString_copy20160223113949592 - dem20150730091219559 - country20131022151106556 - Country_copy20161127151800736 - country20170328164317226 + points20150803121107046 + hello20131022151106574 + Hello_copy20150804164427541 + Hello_SubsetString_copy20160222085231770 + Hello_Project_SubsetString_copy20160223113949592 + dem20150730091219559 + country20131022151106556 + Country_copy20161127151800736 + country20170328164317226 - + - + @@ -3263,9 +4727,9 @@ def my_form_open(dialog, layer, feature): @@ -3273,7 +4737,7 @@ def my_form_open(dialog, layer, feature): - + @@ -3294,9 +4758,9 @@ def my_form_open(dialog, layer, feature): @@ -3304,42 +4768,42 @@ def my_form_open(dialog, layer, feature): - + - - - + + + - + - - - + + + - + - + From 29d280f1ea1aa3ecb9942e42fb946bb49031d0a4 Mon Sep 17 00:00:00 2001 From: rldhont Date: Tue, 7 Aug 2018 18:08:47 +0200 Subject: [PATCH 18/33] [Server] Update deprecated QgsCoordinateTransform instance creation Using the project as the context for QgsCoordinateTransform instance creation --- .../services/wcs/qgswcsdescribecoverage.cpp | 2 +- .../services/wcs/qgswcsgetcapabilities.cpp | 2 +- src/server/services/wcs/qgswcsgetcoverage.cpp | 4 +-- src/server/services/wcs/qgswcsutils.cpp | 6 ++-- src/server/services/wcs/qgswcsutils.h | 2 +- .../services/wfs/qgswfsgetcapabilities.cpp | 4 +-- src/server/services/wfs/qgswfsgetfeature.cpp | 34 +++++++------------ .../services/wms/qgswmsgetcapabilities.cpp | 30 +++++++--------- src/server/services/wms/qgswmsgetcontext.cpp | 4 +-- 9 files changed, 34 insertions(+), 54 deletions(-) diff --git a/src/server/services/wcs/qgswcsdescribecoverage.cpp b/src/server/services/wcs/qgswcsdescribecoverage.cpp index e8e4204af31b..eb516adc8c94 100644 --- a/src/server/services/wcs/qgswcsdescribecoverage.cpp +++ b/src/server/services/wcs/qgswcsdescribecoverage.cpp @@ -120,7 +120,7 @@ namespace QgsWcs if ( coveNameList.size() == 0 || coveNameList.contains( name ) ) { QgsRasterLayer *rLayer = qobject_cast( layer ); - coveDescElement.appendChild( getCoverageOffering( doc, const_cast( rLayer ) ) ); + coveDescElement.appendChild( getCoverageOffering( doc, const_cast( rLayer ), project ) ); } } return doc; diff --git a/src/server/services/wcs/qgswcsgetcapabilities.cpp b/src/server/services/wcs/qgswcsgetcapabilities.cpp index 105d0b4b0bde..4ebed4cf693f 100644 --- a/src/server/services/wcs/qgswcsgetcapabilities.cpp +++ b/src/server/services/wcs/qgswcsgetcapabilities.cpp @@ -323,7 +323,7 @@ namespace QgsWcs #endif QgsRasterLayer *rLayer = qobject_cast( layer ); - QDomElement layerElem = getCoverageOffering( doc, const_cast( rLayer ), true ); + QDomElement layerElem = getCoverageOffering( doc, const_cast( rLayer ), project, true ); contentMetadataElement.appendChild( layerElem ); } diff --git a/src/server/services/wcs/qgswcsgetcoverage.cpp b/src/server/services/wcs/qgswcsgetcoverage.cpp index 312e54ef1f42..aeccfb8cb45c 100644 --- a/src/server/services/wcs/qgswcsgetcoverage.cpp +++ b/src/server/services/wcs/qgswcsgetcoverage.cpp @@ -165,9 +165,7 @@ namespace QgsWcs // transform rect if ( requestCRS != rLayer->crs() ) { - Q_NOWARN_DEPRECATED_PUSH - QgsCoordinateTransform t( requestCRS, rLayer->crs() ); - Q_NOWARN_DEPRECATED_POP + QgsCoordinateTransform t( requestCRS, rLayer->crs(), project ); rect = t.transformBoundingBox( rect ); } diff --git a/src/server/services/wcs/qgswcsutils.cpp b/src/server/services/wcs/qgswcsutils.cpp index 2127cd40960d..b569b9a759a7 100644 --- a/src/server/services/wcs/qgswcsutils.cpp +++ b/src/server/services/wcs/qgswcsutils.cpp @@ -32,7 +32,7 @@ namespace QgsWcs return QStringLiteral( "1.0.0" ); } - QDomElement getCoverageOffering( QDomDocument &doc, const QgsRasterLayer *layer, bool brief ) + QDomElement getCoverageOffering( QDomDocument &doc, const QgsRasterLayer *layer, const QgsProject *project, bool brief ) { QDomElement layerElem; if ( brief ) @@ -73,9 +73,7 @@ namespace QgsWcs //lonLatEnvelope QgsCoordinateReferenceSystem layerCrs = layer->crs(); - Q_NOWARN_DEPRECATED_PUSH - QgsCoordinateTransform t( layerCrs, QgsCoordinateReferenceSystem( 4326 ) ); - Q_NOWARN_DEPRECATED_POP + QgsCoordinateTransform t( layerCrs, QgsCoordinateReferenceSystem( 4326 ), project ); //transform QgsRectangle BBox; try diff --git a/src/server/services/wcs/qgswcsutils.h b/src/server/services/wcs/qgswcsutils.h index 2480989a6094..16cc5196490e 100644 --- a/src/server/services/wcs/qgswcsutils.h +++ b/src/server/services/wcs/qgswcsutils.h @@ -45,7 +45,7 @@ namespace QgsWcs /** * CoverageOffering or CoverageOfferingBrief element */ - QDomElement getCoverageOffering( QDomDocument &doc, const QgsRasterLayer *layer, bool brief = false ); + QDomElement getCoverageOffering( QDomDocument &doc, const QgsRasterLayer *layer, const QgsProject *project, bool brief = false ); /** * Service URL string diff --git a/src/server/services/wfs/qgswfsgetcapabilities.cpp b/src/server/services/wfs/qgswfsgetcapabilities.cpp index 0dfa8cd371c6..0a3f84dece65 100644 --- a/src/server/services/wfs/qgswfsgetcapabilities.cpp +++ b/src/server/services/wfs/qgswfsgetcapabilities.cpp @@ -559,9 +559,7 @@ namespace QgsWfs QgsRectangle wgs84BoundingRect; if ( !layerExtent.isNull() ) { - Q_NOWARN_DEPRECATED_PUSH - QgsCoordinateTransform exGeoTransform( layer->crs(), wgs84 ); - Q_NOWARN_DEPRECATED_POP + QgsCoordinateTransform exGeoTransform( layer->crs(), wgs84, project ); try { wgs84BoundingRect = exGeoTransform.transformBoundingBox( layerExtent ); diff --git a/src/server/services/wfs/qgswfsgetfeature.cpp b/src/server/services/wfs/qgswfsgetfeature.cpp index 208bd09f0791..ab456ee5406a 100644 --- a/src/server/services/wfs/qgswfsgetfeature.cpp +++ b/src/server/services/wfs/qgswfsgetfeature.cpp @@ -62,9 +62,9 @@ namespace QgsWfs QString createFeatureGeoJSON( QgsFeature *feat, const createFeatureParams ¶ms ); - QDomElement createFeatureGML2( QgsFeature *feat, QDomDocument &doc, const createFeatureParams ¶ms ); + QDomElement createFeatureGML2( QgsFeature *feat, QDomDocument &doc, const createFeatureParams ¶ms, const QgsProject *project ); - QDomElement createFeatureGML3( QgsFeature *feat, QDomDocument &doc, const createFeatureParams ¶ms ); + QDomElement createFeatureGML3( QgsFeature *feat, QDomDocument &doc, const createFeatureParams ¶ms, const QgsProject *project ); void hitGetFeature( const QgsServerRequest &request, QgsServerResponse &response, const QgsProject *project, QgsWfsParameters::Format format, int numberOfFeatures, const QStringList &typeNames ); @@ -74,7 +74,7 @@ namespace QgsWfs QgsRectangle *rect, const QStringList &typeNames ); void setGetFeature( QgsServerResponse &response, QgsWfsParameters::Format format, QgsFeature *feat, int featIdx, - const createFeatureParams ¶ms ); + const createFeatureParams ¶ms, const QgsProject *project ); void endGetFeature( QgsServerResponse &response, QgsWfsParameters::Format format ); @@ -155,9 +155,7 @@ namespace QgsWfs } else { - Q_NOWARN_DEPRECATED_PUSH - QgsCoordinateTransform transform( layer->crs(), requestCrs ); - Q_NOWARN_DEPRECATED_POP + QgsCoordinateTransform transform( layer->crs(), requestCrs, project ); try { if ( requestRect.isEmpty() ) @@ -357,9 +355,7 @@ namespace QgsWfs if ( !featureRequest.filterRect().isEmpty() ) { - Q_NOWARN_DEPRECATED_PUSH - QgsCoordinateTransform transform( outputCrs, vlayer->crs() ); - Q_NOWARN_DEPRECATED_POP + QgsCoordinateTransform transform( outputCrs, vlayer->crs(), project ); try { featureRequest.setFilterRect( transform.transform( featureRequest.filterRect() ) ); @@ -405,7 +401,7 @@ namespace QgsWfs if ( iteratedFeatures >= aRequest.startIndex ) { - setGetFeature( response, aRequest.outputFormat, &feature, sentFeatures, cfp ); + setGetFeature( response, aRequest.outputFormat, &feature, sentFeatures, cfp, project ); ++sentFeatures; } ++iteratedFeatures; @@ -1169,7 +1165,7 @@ namespace QgsWfs } void setGetFeature( QgsServerResponse &response, QgsWfsParameters::Format format, QgsFeature *feat, int featIdx, - const createFeatureParams ¶ms ) + const createFeatureParams ¶ms, const QgsProject *project ) { if ( !feat->isValid() ) return; @@ -1196,12 +1192,12 @@ namespace QgsWfs QDomElement featureElement; if ( format == QgsWfsParameters::Format::GML3 ) { - featureElement = createFeatureGML3( feat, gmlDoc, params ); + featureElement = createFeatureGML3( feat, gmlDoc, params, project ); gmlDoc.appendChild( featureElement ); } else { - featureElement = createFeatureGML2( feat, gmlDoc, params ); + featureElement = createFeatureGML2( feat, gmlDoc, params, project ); gmlDoc.appendChild( featureElement ); } response.write( gmlDoc.toByteArray() ); @@ -1255,7 +1251,7 @@ namespace QgsWfs } - QDomElement createFeatureGML2( QgsFeature *feat, QDomDocument &doc, const createFeatureParams ¶ms ) + QDomElement createFeatureGML2( QgsFeature *feat, QDomDocument &doc, const createFeatureParams ¶ms, const QgsProject *project ) { //gml:FeatureMember QDomElement featureElement = doc.createElement( QStringLiteral( "gml:featureMember" )/*wfs:FeatureMember*/ ); @@ -1271,9 +1267,7 @@ namespace QgsWfs { int prec = params.precision; QgsCoordinateReferenceSystem crs = params.crs; - Q_NOWARN_DEPRECATED_PUSH - QgsCoordinateTransform mTransform( crs, params.outputCrs ); - Q_NOWARN_DEPRECATED_POP + QgsCoordinateTransform mTransform( crs, params.outputCrs, project ); try { QgsGeometry transformed = geom; @@ -1352,7 +1346,7 @@ namespace QgsWfs return featureElement; } - QDomElement createFeatureGML3( QgsFeature *feat, QDomDocument &doc, const createFeatureParams ¶ms ) + QDomElement createFeatureGML3( QgsFeature *feat, QDomDocument &doc, const createFeatureParams ¶ms, const QgsProject *project ) { //gml:FeatureMember QDomElement featureElement = doc.createElement( QStringLiteral( "gml:featureMember" )/*wfs:FeatureMember*/ ); @@ -1368,9 +1362,7 @@ namespace QgsWfs { int prec = params.precision; QgsCoordinateReferenceSystem crs = params.crs; - Q_NOWARN_DEPRECATED_PUSH - QgsCoordinateTransform mTransform( crs, params.outputCrs ); - Q_NOWARN_DEPRECATED_POP + QgsCoordinateTransform mTransform( crs, params.outputCrs, project ); try { QgsGeometry transformed = geom; diff --git a/src/server/services/wms/qgswmsgetcapabilities.cpp b/src/server/services/wms/qgswmsgetcapabilities.cpp index 408fb6a504ff..deecfdf2b4a5 100644 --- a/src/server/services/wms/qgswmsgetcapabilities.cpp +++ b/src/server/services/wms/qgswmsgetcapabilities.cpp @@ -61,11 +61,12 @@ namespace QgsWms const QgsProject *project ); void appendLayerBoundingBox( QDomDocument &doc, QDomElement &layerElem, const QgsRectangle &layerExtent, - const QgsCoordinateReferenceSystem &layerCRS, const QString &crsText ); + const QgsCoordinateReferenceSystem &layerCRS, const QString &crsText, + const QgsProject *project ); void appendLayerBoundingBoxes( QDomDocument &doc, QDomElement &layerElem, const QgsRectangle &lExtent, const QgsCoordinateReferenceSystem &layerCRS, const QStringList &crsList, - const QStringList &constrainedCrsList ); + const QStringList &constrainedCrsList, const QgsProject *project ); void appendCrsElementToLayer( QDomDocument &doc, QDomElement &layerElement, const QDomElement &precedingElement, const QString &crsText ); @@ -1037,7 +1038,7 @@ namespace QgsWms appendCrsElementsToLayer( doc, layerElem, crsList, outputCrsList ); //Ex_GeographicBoundingBox - appendLayerBoundingBoxes( doc, layerElem, l->extent(), l->crs(), crsList, outputCrsList ); + appendLayerBoundingBoxes( doc, layerElem, l->extent(), l->crs(), crsList, outputCrsList, project ); } // add details about supported styles of the layer @@ -1291,7 +1292,7 @@ namespace QgsWms void appendLayerBoundingBoxes( QDomDocument &doc, QDomElement &layerElem, const QgsRectangle &lExtent, const QgsCoordinateReferenceSystem &layerCRS, const QStringList &crsList, - const QStringList &constrainedCrsList ) + const QStringList &constrainedCrsList, const QgsProject *project ) { if ( layerElem.isNull() ) { @@ -1315,9 +1316,7 @@ namespace QgsWms QgsRectangle wgs84BoundingRect; if ( !layerExtent.isNull() ) { - Q_NOWARN_DEPRECATED_PUSH - QgsCoordinateTransform exGeoTransform( layerCRS, wgs84 ); - Q_NOWARN_DEPRECATED_POP + QgsCoordinateTransform exGeoTransform( layerCRS, wgs84, project ); try { wgs84BoundingRect = exGeoTransform.transformBoundingBox( layerExtent ); @@ -1375,21 +1374,22 @@ namespace QgsWms { for ( int i = constrainedCrsList.size() - 1; i >= 0; --i ) { - appendLayerBoundingBox( doc, layerElem, layerExtent, layerCRS, constrainedCrsList.at( i ) ); + appendLayerBoundingBox( doc, layerElem, layerExtent, layerCRS, constrainedCrsList.at( i ), project ); } } else //no crs constraint { Q_FOREACH ( const QString &crs, crsList ) { - appendLayerBoundingBox( doc, layerElem, layerExtent, layerCRS, crs ); + appendLayerBoundingBox( doc, layerElem, layerExtent, layerCRS, crs, project ); } } } void appendLayerBoundingBox( QDomDocument &doc, QDomElement &layerElem, const QgsRectangle &layerExtent, - const QgsCoordinateReferenceSystem &layerCRS, const QString &crsText ) + const QgsCoordinateReferenceSystem &layerCRS, const QString &crsText, + const QgsProject *project ) { if ( layerElem.isNull() ) { @@ -1409,9 +1409,7 @@ namespace QgsWms QgsRectangle crsExtent; if ( !layerExtent.isNull() ) { - Q_NOWARN_DEPRECATED_PUSH - QgsCoordinateTransform crsTransform( layerCRS, crs ); - Q_NOWARN_DEPRECATED_POP + QgsCoordinateTransform crsTransform( layerCRS, crs, project ); try { crsExtent = crsTransform.transformBoundingBox( layerExtent ); @@ -1524,9 +1522,7 @@ namespace QgsWms } //get project crs - Q_NOWARN_DEPRECATED_PUSH - QgsCoordinateTransform t( layerCrs, project->crs() ); - Q_NOWARN_DEPRECATED_POP + QgsCoordinateTransform t( layerCrs, project->crs(), project ); //transform try @@ -1629,7 +1625,7 @@ namespace QgsWms combinedBBox = mapRect; } } - appendLayerBoundingBoxes( doc, groupElem, combinedBBox, groupCRS, combinedCRSSet.toList(), outputCrsList ); + appendLayerBoundingBoxes( doc, groupElem, combinedBBox, groupCRS, combinedCRSSet.toList(), outputCrsList, project ); } diff --git a/src/server/services/wms/qgswmsgetcontext.cpp b/src/server/services/wms/qgswmsgetcontext.cpp index 41347fe0c266..2a09b42c1e7c 100644 --- a/src/server/services/wms/qgswmsgetcontext.cpp +++ b/src/server/services/wms/qgswmsgetcontext.cpp @@ -405,9 +405,7 @@ namespace QgsWms // update combineBBox try { - Q_NOWARN_DEPRECATED_PUSH - QgsCoordinateTransform t( l->crs(), project->crs() ); - Q_NOWARN_DEPRECATED_POP + QgsCoordinateTransform t( l->crs(), project->crs(), project ); QgsRectangle BBox = t.transformBoundingBox( l->extent() ); if ( combinedBBox.isEmpty() ) { From f6d0fc08f92389b36f52a781dfec5256da38127e Mon Sep 17 00:00:00 2001 From: rldhont Date: Tue, 7 Aug 2018 18:34:49 +0200 Subject: [PATCH 19/33] [Server] Q_FOREACH replaced by for --- src/server/qgsaccesscontrol.cpp | 2 +- src/server/qgsrequesthandler.cpp | 2 +- src/server/qgsserverplugins.cpp | 2 +- src/server/qgsservicenativeloader.cpp | 2 +- src/server/services/wfs/qgswfsgetfeature.cpp | 2 +- src/server/services/wms/qgslayerrestorer.cpp | 2 +- .../services/wms/qgswmsdescribelayer.cpp | 2 +- .../services/wms/qgswmsgetcapabilities.cpp | 6 +- src/server/services/wms/qgswmsgetcontext.cpp | 2 +- src/server/services/wms/qgswmsgetstyles.cpp | 4 +- src/server/services/wms/qgswmsrenderer.cpp | 65 +++++++++---------- 11 files changed, 45 insertions(+), 46 deletions(-) diff --git a/src/server/qgsaccesscontrol.cpp b/src/server/qgsaccesscontrol.cpp index fdd8d8ac8c69..0ef5a80b67cf 100644 --- a/src/server/qgsaccesscontrol.cpp +++ b/src/server/qgsaccesscontrol.cpp @@ -24,7 +24,7 @@ void QgsAccessControl::resolveFilterFeatures( const QList &layers ) { - Q_FOREACH ( QgsMapLayer *l, layers ) + for ( QgsMapLayer *l : layers ) { if ( l->type() == QgsMapLayer::LayerType::VectorLayer ) { diff --git a/src/server/qgsrequesthandler.cpp b/src/server/qgsrequesthandler.cpp index 07b5914c6b41..b6080f48fe7a 100644 --- a/src/server/qgsrequesthandler.cpp +++ b/src/server/qgsrequesthandler.cpp @@ -228,7 +228,7 @@ void QgsRequestHandler::parseInput() typedef QPair pair_t; QUrlQuery query( inputString ); QList items = query.queryItems(); - Q_FOREACH ( pair_t pair, items ) + for ( pair_t pair : items ) { // QUrl::fromPercentEncoding doesn't replace '+' with space const QString key = QUrl::fromPercentEncoding( pair.first.replace( '+', ' ' ).toUtf8() ); diff --git a/src/server/qgsserverplugins.cpp b/src/server/qgsserverplugins.cpp index b735c7ac80a7..62216b15f8fa 100644 --- a/src/server/qgsserverplugins.cpp +++ b/src/server/qgsserverplugins.cpp @@ -90,7 +90,7 @@ bool QgsServerPlugins::initPlugins( QgsServerInterface *interface ) //Init plugins: loads a list of installed plugins and filter them //for "server" metadata bool atLeastOneEnabled = false; - Q_FOREACH ( const QString &pluginName, sPythonUtils->pluginList() ) + for ( const QString &pluginName : sPythonUtils->pluginList() ) { QString pluginService = sPythonUtils->getPluginMetadata( pluginName, QStringLiteral( "server" ) ); if ( pluginService == QLatin1String( "True" ) ) diff --git a/src/server/qgsservicenativeloader.cpp b/src/server/qgsservicenativeloader.cpp index cc12df4eaa8c..fc50267b2c72 100644 --- a/src/server/qgsservicenativeloader.cpp +++ b/src/server/qgsservicenativeloader.cpp @@ -70,7 +70,7 @@ void QgsServiceNativeLoader::loadModules( const QString &modulePath, QgsServiceR qDebug() << QString( "Checking %1 for native services modules" ).arg( moduleDir.path() ); //QgsDebugMsg( QString( "Checking %1 for native services modules" ).arg( moduleDir.path() ) ); - Q_FOREACH ( const QFileInfo &fi, moduleDir.entryInfoList() ) + for ( const QFileInfo &fi : moduleDir.entryInfoList() ) { QgsServiceModule *module = loadNativeModule( fi.filePath() ); if ( module ) diff --git a/src/server/services/wfs/qgswfsgetfeature.cpp b/src/server/services/wfs/qgswfsgetfeature.cpp index ab456ee5406a..45c1000c159d 100644 --- a/src/server/services/wfs/qgswfsgetfeature.cpp +++ b/src/server/services/wfs/qgswfsgetfeature.cpp @@ -317,7 +317,7 @@ namespace QgsWfs accessControl->filterFeatures( vlayer, featureRequest ); QStringList attributes = QStringList(); - Q_FOREACH ( int idx, attrIndexes ) + for ( int idx : attrIndexes ) { attributes.append( vlayer->fields().field( idx ).name() ); } diff --git a/src/server/services/wms/qgslayerrestorer.cpp b/src/server/services/wms/qgslayerrestorer.cpp index 78f9585121de..5f51579c3219 100644 --- a/src/server/services/wms/qgslayerrestorer.cpp +++ b/src/server/services/wms/qgslayerrestorer.cpp @@ -23,7 +23,7 @@ QgsLayerRestorer::QgsLayerRestorer( const QList &layers ) { - Q_FOREACH ( QgsMapLayer *layer, layers ) + for ( QgsMapLayer *layer : layers ) { QgsLayerSettings settings; settings.name = layer->name(); diff --git a/src/server/services/wms/qgswmsdescribelayer.cpp b/src/server/services/wms/qgswmsdescribelayer.cpp index 2007b8bb833d..760115450737 100644 --- a/src/server/services/wms/qgswmsdescribelayer.cpp +++ b/src/server/services/wms/qgswmsdescribelayer.cpp @@ -113,7 +113,7 @@ namespace QgsWms // WCS layers QStringList wcsLayerIds = QgsServerProjectUtils::wcsLayerIds( *project ); - Q_FOREACH ( QgsMapLayer *layer, project->mapLayers() ) + for ( QgsMapLayer *layer : project->mapLayers() ) { QString name = layer->name(); if ( useLayerIds ) diff --git a/src/server/services/wms/qgswmsgetcapabilities.cpp b/src/server/services/wms/qgswmsgetcapabilities.cpp index deecfdf2b4a5..0b02e21299a5 100644 --- a/src/server/services/wms/qgswmsgetcapabilities.cpp +++ b/src/server/services/wms/qgswmsgetcapabilities.cpp @@ -1169,7 +1169,7 @@ namespace QgsWms //href needs to be a prefix QString hrefString = href.toString( QUrl::FullyDecoded ); hrefString.append( href.hasQuery() ? "&" : "?" ); - Q_FOREACH ( QString styleName, currentLayer->styleManager()->styles() ) + for ( const QString &styleName : currentLayer->styleManager()->styles() ) { QDomElement styleElem = doc.createElement( QStringLiteral( "Style" ) ); QDomElement styleNameElem = doc.createElement( QStringLiteral( "Name" ) ); @@ -1268,7 +1268,7 @@ namespace QgsWms } else //no crs constraint { - Q_FOREACH ( const QString &crs, crsList ) + for ( const QString &crs : crsList ) { appendCrsElementToLayer( doc, layerElement, CRSPrecedingElement, crs ); } @@ -1379,7 +1379,7 @@ namespace QgsWms } else //no crs constraint { - Q_FOREACH ( const QString &crs, crsList ) + for ( const QString &crs : crsList ) { appendLayerBoundingBox( doc, layerElem, layerExtent, layerCRS, crs, project ); } diff --git a/src/server/services/wms/qgswmsgetcontext.cpp b/src/server/services/wms/qgswmsgetcontext.cpp index 2a09b42c1e7c..36cb0ea3bcbd 100644 --- a/src/server/services/wms/qgswmsgetcontext.cpp +++ b/src/server/services/wms/qgswmsgetcontext.cpp @@ -435,7 +435,7 @@ namespace QgsWms void appendOwsLayerStyles( QDomDocument &doc, QDomElement &layerElem, QgsMapLayer *currentLayer ) { - Q_FOREACH ( QString styleName, currentLayer->styleManager()->styles() ) + for ( const QString &styleName : currentLayer->styleManager()->styles() ) { QDomElement styleListElem = doc.createElement( QStringLiteral( "StyleList" ) ); //only one default style in project file mode diff --git a/src/server/services/wms/qgswmsgetstyles.cpp b/src/server/services/wms/qgswmsgetstyles.cpp index 2ae53b20c6ab..a338730281d8 100644 --- a/src/server/services/wms/qgswmsgetstyles.cpp +++ b/src/server/services/wms/qgswmsgetstyles.cpp @@ -133,7 +133,7 @@ namespace QgsWms // WMS restricted layers QStringList restrictedLayers = QgsServerProjectUtils::wmsRestrictedLayers( *project ); - Q_FOREACH ( QgsMapLayer *layer, project->mapLayers() ) + for ( QgsMapLayer *layer : project->mapLayers() ) { QString name = layer->name(); if ( useLayerIds ) @@ -172,7 +172,7 @@ namespace QgsWms if ( vlayer->isSpatial() ) { QString currentStyle = vlayer->styleManager()->currentStyle(); - Q_FOREACH ( QString styleName, vlayer->styleManager()->styles() ) + for ( const QString &styleName : vlayer->styleManager()->styles() ) { vlayer->styleManager()->setCurrentStyle( styleName ); QDomElement styleElem = vlayer->renderer()->writeSld( myDocument, styleName ); diff --git a/src/server/services/wms/qgswmsrenderer.cpp b/src/server/services/wms/qgswmsrenderer.cpp index e0e5286c0b3a..e7e3e4c88836 100644 --- a/src/server/services/wms/qgswmsrenderer.cpp +++ b/src/server/services/wms/qgswmsrenderer.cpp @@ -111,9 +111,9 @@ namespace QgsWms QgsLayerTreeModelLegendNode *_findLegendNodeForRule( QgsLayerTreeModel *legendModel, const QString &rule ) { - Q_FOREACH ( QgsLayerTreeLayer *nodeLayer, legendModel->rootGroup()->findLayers() ) + for ( QgsLayerTreeLayer *nodeLayer : legendModel->rootGroup()->findLayers() ) { - Q_FOREACH ( QgsLayerTreeModelLegendNode *legendNode, legendModel->layerLegendNodes( nodeLayer ) ) + for ( QgsLayerTreeModelLegendNode *legendNode : legendModel->layerLegendNodes( nodeLayer ) ) { if ( legendNode->data( Qt::DisplayRole ).toString() == rule ) return legendNode; @@ -181,7 +181,7 @@ namespace QgsWms std::reverse( layers.begin(), layers.end() ); // check permissions - Q_FOREACH ( QgsMapLayer *ml, layers ) + for ( QgsMapLayer *ml : layers ) checkLayerReadPermissions( ml ); // build layer tree model for legend @@ -241,7 +241,7 @@ namespace QgsWms { QgsRenderContext context = QgsRenderContext::fromMapSettings( mapSettings ); - Q_FOREACH ( const QString &id, mapSettings.layerIds() ) + for ( const QString &id : mapSettings.layerIds() ) { QgsVectorLayer *vl = qobject_cast( mProject->mapLayer( id ) ); if ( !vl || !vl->renderer() ) @@ -276,7 +276,7 @@ namespace QgsWms context.expressionContext().setFeature( f ); if ( moreSymbolsPerFeature ) { - Q_FOREACH ( QgsSymbol *s, r->originalSymbolsForFeature( f, context ) ) + for ( QgsSymbol *s : r->originalSymbolsForFeature( f, context ) ) usedSymbols.insert( QgsSymbolLayerUtils::symbolProperties( s ) ); } else @@ -328,11 +328,11 @@ namespace QgsWms // configure each layer with opacity, selection filter, ... bool updateMapExtent = mWmsParameters.bbox().isEmpty(); - Q_FOREACH ( QgsMapLayer *layer, layers ) + for ( QgsMapLayer *layer : layers ) { checkLayerReadPermissions( layer ); - Q_FOREACH ( QgsWmsParametersLayer param, params ) + for ( QgsWmsParametersLayer param : params ) { if ( param.mNickname == layerNickname( *layer ) ) { @@ -677,11 +677,11 @@ namespace QgsWms // configure each layer with opacity, selection filter, ... bool updateMapExtent = mWmsParameters.bbox().isEmpty(); - Q_FOREACH ( QgsMapLayer *layer, layers ) + for ( QgsMapLayer *layer : layers ) { checkLayerReadPermissions( layer ); - Q_FOREACH ( QgsWmsParametersLayer param, params ) + for ( const QgsWmsParametersLayer param : params ) { if ( param.mNickname == layerNickname( *layer ) ) { @@ -774,7 +774,7 @@ namespace QgsWms // get dxf layers QList< QgsDxfExport::DxfLayer > dxfLayers; int layerIdx = -1; - Q_FOREACH ( QgsMapLayer *layer, layers ) + for ( QgsMapLayer *layer : layers ) { layerIdx++; if ( layer->type() != QgsMapLayer::VectorLayer ) @@ -784,7 +784,7 @@ namespace QgsWms checkLayerReadPermissions( layer ); - Q_FOREACH ( QgsWmsParametersLayer param, params ) + for ( QgsWmsParametersLayer param : params ) { if ( param.mNickname == layerNickname( *layer ) ) { @@ -951,11 +951,11 @@ namespace QgsWms // remove non identifiable layers //removeNonIdentifiableLayers( layers ); - Q_FOREACH ( QgsMapLayer *layer, layers ) + for ( QgsMapLayer *layer : layers ) { checkLayerReadPermissions( layer ); - Q_FOREACH ( QgsWmsParametersLayer param, params ) + for ( QgsWmsParametersLayer param : params ) { if ( param.mNickname == layerNickname( *layer ) ) { @@ -1244,11 +1244,11 @@ namespace QgsWms //layers can have assigned a different name for GetCapabilities QHash layerAliasMap = QgsServerProjectUtils::wmsFeatureInfoLayerAliasMap( *mProject ); - Q_FOREACH ( QString queryLayer, queryLayers ) + for ( const QString &queryLayer : queryLayers ) { bool validLayer = false; bool queryableLayer = true; - Q_FOREACH ( QgsMapLayer *layer, layers ) + for ( QgsMapLayer *layer : layers ) { if ( queryLayer == layerNickname( *layer ) ) { @@ -1456,8 +1456,7 @@ namespace QgsWms mAccessControl->filterFeatures( layer, fReq ); QStringList attributes; - QgsField field; - Q_FOREACH ( field, layer->fields().toList() ) + for ( QgsField field : layer->fields().toList() ) { attributes.append( field.name() ); } @@ -2368,13 +2367,13 @@ namespace QgsWms QStringList restrictedLayersNames; QgsLayerTreeGroup *root = mProject->layerTreeRoot(); - Q_FOREACH ( QString l, restricted ) + for ( const QString &l : restricted ) { QgsLayerTreeGroup *group = root->findGroup( l ); if ( group ) { QList groupLayers = group->findLayers(); - Q_FOREACH ( QgsLayerTreeLayer *treeLayer, groupLayers ) + for ( QgsLayerTreeLayer *treeLayer : groupLayers ) { restrictedLayersNames.append( treeLayer->name() ); } @@ -2387,7 +2386,7 @@ namespace QgsWms // build output with names, ids or short name according to the configuration QList layers = root->findLayers(); - Q_FOREACH ( QgsLayerTreeLayer *layer, layers ) + for ( QgsLayerTreeLayer *layer : layers ) { if ( restrictedLayersNames.contains( layer->name() ) ) { @@ -2398,7 +2397,7 @@ namespace QgsWms void QgsRenderer::initNicknameLayers() { - Q_FOREACH ( QgsMapLayer *ml, mProject->mapLayers() ) + for ( QgsMapLayer *ml : mProject->mapLayers() ) { mNicknameLayers[ layerNickname( *ml ) ] = ml; } @@ -2469,7 +2468,7 @@ namespace QgsWms // try to create highlight layer for each geometry QString crs = mWmsParameters.crs(); - Q_FOREACH ( QgsWmsParametersHighlightLayer param, params ) + for ( QgsWmsParametersHighlightLayer param : params ) { // create sld document from symbology QDomDocument sldDoc; @@ -2675,7 +2674,7 @@ namespace QgsWms { QList layers; - Q_FOREACH ( QgsWmsParametersLayer param, params ) + for ( QgsWmsParametersLayer param : params ) { QString nickname = param.mNickname; QString style = param.mStyle; @@ -2798,7 +2797,7 @@ namespace QgsWms if ( layer->type() == QgsMapLayer::VectorLayer ) { QgsVectorLayer *filteredLayer = qobject_cast( layer ); - Q_FOREACH ( QString filter, filters ) + for ( const QString &filter : filters ) { if ( filter.startsWith( QStringLiteral( "<" ) ) && filter.endsWith( QStringLiteral( "Filter>" ) ) ) { @@ -2850,7 +2849,7 @@ namespace QgsWms { QgsFeatureIds selectedIds; - Q_FOREACH ( const QString &id, fids ) + for ( const QString &id : fids ) { selectedIds.insert( STRING_TO_FID( id ) ); } @@ -2927,7 +2926,7 @@ namespace QgsWms { QList wantedLayers; - Q_FOREACH ( QgsMapLayer *layer, layers ) + for ( QgsMapLayer *layer : layers ) { if ( !layerScaleVisibility( *layer, scaleDenominator ) ) continue; @@ -2948,7 +2947,7 @@ namespace QgsWms { QList wantedLayers; - Q_FOREACH ( QgsMapLayer *layer, layers ) + for ( QgsMapLayer *layer : layers ) { if ( nonIdentifiableLayers.contains( layer->id() ) ) continue; @@ -2989,7 +2988,7 @@ namespace QgsWms // build layer tree rootGroup.clear(); QList counters; - Q_FOREACH ( QgsMapLayer *ml, layers ) + for ( QgsMapLayer *ml : layers ) { QgsLayerTreeLayer *lt = rootGroup.addLayer( ml ); lt->setCustomProperty( QStringLiteral( "showFeatureCount" ), showFeatureCount ); @@ -3018,7 +3017,7 @@ namespace QgsWms HitTest hitTest; getMap( contentBasedMapSettings, &hitTest ); - Q_FOREACH ( QgsLayerTreeNode *node, rootGroup.children() ) + for ( QgsLayerTreeNode *node : rootGroup.children() ) { Q_ASSERT( QgsLayerTree::isLayer( node ) ); QgsLayerTreeLayer *nodeLayer = QgsLayerTree::toLayer( node ); @@ -3030,7 +3029,7 @@ namespace QgsWms const SymbolSet &usedSymbols = hitTest[vl]; QList order; int i = 0; - Q_FOREACH ( const QgsLegendSymbolItem &legendItem, vl->renderer()->legendSymbolItems() ) + for ( const QgsLegendSymbolItem &legendItem : vl->renderer()->legendSymbolItems() ) { QString sProp = QgsSymbolLayerUtils::symbolProperties( legendItem.legacyRuleKey() ); if ( usedSymbols.contains( sProp ) ) @@ -3053,7 +3052,7 @@ namespace QgsWms if ( ! ruleDefined ) { QList rootChildren = rootGroup.children(); - Q_FOREACH ( QgsLayerTreeNode *node, rootChildren ) + for ( QgsLayerTreeNode *node : rootChildren ) { if ( QgsLayerTree::isLayer( node ) ) { @@ -3065,14 +3064,14 @@ namespace QgsWms // rule item titles if ( !drawLegendItemLabel ) { - Q_FOREACH ( QgsLayerTreeModelLegendNode *legendNode, legendModel->layerLegendNodes( nodeLayer ) ) + for ( QgsLayerTreeModelLegendNode *legendNode : legendModel->layerLegendNodes( nodeLayer ) ) { legendNode->setUserLabel( QStringLiteral( " " ) ); // empty string = no override, so let's use one space } } else if ( !drawLegendLayerLabel ) { - Q_FOREACH ( QgsLayerTreeModelLegendNode *legendNode, legendModel->layerLegendNodes( nodeLayer ) ) + for ( QgsLayerTreeModelLegendNode *legendNode : legendModel->layerLegendNodes( nodeLayer ) ) { if ( legendNode->isEmbeddedInParent() ) legendNode->setEmbeddedInParent( false ); From 6895926e7b89fc4f38908b863944191930187694 Mon Sep 17 00:00:00 2001 From: rldhont Date: Fri, 10 Aug 2018 14:38:41 +0200 Subject: [PATCH 20/33] [Server] Various code cleaning for server cache manager and WMTS service --- src/server/qgsserverinterfaceimpl.h | 1 + src/server/services/wmts/qgswmts.cpp | 4 ++-- src/server/services/wmts/qgswmtsgettile.cpp | 2 +- src/server/services/wmts/qgswmtsgettile.h | 2 +- src/server/services/wmts/qgswmtsserviceexception.h | 6 +++--- src/server/services/wmts/qgswmtsutils.h | 1 + 6 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/server/qgsserverinterfaceimpl.h b/src/server/qgsserverinterfaceimpl.h index 3b31e2bc349e..a258171e8ede 100644 --- a/src/server/qgsserverinterfaceimpl.h +++ b/src/server/qgsserverinterfaceimpl.h @@ -72,6 +72,7 @@ class QgsServerInterfaceImpl : public QgsServerInterface /** * Gets the helper over all the registered server cache filters * \returns the server cache helper + * \since QGIS 3.4 */ QgsServerCacheManager *cacheManager() const override { return mCacheManager.get(); } diff --git a/src/server/services/wmts/qgswmts.cpp b/src/server/services/wmts/qgswmts.cpp index 465efcee36ea..c128a3828526 100644 --- a/src/server/services/wmts/qgswmts.cpp +++ b/src/server/services/wmts/qgswmts.cpp @@ -31,7 +31,7 @@ namespace QgsWmts * \ingroup server * \class QgsWmts::Service * \brief OGC web service specialized for WMTS - * \since QGIS 3.0 + * \since QGIS 3.4 */ class Service: public QgsService { @@ -68,7 +68,7 @@ namespace QgsWmts } // Get the request - QString req = params.value( QStringLiteral( "REQUEST" ) ); + QString req = params.value( QgsServerParameter::name( QgsServerParameter::REQUEST ) ); if ( req.isEmpty() ) { throw QgsServiceException( QStringLiteral( "OperationNotSupported" ), diff --git a/src/server/services/wmts/qgswmtsgettile.cpp b/src/server/services/wmts/qgswmtsgettile.cpp index d1ba058e1012..363719dcc93f 100644 --- a/src/server/services/wmts/qgswmtsgettile.cpp +++ b/src/server/services/wmts/qgswmtsgettile.cpp @@ -1,5 +1,5 @@ /*************************************************************************** - qgswmsgetmap.cpp + qgswmtsgettile.cpp ------------------------- begin : July 23 , 2017 copyright : (C) 2018 by René-Luc D'Hont diff --git a/src/server/services/wmts/qgswmtsgettile.h b/src/server/services/wmts/qgswmtsgettile.h index c71edc3ea398..376ee6b4cfee 100644 --- a/src/server/services/wmts/qgswmtsgettile.h +++ b/src/server/services/wmts/qgswmtsgettile.h @@ -1,5 +1,5 @@ /*************************************************************************** - qgswmsgettile.h + qgswmtsgettile.h ------------------------- begin : July 23 , 2017 copyright : (C) 2018 by René-Luc D'Hont diff --git a/src/server/services/wmts/qgswmtsserviceexception.h b/src/server/services/wmts/qgswmtsserviceexception.h index 2d80519a1bb0..6426059d1e63 100644 --- a/src/server/services/wmts/qgswmtsserviceexception.h +++ b/src/server/services/wmts/qgswmtsserviceexception.h @@ -1,7 +1,7 @@ /*************************************************************************** qgswmtsserviceexception.h ------------------------ - begin : July 23, 2017 + begin : July 23, 2018 copyright : (C) 2018 by René-Luc D'Hont email : rldhont at 3liz dot com ***************************************************************************/ @@ -28,8 +28,8 @@ namespace QgsWmts /** * \ingroup server * \class QgsWmts::QgsServiceException - * \brief Exception class for WFS services - * \since QGIS 3.0 + * \brief Exception class for WMTS services + * \since QGIS 3.4 */ class QgsServiceException : public QgsOgcServiceException { diff --git a/src/server/services/wmts/qgswmtsutils.h b/src/server/services/wmts/qgswmtsutils.h index c856d4e564ba..6767dc6f24bf 100644 --- a/src/server/services/wmts/qgswmtsutils.h +++ b/src/server/services/wmts/qgswmtsutils.h @@ -27,6 +27,7 @@ /** * \ingroup server * WMTS implementation + * \since QGIS 3.4 */ //! WMTS implementation From 408484d48637fed9e59fe7af27c98dc31d524f47 Mon Sep 17 00:00:00 2001 From: rldhont Date: Sun, 12 Aug 2018 17:57:10 +0200 Subject: [PATCH 21/33] [Server][Feature][needs-docs] Add QgsWmtsParameters to WMTS service --- src/server/services/wmts/CMakeLists.txt | 9 +- src/server/services/wmts/qgswmts.cpp | 6 +- .../services/wmts/qgswmtsgetcapabilities.cpp | 18 +- .../services/wmts/qgswmtsgetfeatureinfo.cpp | 14 +- .../services/wmts/qgswmtsgetfeatureinfo.h | 2 +- src/server/services/wmts/qgswmtsgettile.cpp | 11 +- .../services/wmts/qgswmtsparameters.cpp | 295 ++++++++++++++++++ src/server/services/wmts/qgswmtsparameters.h | 285 +++++++++++++++++ src/server/services/wmts/qgswmtsutils.cpp | 125 ++------ src/server/services/wmts/qgswmtsutils.h | 19 +- 10 files changed, 652 insertions(+), 132 deletions(-) create mode 100644 src/server/services/wmts/qgswmtsparameters.cpp create mode 100644 src/server/services/wmts/qgswmtsparameters.h diff --git a/src/server/services/wmts/CMakeLists.txt b/src/server/services/wmts/CMakeLists.txt index 18790caa531c..f9d28d92c464 100644 --- a/src/server/services/wmts/CMakeLists.txt +++ b/src/server/services/wmts/CMakeLists.txt @@ -8,12 +8,19 @@ SET (wmts_SRCS qgswmtsgetcapabilities.cpp qgswmtsgettile.cpp qgswmtsgetfeatureinfo.cpp + qgswmtsparameters.cpp +) + +SET (wmts_MOC_HDRS + qgswmtsparameters.h ) ######################################################## # Build -ADD_LIBRARY (wmts MODULE ${wmts_SRCS}) +QT5_WRAP_CPP(wmts_MOC_SRCS ${wmts_MOC_HDRS}) + +ADD_LIBRARY (wmts MODULE ${wmts_SRCS} ${wmts_MOC_SRCS} ${wmts_MOC_HDRS}) INCLUDE_DIRECTORIES(SYSTEM diff --git a/src/server/services/wmts/qgswmts.cpp b/src/server/services/wmts/qgswmts.cpp index c128a3828526..7fa58ca99cab 100644 --- a/src/server/services/wmts/qgswmts.cpp +++ b/src/server/services/wmts/qgswmts.cpp @@ -58,13 +58,13 @@ namespace QgsWmts { Q_UNUSED( project ); - QgsServerRequest::Parameters params = request.parameters(); - QString versionString = params.value( QStringLiteral( "VERSION" ) ); + const QgsWmtsParameters params( QUrlQuery( request.url() ) ); // Set the default version + QString versionString = params.version(); if ( versionString.isEmpty() ) { - versionString = version(); + versionString = version(); // defined in qgswfsutils.h } // Get the request diff --git a/src/server/services/wmts/qgswmtsgetcapabilities.cpp b/src/server/services/wmts/qgswmtsgetcapabilities.cpp index 1f5630f65fb0..986da3790128 100644 --- a/src/server/services/wmts/qgswmtsgetcapabilities.cpp +++ b/src/server/services/wmts/qgswmtsgetcapabilities.cpp @@ -320,12 +320,12 @@ namespace QgsWmts */ QDomElement contentsElement = doc.createElement( QStringLiteral( "Contents" )/*wmts:Contents*/ ); - QList< tileMatrixSet > tmsList = getTileMatrixSetList( project ); + QList< tileMatrixSetDef > tmsList = getTileMatrixSetList( project ); if ( !tmsList.isEmpty() ) { QList< layerDef > wmtsLayers; QgsCoordinateReferenceSystem wgs84 = QgsCoordinateReferenceSystem::fromOgcWmsCrs( GEO_EPSG_CRS_AUTHID ); - QList::iterator tmsIt = tmsList.begin(); + QList::iterator tmsIt = tmsList.begin(); QStringList nonIdentifiableLayers = project->nonIdentifiableLayers(); @@ -547,7 +547,7 @@ namespace QgsWmts tmsIt = tmsList.begin(); for ( ; tmsIt != tmsList.end(); ++tmsIt ) { - tileMatrixSet &tms = *tmsIt; + tileMatrixSetDef &tms = *tmsIt; if ( tms.ref == QLatin1String( "EPSG:4326" ) ) continue; @@ -609,7 +609,7 @@ namespace QgsWmts tmsIt = tmsList.begin(); for ( ; tmsIt != tmsList.end(); ++tmsIt ) { - tileMatrixSet &tms = *tmsIt; + tileMatrixSetDef &tms = *tmsIt; if ( tms.ref != QLatin1String( "EPSG:4326" ) ) { QgsRectangle rect; @@ -636,10 +636,10 @@ namespace QgsWmts //wmts:TileMatrixSetLimits QDomElement tmsLimitsElement = doc.createElement( QStringLiteral( "TileMatrixSetLimits" )/*wmts:TileMatrixSetLimits*/ ); int tmIdx = 0; - QList::iterator tmIt = tms.tileMatrixList.begin(); + QList::iterator tmIt = tms.tileMatrixList.begin(); for ( ; tmIt != tms.tileMatrixList.end(); ++tmIt ) { - tileMatrix &tm = *tmIt; + tileMatrixDef &tm = *tmIt; QDomElement tmLimitsElement = doc.createElement( QStringLiteral( "TileMatrixLimits" )/*wmts:TileMatrixLimits*/ ); @@ -683,7 +683,7 @@ namespace QgsWmts tmsIt = tmsList.begin(); for ( ; tmsIt != tmsList.end(); ++tmsIt ) { - tileMatrixSet &tms = *tmsIt; + tileMatrixSetDef &tms = *tmsIt; //wmts:TileMatrixSet QDomElement tmsElement = doc.createElement( QStringLiteral( "TileMatrixSet" )/*wmts:TileMatrixSet*/ ); @@ -700,10 +700,10 @@ namespace QgsWmts //wmts:TileMatrix int tmIdx = 0; - QList::iterator tmIt = tms.tileMatrixList.begin(); + QList::iterator tmIt = tms.tileMatrixList.begin(); for ( ; tmIt != tms.tileMatrixList.end(); ++tmIt ) { - tileMatrix &tm = *tmIt; + tileMatrixDef &tm = *tmIt; QDomElement tmElement = doc.createElement( QStringLiteral( "TileMatrix" )/*wmts:TileMatrix*/ ); diff --git a/src/server/services/wmts/qgswmtsgetfeatureinfo.cpp b/src/server/services/wmts/qgswmtsgetfeatureinfo.cpp index 1212ee3b7685..1b86a2d600fb 100644 --- a/src/server/services/wmts/qgswmtsgetfeatureinfo.cpp +++ b/src/server/services/wmts/qgswmtsgetfeatureinfo.cpp @@ -1,5 +1,5 @@ /*************************************************************************** - qgswmsgetfeatureinfo.cpp + qgswmtsgetfeatureinfo.cpp ------------------------- begin : July 23 , 2017 copyright : (C) 2018 by René-Luc D'Hont @@ -15,6 +15,7 @@ * * ***************************************************************************/ #include "qgswmtsutils.h" +#include "qgswmtsparameters.h" #include "qgswmtsgetfeatureinfo.h" #include @@ -27,17 +28,16 @@ namespace QgsWmts QgsServerResponse &response ) { Q_UNUSED( version ); - - QgsServerRequest::Parameters params = request.parameters(); + const QgsWmtsParameters params( QUrlQuery( request.url() ) ); // WMS query QUrlQuery query = translateWmtsParamToWmsQueryItem( QStringLiteral( "GetFeatureInfo" ), params, project, serverIface ); // GetFeatureInfo query items - query.addQueryItem( QStringLiteral( "query_layers" ), query.queryItemValue( QStringLiteral( "layers" ) ) ); - query.addQueryItem( QStringLiteral( "i" ), params.value( QStringLiteral( "I" ) ) ); - query.addQueryItem( QStringLiteral( "j" ), params.value( QStringLiteral( "J" ) ) ); - query.addQueryItem( QStringLiteral( "info_format" ), params.value( QStringLiteral( "INFOFORMAT" ) ) ); + query.addQueryItem( QStringLiteral( "query_layers" ), params.layer() ); + query.addQueryItem( QgsWmtsParameter::name( QgsWmtsParameter::I ), params.i() ); + query.addQueryItem( QgsWmtsParameter::name( QgsWmtsParameter::J ), params.j() ); + query.addQueryItem( QStringLiteral( "info_format" ), params.infoFormatAsString() ); QgsServerParameters wmsParams( query ); QgsServerRequest wmsRequest( "?" + query.query( QUrl::FullyDecoded ) ); diff --git a/src/server/services/wmts/qgswmtsgetfeatureinfo.h b/src/server/services/wmts/qgswmtsgetfeatureinfo.h index a64c6dcfd698..af1e3fd1f639 100644 --- a/src/server/services/wmts/qgswmtsgetfeatureinfo.h +++ b/src/server/services/wmts/qgswmtsgetfeatureinfo.h @@ -1,5 +1,5 @@ /*************************************************************************** - qgswmsgetfeatureinfo.h + qgswmtsgetfeatureinfo.h ------------------------- begin : July 23 , 2017 copyright : (C) 2018 by René-Luc D'Hont diff --git a/src/server/services/wmts/qgswmtsgettile.cpp b/src/server/services/wmts/qgswmtsgettile.cpp index 363719dcc93f..5da4ad27184d 100644 --- a/src/server/services/wmts/qgswmtsgettile.cpp +++ b/src/server/services/wmts/qgswmtsgettile.cpp @@ -15,6 +15,7 @@ * * ***************************************************************************/ #include "qgswmtsutils.h" +#include "qgswmtsparameters.h" #include "qgswmtsgettile.h" #include @@ -27,7 +28,8 @@ namespace QgsWmts QgsServerResponse &response ) { Q_UNUSED( version ); - QgsServerRequest::Parameters params = request.parameters(); + //QgsServerRequest::Parameters params = request.parameters(); + const QgsWmtsParameters params( QUrlQuery( request.url() ) ); // WMS query QUrlQuery query = translateWmtsParamToWmsQueryItem( QStringLiteral( "GetMap" ), params, project, serverIface ); @@ -44,16 +46,19 @@ namespace QgsWmts QgsServerCacheManager *cacheManager = serverIface->cacheManager(); if ( cacheManager && cache ) { - QString contentType = params.value( QStringLiteral( "FORMAT" ) ); + QgsWmtsParameters::Format f = params.format(); + QString contentType; QString saveFormat; std::unique_ptr image; - if ( contentType == QLatin1String( "image/jpeg" ) ) + if ( f == QgsWmtsParameters::Format::JPG ) { + contentType = QStringLiteral( "image/jpeg" ); saveFormat = QStringLiteral( "JPEG" ); image = qgis::make_unique( 256, 256, QImage::Format_RGB32 ); } else { + contentType = QStringLiteral( "image/png" ); saveFormat = QStringLiteral( "PNG" ); image = qgis::make_unique( 256, 256, QImage::Format_ARGB32_Premultiplied ); } diff --git a/src/server/services/wmts/qgswmtsparameters.cpp b/src/server/services/wmts/qgswmtsparameters.cpp new file mode 100644 index 000000000000..a61761b87715 --- /dev/null +++ b/src/server/services/wmts/qgswmtsparameters.cpp @@ -0,0 +1,295 @@ +/*************************************************************************** + qgswmtsparameters.cpp + -------------------- + begin : Aug 10, 2018 + copyright : (C) 2018 by René-Luc Dhont + email : rldhont at 3liz dot 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 "qgswmtsparameters.h" +#include "qgsmessagelog.h" +#include + +namespace QgsWmts +{ + // + // QgsWmtsParameter + // + QgsWmtsParameter::QgsWmtsParameter( const QgsWmtsParameter::Name name, + const QVariant::Type type, + const QVariant defaultValue ) + : QgsServerParameterDefinition( type, defaultValue ) + , mName( name ) + { + } + + int QgsWmtsParameter::toInt() const + { + bool ok = false; + const int val = QgsServerParameterDefinition::toInt( ok ); + + if ( !ok ) + { + raiseError(); + } + + return val; + } + + void QgsWmtsParameter::raiseError() const + { + const QString msg = QString( "%1 ('%2') cannot be converted into %3" ).arg( name( mName ), toString(), typeName() ); + QgsServerParameterDefinition::raiseError( msg ); + } + + QString QgsWmtsParameter::name( const QgsWmtsParameter::Name name ) + { + const QMetaEnum metaEnum( QMetaEnum::fromType() ); + return metaEnum.valueToKey( name ); + } + + QgsWmtsParameter::Name QgsWmtsParameter::name( const QString &name ) + { + const QMetaEnum metaEnum( QMetaEnum::fromType() ); + return ( QgsWmtsParameter::Name ) metaEnum.keyToValue( name.toUpper().toStdString().c_str() ); + } + + // + // QgsWmtsParameters + // + QgsWmtsParameters::QgsWmtsParameters() + : QgsServerParameters() + { + // Available version number + mVersions.append( QgsProjectVersion( 1, 0, 0 ) ); + + const QgsWmtsParameter pLayer = QgsWmtsParameter( QgsWmtsParameter::LAYER ); + save( pLayer ); + + const QgsWmtsParameter pFormat = QgsWmtsParameter( QgsWmtsParameter::FORMAT ); + save( pFormat ); + + const QgsWmtsParameter pTileMatrix = QgsWmtsParameter( QgsWmtsParameter::TILEMATRIX, + QVariant::Int, + QVariant( -1 ) ); + save( pTileMatrix ); + + const QgsWmtsParameter pTileRow = QgsWmtsParameter( QgsWmtsParameter::TILEROW, + QVariant::Int, + QVariant( -1 ) ); + save( pTileRow ); + + const QgsWmtsParameter pTileCol = QgsWmtsParameter( QgsWmtsParameter::TILECOL, + QVariant::Int, + QVariant( -1 ) ); + save( pTileCol ); + + const QgsWmtsParameter pInfoFormat( QgsWmtsParameter::INFOFORMAT ); + save( pInfoFormat ); + + const QgsWmtsParameter pI( QgsWmtsParameter::I, + QVariant::Int, + QVariant( -1 ) ); + save( pI ); + + const QgsWmtsParameter pJ( QgsWmtsParameter::J, + QVariant::Int, + QVariant( -1 ) ); + save( pJ ); + } + + QgsWmtsParameters::QgsWmtsParameters( const QgsServerParameters ¶meters ) + : QgsWmtsParameters() + { + load( parameters.urlQuery() ); + } + + bool QgsWmtsParameters::loadParameter( const QString &key, const QString &value ) + { + bool loaded = false; + + const QgsWmtsParameter::Name name = QgsWmtsParameter::name( key ); + if ( name >= 0 ) + { + mWmtsParameters[name].mValue = value; + if ( ! mWmtsParameters[name].isValid() ) + { + mWmtsParameters[name].raiseError(); + } + + loaded = true; + } + + return loaded; + } + + void QgsWmtsParameters::save( const QgsWmtsParameter ¶meter ) + { + mWmtsParameters[ parameter.mName ] = parameter; + } + + void QgsWmtsParameters::dump() const + { + log( "WMTS Request parameters:" ); + for ( auto parameter : mWmtsParameters.toStdMap() ) + { + const QString value = parameter.second.toString(); + + if ( ! value.isEmpty() ) + { + const QString name = QgsWmtsParameter::name( parameter.first ); + log( QStringLiteral( " - %1 : %2" ).arg( name, value ) ); + } + } + + if ( !version().isEmpty() ) + log( QStringLiteral( " - VERSION : %1" ).arg( version() ) ); + } + + QString QgsWmtsParameters::layer() const + { + return mWmtsParameters[ QgsWmtsParameter::LAYER ].toString(); + } + + QString QgsWmtsParameters::formatAsString() const + { + return mWmtsParameters[ QgsWmtsParameter::FORMAT ].toString(); + } + + QgsWmtsParameters::Format QgsWmtsParameters::format() const + { + QString fStr = formatAsString(); + + if ( fStr.isEmpty() ) + return Format::NONE; + + Format f = Format::PNG; + if ( fStr.compare( QLatin1String( "jpg" ), Qt::CaseInsensitive ) == 0 + || fStr.compare( QLatin1String( "jpeg" ), Qt::CaseInsensitive ) == 0 + || fStr.compare( QLatin1String( "image/jpeg" ), Qt::CaseInsensitive ) == 0 ) + f = Format::JPG; + + return f; + } + + QString QgsWmtsParameters::tileMatrixSet() const + { + return mWmtsParameters[ QgsWmtsParameter::TILEMATRIXSET ].toString(); + } + + QString QgsWmtsParameters::tileMatrix() const + { + return mWmtsParameters[ QgsWmtsParameter::TILEMATRIX ].toString(); + } + + int QgsWmtsParameters::tileMatrixAsInt() const + { + return mWmtsParameters[ QgsWmtsParameter::TILEMATRIX ].toInt(); + } + + QString QgsWmtsParameters::tileRow() const + { + return mWmtsParameters[ QgsWmtsParameter::TILEROW ].toString(); + } + + int QgsWmtsParameters::tileRowAsInt() const + { + return mWmtsParameters[ QgsWmtsParameter::TILEROW ].toInt(); + } + + QString QgsWmtsParameters::tileCol() const + { + return mWmtsParameters[ QgsWmtsParameter::TILECOL ].toString(); + } + + int QgsWmtsParameters::tileColAsInt() const + { + return mWmtsParameters[ QgsWmtsParameter::TILECOL ].toInt(); + } + + QString QgsWmtsParameters::infoFormatAsString() const + { + return mWmtsParameters[ QgsWmtsParameter::INFOFORMAT ].toString(); + } + + QgsWmtsParameters::Format QgsWmtsParameters::infoFormat() const + { + QString fStr = infoFormatAsString(); + + Format f = Format::TEXT; + if ( fStr.isEmpty() ) + return f; + + if ( fStr.startsWith( QLatin1String( "text/xml" ), Qt::CaseInsensitive ) ) + f = Format::XML; + else if ( fStr.startsWith( QLatin1String( "text/html" ), Qt::CaseInsensitive ) ) + f = Format::HTML; + else if ( fStr.startsWith( QLatin1String( "text/plain" ), Qt::CaseInsensitive ) ) + f = Format::TEXT; + else if ( fStr.startsWith( QLatin1String( "application/vnd.ogc.gml" ), Qt::CaseInsensitive ) ) + f = Format::GML; + else + f = Format::NONE; + + return f; + } + + int QgsWmtsParameters::infoFormatVersion() const + { + if ( infoFormat() != Format::GML ) + return -1; + + QString fStr = infoFormatAsString(); + if ( fStr.startsWith( QLatin1String( "application/vnd.ogc.gml/3" ), Qt::CaseInsensitive ) ) + return 3; + else + return 2; + } + + QString QgsWmtsParameters::i() const + { + return mWmtsParameters[ QgsWmtsParameter::I ].toString(); + } + + QString QgsWmtsParameters::j() const + { + return mWmtsParameters[ QgsWmtsParameter::J ].toString(); + } + + int QgsWmtsParameters::iAsInt() const + { + return mWmtsParameters[ QgsWmtsParameter::I ].toInt(); + } + + int QgsWmtsParameters::jAsInt() const + { + return mWmtsParameters[ QgsWmtsParameter::J ].toInt(); + } + + QgsProjectVersion QgsWmtsParameters::versionAsNumber() const + { + QString vStr = version(); + QgsProjectVersion version; + + if ( vStr.isEmpty() ) + version = QgsProjectVersion( 1, 0, 0 ); // default value + else if ( mVersions.contains( QgsProjectVersion( vStr ) ) ) + version = QgsProjectVersion( vStr ); + + return version; + } + + void QgsWmtsParameters::log( const QString &msg ) const + { + QgsMessageLog::logMessage( msg, "Server", Qgis::Info ); + } +} diff --git a/src/server/services/wmts/qgswmtsparameters.h b/src/server/services/wmts/qgswmtsparameters.h new file mode 100644 index 000000000000..187fb86a9885 --- /dev/null +++ b/src/server/services/wmts/qgswmtsparameters.h @@ -0,0 +1,285 @@ +/*************************************************************************** + qgswmtsparameters.h + ------------------- + begin : Aug 10, 2018 + copyright : (C) 2018 by René-Luc Dhont + email : rldhont at 3liz dot 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 QGSWMTSPARAMETERS_H +#define QGSWMTSPARAMETERS_H + +#include +#include +#include + +#include "qgswmtsserviceexception.h" +#include "qgsserverrequest.h" +#include "qgsprojectversion.h" +#include "qgsserverparameters.h" + +namespace QgsWmts +{ + + /** + * \ingroup server + * \class QgsWmts::QgsWmtsParameter + * \brief WMTS parameter received from the client. + * \since QGIS 3.4 + */ + class QgsWmtsParameter : public QgsServerParameterDefinition + { + Q_GADGET + + public: + //! Available parameters for WMTS requests + enum Name + { + UNKNOWN, + LAYER, + FORMAT, + TILEMATRIXSET, + TILEMATRIX, + TILEROW, + TILECOL, + INFOFORMAT, + I, + J + }; + Q_ENUM( Name ) + + /** + * Constructor for QgsWmtsParameter. + * \param name Name of the WMS parameter + * \param type Type of the parameter + * \param defaultValue Default value of the parameter + */ + QgsWmtsParameter( const QgsWmtsParameter::Name name = QgsWmtsParameter::UNKNOWN, + const QVariant::Type type = QVariant::String, + const QVariant defaultValue = QVariant( "" ) ); + + /** + * Default destructor for QgsWmtsParameter. + */ + virtual ~QgsWmtsParameter() = default; + + /** + * Converts the parameter into an integer. + * \returns An integer + * \throws QgsBadRequestException Invalid parameter exception + */ + int toInt() const; + + /** + * Raises an error in case of an invalid conversion. + * \throws QgsBadRequestException Invalid parameter exception + */ + void raiseError() const; + + /** + * Converts a parameter's name into its string representation. + */ + static QString name( const QgsWmtsParameter::Name ); + + /** + * Converts a string into a parameter's name (UNKNOWN in case of an + * invalid string). + */ + static QgsWmtsParameter::Name name( const QString &name ); + + QgsWmtsParameter::Name mName; + }; + + + /** + * \ingroup server + * \class QgsWmts::QgsWmtsParameters + * \brief Provides an interface to retrieve and manipulate WMTS parameters received from the client. + * \since QGIS 3.4 + */ + class QgsWmtsParameters : public QgsServerParameters + { + Q_GADGET + + public: + + //! Output format for the response + enum Format + { + NONE, + JPG, + PNG, + TEXT, + XML, + HTML, + GML + }; + + /** + * Constructor for WMTS parameters with specific values. + * \param parameters Map of parameters where keys are parameters' names. + */ + QgsWmtsParameters( const QgsServerParameters ¶meters ); + + /** + * Constructor for WMTS parameters with default values only. + */ + QgsWmtsParameters(); + + /** + * Default destructor for QgsWmtsParameters. + */ + virtual ~QgsWmtsParameters() = default; + + /** + * Dumps parameters. + */ + void dump() const; + + /** + * Returns VERSION parameter if defined or its default value. + * \returns version + */ + QgsProjectVersion versionAsNumber() const; + + /** + * Returns LAYER parameter as a string. + * \returns layer parameter as string + */ + QString layer() const; + + /** + * Returns FORMAT parameter as a string. + * \returns Format parameter as string + */ + QString formatAsString() const; + + /** + * Returns format. If the FORMAT parameter is not used, then the + * default value is NONE. + * \returns format + */ + Format format() const; + + /** + * Returns TILEMATRIXSET parameter as a string. + * \returns tileMatrixSet parameter as string + */ + QString tileMatrixSet() const; + + /** + * Returns TILEMATRIX parameter as a string. + * \returns tileMatrix parameter as string + */ + QString tileMatrix() const; + + /** + * Returns TILEMATRIX parameter as an int or its default value if not + * defined. An exception is raised if TILEMATRIX is defined and cannot be + * converted. + * \returns tileMatrix parameter + * \throws QgsBadRequestException + */ + int tileMatrixAsInt() const; + + /** + * Returns TILEROW parameter as a string. + * \returns tileRow parameter as string + */ + QString tileRow() const; + + /** + * Returns TILEROW parameter as an int or its default value if not + * defined. An exception is raised if TILEROW is defined and cannot be + * converted. + * \returns tileRow parameter + * \throws QgsBadRequestException + */ + int tileRowAsInt() const; + + /** + * Returns TILECOL parameter as a string. + * \returns tileCol parameter as string + */ + QString tileCol() const; + + /** + * Returns TILECOL parameter as an int or its default value if not + * defined. An exception is raised if TILECOL is defined and cannot be + * converted. + * \returns tileCol parameter + * \throws QgsBadRequestException + */ + int tileColAsInt() const; + + /** + * Returns INFO_FORMAT parameter as a string. + * \returns INFO_FORMAT parameter as string + */ + QString infoFormatAsString() const; + + /** + * Returns infoFormat. If the INFO_FORMAT parameter is not used, then the + * default value is text/plain. + * \returns infoFormat + */ + Format infoFormat() const; + + /** + * Returns the infoFormat version for GML. If the INFO_FORMAT is not GML, + * then the default value is -1. + * \returns infoFormat version + */ + int infoFormatVersion() const; + + /** + * Returns I parameter or an empty string if not defined. + * \returns i parameter + */ + QString i() const; + + /** + * Returns I parameter as an int or its default value if not + * defined. An exception is raised if I is defined and cannot be + * converted. + * \returns i parameter + * \throws QgsBadRequestException + */ + int iAsInt() const; + + /** + * Returns J parameter or an empty string if not defined. + * \returns j parameter + */ + QString j() const; + + /** + * Returns J parameter as an int or its default value if not + * defined. An exception is raised if J is defined and cannot be + * converted. + * \returns j parameter + * \throws QgsBadRequestException + */ + int jAsInt() const; + + private: + bool loadParameter( const QString &name, const QString &key ) override; + void save( const QgsWmtsParameter ¶meter ); + + void log( const QString &msg ) const; + + QList mVersions; + QMap mWmtsParameters; + }; +} + +#endif diff --git a/src/server/services/wmts/qgswmtsutils.cpp b/src/server/services/wmts/qgswmtsutils.cpp index 3aaea8875078..5d5f209d846e 100644 --- a/src/server/services/wmts/qgswmtsutils.cpp +++ b/src/server/services/wmts/qgswmtsutils.cpp @@ -16,6 +16,7 @@ ***************************************************************************/ #include "qgswmtsutils.h" +#include "qgswmtsparameters.h" #include "qgsconfigcache.h" #include "qgsserverprojectutils.h" @@ -65,39 +66,20 @@ namespace QgsWmts if ( href.isEmpty() ) { QUrl url = request.url(); - QUrlQuery q( url ); - q.removeAllQueryItems( QStringLiteral( "REQUEST" ) ); - q.removeAllQueryItems( QStringLiteral( "VERSION" ) ); - q.removeAllQueryItems( QStringLiteral( "SERVICE" ) ); - q.removeAllQueryItems( QStringLiteral( "_DC" ) ); + QgsWmtsParameters params; + params.load( QUrlQuery( url ) ); + params.remove( QgsServerParameter::REQUEST ); + params.remove( QgsServerParameter::VERSION_SERVICE ); + params.remove( QgsServerParameter::SERVICE ); - url.setQuery( q ); + url.setQuery( params.urlQuery() ); href = url.toString( QUrl::FullyDecoded ); - } return href; } - QgsRectangle parseBbox( const QString &bboxStr ) - { - QStringList lst = bboxStr.split( ',' ); - if ( lst.count() != 4 ) - return QgsRectangle(); - - double d[4]; - bool ok; - for ( int i = 0; i < 4; i++ ) - { - lst[i].replace( ' ', '+' ); - d[i] = lst[i].toDouble( &ok ); - if ( !ok ) - return QgsRectangle(); - } - return QgsRectangle( d[0], d[1], d[2], d[3] ); - } - tileMatrixInfo getTileMatrixInfo( const QString &crsStr, const QgsProject *project ) { if ( tileMatrixInfoMap.contains( crsStr ) ) @@ -133,9 +115,9 @@ namespace QgsWmts return tmi; } - tileMatrixSet getTileMatrixSet( tileMatrixInfo tmi, double minScale ) + tileMatrixSetDef getTileMatrixSet( tileMatrixInfo tmi, double minScale ) { - QList< tileMatrix > tileMatrixList; + QList< tileMatrixDef > tileMatrixList; double scaleDenominator = tmi.scaleDenominator; QgsRectangle extent = tmi.extent; QgsUnitTypes::DistanceUnit unit = tmi.unit; @@ -149,7 +131,7 @@ namespace QgsWmts double left = ( extent.xMinimum() + ( extent.xMaximum() - extent.xMinimum() ) / 2.0 ) - ( col / 2.0 ) * ( tileWidth * res ); double top = ( extent.yMinimum() + ( extent.yMaximum() - extent.yMinimum() ) / 2.0 ) + ( row / 2.0 ) * ( tileHeight * res ); - tileMatrix tm; + tileMatrixDef tm; tm.resolution = res; tm.scaleDenominator = scale; tm.col = col; @@ -161,7 +143,7 @@ namespace QgsWmts scaleDenominator = scale / 2; } - tileMatrixSet tms; + tileMatrixSetDef tms; tms.ref = tmi.ref; tms.extent = extent; tms.unit = unit; @@ -206,9 +188,9 @@ namespace QgsWmts return scale; } - QList< tileMatrixSet > getTileMatrixSetList( const QgsProject *project ) + QList< tileMatrixSetDef > getTileMatrixSetList( const QgsProject *project ) { - QList< tileMatrixSet > tmsList; + QList< tileMatrixSetDef > tmsList; double minScale = getProjectMinScale( project ); @@ -225,18 +207,13 @@ namespace QgsWmts return tmsList; } - QUrlQuery translateWmtsParamToWmsQueryItem( const QString &request, const QgsServerRequest::Parameters ¶ms, + QUrlQuery translateWmtsParamToWmsQueryItem( const QString &request, const QgsWmtsParameters ¶ms, const QgsProject *project, QgsServerInterface *serverIface ) { //defining Layer - QString layer; + QString layer = params.layer(); //read Layer - QMap::const_iterator layer_it = params.constFind( QStringLiteral( "LAYER" ) ); - if ( layer_it != params.constEnd() ) - { - layer = layer_it.value(); - } - else + if ( layer.isEmpty() ) { throw QgsRequestNotWellFormedException( QStringLiteral( "Layer is mandatory" ) ); } @@ -309,27 +286,17 @@ namespace QgsWmts } //defining Format - QString format; + QString format = params.formatAsString(); //read Format - QMap::const_iterator format_it = params.constFind( QStringLiteral( "FORMAT" ) ); - if ( format_it != params.constEnd() ) - { - format = format_it.value(); - } - else + if ( format.isEmpty() ) { throw QgsRequestNotWellFormedException( QStringLiteral( "Format is mandatory" ) ); } //defining TileMatrixSet ref - QString tms_ref; + QString tms_ref = params.tileMatrixSet(); //read TileMatrixSet - QMap::const_iterator tms_ref_it = params.constFind( QStringLiteral( "TILEMATRIXSET" ) ); - if ( tms_ref_it != params.constEnd() ) - { - tms_ref = tms_ref_it.value(); - } - else + if ( tms_ref.isEmpty() ) { throw QgsRequestNotWellFormedException( QStringLiteral( "TileMatrixSet is mandatory" ) ); } @@ -346,24 +313,12 @@ namespace QgsWmts { throw QgsRequestNotWellFormedException( QStringLiteral( "TileMatrixSet is unknown" ) ); } - tileMatrixSet tms = getTileMatrixSet( tmi, getProjectMinScale( project ) ); - - bool conversionSuccess = false; + tileMatrixSetDef tms = getTileMatrixSet( tmi, getProjectMinScale( project ) ); //difining TileMatrix idx - int tm_idx; + int tm_idx = params.tileMatrixAsInt(); //read TileMatrix - QMap::const_iterator tm_ref_it = params.constFind( QStringLiteral( "TILEMATRIX" ) ); - if ( tm_ref_it != params.constEnd() ) - { - QString tm_ref = tm_ref_it.value(); - tm_idx = tm_ref.toInt( &conversionSuccess ); - if ( !conversionSuccess ) - { - throw QgsRequestNotWellFormedException( QStringLiteral( "TileMatrix is unknown" ) ); - } - } - else + if ( tm_idx == -1 ) { throw QgsRequestNotWellFormedException( QStringLiteral( "TileMatrix is mandatory" ) ); } @@ -371,23 +326,12 @@ namespace QgsWmts { throw QgsRequestNotWellFormedException( QStringLiteral( "TileMatrix is unknown" ) ); } - tileMatrix tm = tms.tileMatrixList.at( tm_idx ); + tileMatrixDef tm = tms.tileMatrixList.at( tm_idx ); //defining TileRow - int tr; + int tr = params.tileRowAsInt(); //read TileRow - QMap::const_iterator tr_it = params.constFind( QStringLiteral( "TILEROW" ) ); - if ( tr_it != params.constEnd() ) - { - QString tr_str = tr_it.value(); - conversionSuccess = false; - tr = tr_str.toInt( &conversionSuccess ); - if ( !conversionSuccess ) - { - throw QgsRequestNotWellFormedException( QStringLiteral( "TileRow is unknown" ) ); - } - } - else + if ( tr == -1 ) { throw QgsRequestNotWellFormedException( QStringLiteral( "TileRow is mandatory" ) ); } @@ -397,20 +341,9 @@ namespace QgsWmts } //defining TileCol - int tc; + int tc = params.tileColAsInt(); //read TileCol - QMap::const_iterator tc_it = params.constFind( QStringLiteral( "TILECOL" ) ); - if ( tc_it != params.constEnd() ) - { - QString tc_str = tc_it.value(); - conversionSuccess = false; - tc = tc_str.toInt( &conversionSuccess ); - if ( !conversionSuccess ) - { - throw QgsRequestNotWellFormedException( QStringLiteral( "TileCol is unknown" ) ); - } - } - else + if ( tc == -1 ) { throw QgsRequestNotWellFormedException( QStringLiteral( "TileCol is mandatory" ) ); } @@ -457,7 +390,7 @@ namespace QgsWmts query.addQueryItem( QStringLiteral( "width" ), QStringLiteral( "256" ) ); query.addQueryItem( QStringLiteral( "height" ), QStringLiteral( "256" ) ); query.addQueryItem( QStringLiteral( "format" ), format ); - if ( format.startsWith( QStringLiteral( "image/png" ) ) ) + if ( params.format() == QgsWmtsParameters::Format::PNG ) { query.addQueryItem( QStringLiteral( "transparent" ), QStringLiteral( "true" ) ); } diff --git a/src/server/services/wmts/qgswmtsutils.h b/src/server/services/wmts/qgswmtsutils.h index 6767dc6f24bf..a6d5a439012b 100644 --- a/src/server/services/wmts/qgswmtsutils.h +++ b/src/server/services/wmts/qgswmtsutils.h @@ -20,6 +20,7 @@ #define QGSWMTSUTILS_H #include "qgsmodule.h" +#include "qgswmtsparameters.h" #include "qgswmtsserviceexception.h" #include @@ -45,7 +46,7 @@ namespace QgsWmts QgsUnitTypes::DistanceUnit unit; }; - struct tileMatrix + struct tileMatrixDef { double resolution = 0.0; @@ -60,7 +61,7 @@ namespace QgsWmts double top = 0.0; }; - struct tileMatrixSet + struct tileMatrixSetDef { QString ref; @@ -68,7 +69,7 @@ namespace QgsWmts QgsUnitTypes::DistanceUnit unit; - QList< tileMatrix > tileMatrixList; + QList< tileMatrixDef > tileMatrixList; }; struct layerDef @@ -96,26 +97,20 @@ namespace QgsWmts */ QString serviceUrl( const QgsServerRequest &request, const QgsProject *project ); - /** - * Parse bounding box - */ - //XXX At some point, should be moved to common library - QgsRectangle parseBbox( const QString &bboxStr ); - // Define namespaces used in WMTS documents const QString WMTS_NAMESPACE = QStringLiteral( "http://www.opengis.net/wmts/1.0" ); const QString GML_NAMESPACE = QStringLiteral( "http://www.opengis.net/gml" ); const QString OWS_NAMESPACE = QStringLiteral( "http://www.opengis.net/ows/1.1" ); tileMatrixInfo getTileMatrixInfo( const QString &crsStr, const QgsProject *project ); - tileMatrixSet getTileMatrixSet( tileMatrixInfo tmi, double minScale ); + tileMatrixSetDef getTileMatrixSet( tileMatrixInfo tmi, double minScale ); double getProjectMinScale( const QgsProject *project ); - QList< tileMatrixSet > getTileMatrixSetList( const QgsProject *project ); + QList< tileMatrixSetDef > getTileMatrixSetList( const QgsProject *project ); /** * Translate WMTS parameters to WMS query item */ - QUrlQuery translateWmtsParamToWmsQueryItem( const QString &request, const QgsServerRequest::Parameters ¶ms, + QUrlQuery translateWmtsParamToWmsQueryItem( const QString &request, const QgsWmtsParameters ¶ms, const QgsProject *project, QgsServerInterface *serverIface ); } // namespace QgsWmts From a53717c1535ddb3ca6cdf6f65fef2a5b11f25aef Mon Sep 17 00:00:00 2001 From: rldhont Date: Wed, 15 Aug 2018 16:48:27 +0200 Subject: [PATCH 22/33] [Server][Feature][needs-docs] Enhancing WMTS GetCapabilities code --- .../services/wmts/qgswmtsgetcapabilities.cpp | 75 ++++++++++++------- 1 file changed, 49 insertions(+), 26 deletions(-) diff --git a/src/server/services/wmts/qgswmtsgetcapabilities.cpp b/src/server/services/wmts/qgswmtsgetcapabilities.cpp index 986da3790128..d010c5f7609e 100644 --- a/src/server/services/wmts/qgswmtsgetcapabilities.cpp +++ b/src/server/services/wmts/qgswmtsgetcapabilities.cpp @@ -30,6 +30,17 @@ namespace QgsWmts { + namespace + { + QList< layerDef > getWmtsLayerList( QgsServerInterface *serverIface, const QgsProject *project ); + + void appendLayerElements( QDomDocument &doc, QDomElement &contentsElement, + QList< layerDef > wmtsLayers, QList< tileMatrixSetDef > tmsList, + const QgsProject *project ); + + void appendTileMatrixSetElements( QDomDocument &doc, QDomElement &contentsElement, + QList< tileMatrixSetDef > tmsList ); + } /** * Output WMTS GetCapabilities response @@ -312,9 +323,6 @@ namespace QgsWmts QDomElement getContentsElement( QDomDocument &doc, QgsServerInterface *serverIface, const QgsProject *project ) { -#ifdef HAVE_SERVER_PYTHON_PLUGINS - QgsAccessControl *accessControl = serverIface->accessControls(); -#endif /* * Adding layer list in ContentMetadata */ @@ -322,10 +330,29 @@ namespace QgsWmts QList< tileMatrixSetDef > tmsList = getTileMatrixSetList( project ); if ( !tmsList.isEmpty() ) + { + // get layer list + QList< layerDef > wmtsLayers = getWmtsLayerList( serverIface, project ); + if ( !wmtsLayers.isEmpty() ) + { + appendLayerElements( doc, contentsElement, wmtsLayers, tmsList, project ); + } + + appendTileMatrixSetElements( doc, contentsElement, tmsList ); + } + + //End + return contentsElement; + } + namespace + { + QList< layerDef > getWmtsLayerList( QgsServerInterface *serverIface, const QgsProject *project ) { QList< layerDef > wmtsLayers; +#ifdef HAVE_SERVER_PYTHON_PLUGINS + QgsAccessControl *accessControl = serverIface->accessControls(); +#endif QgsCoordinateReferenceSystem wgs84 = QgsCoordinateReferenceSystem::fromOgcWmsCrs( GEO_EPSG_CRS_AUTHID ); - QList::iterator tmsIt = tmsList.begin(); QStringList nonIdentifiableLayers = project->nonIdentifiableLayers(); @@ -492,8 +519,15 @@ namespace QgsWmts wmtsLayers.append( pLayer ); } + return wmtsLayers; + } - // Append InfoFormat helper + void appendLayerElements( QDomDocument &doc, QDomElement &contentsElement, + QList< layerDef > wmtsLayers, QList< tileMatrixSetDef > tmsList, + const QgsProject *project ) + { + QgsCoordinateReferenceSystem wgs84 = QgsCoordinateReferenceSystem::fromOgcWmsCrs( GEO_EPSG_CRS_AUTHID ); + // Define InfoFormat helper std::function < void ( QDomElement &, const QString & ) > appendInfoFormat = [&doc]( QDomElement & elem, const QString & format ) { QDomElement formatElem = doc.createElement( QStringLiteral( "InfoFormat" )/*wmts:InfoFormat*/ ); @@ -544,10 +578,8 @@ namespace QgsWmts layerElem.appendChild( wgs84BBoxElement ); // Other bounding boxes - tmsIt = tmsList.begin(); - for ( ; tmsIt != tmsList.end(); ++tmsIt ) + for ( tileMatrixSetDef tms : tmsList ) { - tileMatrixSetDef &tms = *tmsIt; if ( tms.ref == QLatin1String( "EPSG:4326" ) ) continue; @@ -606,10 +638,8 @@ namespace QgsWmts appendInfoFormat( layerElem, QStringLiteral( "application/vnd.ogc.gml/3.1.1" ) ); } - tmsIt = tmsList.begin(); - for ( ; tmsIt != tmsList.end(); ++tmsIt ) + for ( tileMatrixSetDef tms : tmsList ) { - tileMatrixSetDef &tms = *tmsIt; if ( tms.ref != QLatin1String( "EPSG:4326" ) ) { QgsRectangle rect; @@ -636,11 +666,8 @@ namespace QgsWmts //wmts:TileMatrixSetLimits QDomElement tmsLimitsElement = doc.createElement( QStringLiteral( "TileMatrixSetLimits" )/*wmts:TileMatrixSetLimits*/ ); int tmIdx = 0; - QList::iterator tmIt = tms.tileMatrixList.begin(); - for ( ; tmIt != tms.tileMatrixList.end(); ++tmIt ) + for ( tileMatrixDef tm : tms.tileMatrixList ) { - tileMatrixDef &tm = *tmIt; - QDomElement tmLimitsElement = doc.createElement( QStringLiteral( "TileMatrixLimits" )/*wmts:TileMatrixLimits*/ ); QDomElement tmIdentifierElem = doc.createElement( QStringLiteral( "TileMatrix" ) ); @@ -679,12 +706,13 @@ namespace QgsWmts contentsElement.appendChild( layerElem ); } + } - tmsIt = tmsList.begin(); - for ( ; tmsIt != tmsList.end(); ++tmsIt ) + void appendTileMatrixSetElements( QDomDocument &doc, QDomElement &contentsElement, + QList< tileMatrixSetDef > tmsList ) + { + for ( tileMatrixSetDef tms : tmsList ) { - tileMatrixSetDef &tms = *tmsIt; - //wmts:TileMatrixSet QDomElement tmsElement = doc.createElement( QStringLiteral( "TileMatrixSet" )/*wmts:TileMatrixSet*/ ); @@ -700,11 +728,8 @@ namespace QgsWmts //wmts:TileMatrix int tmIdx = 0; - QList::iterator tmIt = tms.tileMatrixList.begin(); - for ( ; tmIt != tms.tileMatrixList.end(); ++tmIt ) + for ( tileMatrixDef tm : tms.tileMatrixList ) { - tileMatrixDef &tm = *tmIt; - QDomElement tmElement = doc.createElement( QStringLiteral( "TileMatrix" )/*wmts:TileMatrix*/ ); QDomElement tmIdentifierElem = doc.createElement( QStringLiteral( "ows:Identifier" ) ); @@ -750,9 +775,7 @@ namespace QgsWmts } } - //End - return contentsElement; - } + } // namespace } // namespace QgsWmts From c9409e515083530bf4009cf07576bf5060f05b65 Mon Sep 17 00:00:00 2001 From: rldhont Date: Mon, 20 Aug 2018 11:10:50 +0200 Subject: [PATCH 23/33] [Server][Feature][needs-docs] Update Cache manager API --- .../qgsservercachemanager.sip.in | 27 ++--- src/server/qgsservercachemanager.cpp | 106 ++++++++++++++++-- src/server/qgsservercachemanager.h | 69 ++++-------- .../services/wcs/qgswcsgetcapabilities.cpp | 36 +----- .../services/wfs/qgswfsgetcapabilities.cpp | 36 +----- .../wfs/qgswfsgetcapabilities_1_0_0.cpp | 36 +----- .../services/wms/qgswmsgetcapabilities.cpp | 79 +++++-------- .../services/wmts/qgswmtsgetcapabilities.cpp | 36 +----- src/server/services/wmts/qgswmtsgettile.cpp | 16 +-- .../src/python/test_qgsserver_cachemanager.py | 17 ++- 10 files changed, 196 insertions(+), 262 deletions(-) diff --git a/python/server/auto_generated/qgsservercachemanager.sip.in b/python/server/auto_generated/qgsservercachemanager.sip.in index f872e12ff30d..1a7bbbd4a20f 100644 --- a/python/server/auto_generated/qgsservercachemanager.sip.in +++ b/python/server/auto_generated/qgsservercachemanager.sip.in @@ -39,36 +39,37 @@ Copy constructor ~QgsServerCacheManager(); - QByteArray getCachedDocument( const QgsProject *project, const QgsServerRequest &request, const QString &key ) const; + bool getCachedDocument( QDomDocument *doc, const QgsProject *project, const QgsServerRequest &request, QgsAccessControl *accessControl ) const; %Docstring Returns cached document (or 0 if document not in cache) like capabilities +:param doc: the document to update by content found in cache :param project: the project used to generate the document to provide path :param request: the request used to generate the document to provider parameters or data -:param key: the key provided by the access control to identify different documents for the same request +:param accessControl: the access control to identify different documents for the same request provided by server interface -:return: the cached document or 0 if no corresponding document found +:return: true if the document has been found in cache and the document's content set %End - bool setCachedDocument( const QDomDocument *doc, const QgsProject *project, const QgsServerRequest &request, const QString &key ) const; + bool setCachedDocument( const QDomDocument *doc, const QgsProject *project, const QgsServerRequest &request, QgsAccessControl *accessControl ) const; %Docstring Updates or inserts the document in cache like capabilities :param doc: the document to cache :param project: the project used to generate the document to provide path :param request: the request used to generate the document to provider parameters or data -:param key: the key provided by the access control to identify different documents for the same request +:param accessControl: the access control to identify different documents for the same request provided by server interface :return: true if the document has been cached %End - bool deleteCachedDocument( const QgsProject *project, const QgsServerRequest &request, const QString &key ) const; + bool deleteCachedDocument( const QgsProject *project, const QgsServerRequest &request, QgsAccessControl *accessControl ) const; %Docstring Deletes the cached document :param project: the project used to generate the document to provide path :param request: the request used to generate the document to provider parameters or data -:param key: the key provided by the access control to identify different documents for the same request +:param accessControl: the access control to identify different documents for the same request provided by server interface :return: true if the document has been deleted %End @@ -82,36 +83,36 @@ Deletes all cached documents for a QGIS project :return: true if the document has been deleted %End - QByteArray getCachedImage( const QgsProject *project, const QgsServerRequest &request, const QString &key ) const; + QByteArray getCachedImage( const QgsProject *project, const QgsServerRequest &request, QgsAccessControl *accessControl ) const; %Docstring Returns cached image (or 0 if image not in cache) like tiles :param project: the project used to generate the image to provide path :param request: the request used to generate the image to provider parameters or data -:param key: the key provided by the access control to identify different images for the same request +:param accessControl: the access control to identify different documents for the same request provided by server interface :return: the cached image or 0 if no corresponding image found %End - bool setCachedImage( const QByteArray *img, const QgsProject *project, const QgsServerRequest &request, const QString &key ) const; + bool setCachedImage( const QByteArray *img, const QgsProject *project, const QgsServerRequest &request, QgsAccessControl *accessControl ) const; %Docstring Updates or inserts the image in cache like tiles :param img: the image to cache :param project: the project used to generate the image to provide path :param request: the request used to generate the image to provider parameters or data -:param key: the key provided by the access control to identify different images for the same request +:param accessControl: the access control to identify different documents for the same request provided by server interface :return: true if the image has been cached %End - bool deleteCachedImage( const QgsProject *project, const QgsServerRequest &request, const QString &key ) const; + bool deleteCachedImage( const QgsProject *project, const QgsServerRequest &request, QgsAccessControl *accessControl ) const; %Docstring Deletes the cached image :param project: the project used to generate the image to provide path :param request: the request used to generate the image to provider parameters or data -:param key: the key provided by the access control to identify different images for the same request +:param accessControl: the access control to identify different documents for the same request provided by server interface :return: true if the image has been deleted %End diff --git a/src/server/qgsservercachemanager.cpp b/src/server/qgsservercachemanager.cpp index 98d7bd46005a..6d00915e4331 100644 --- a/src/server/qgsservercachemanager.cpp +++ b/src/server/qgsservercachemanager.cpp @@ -18,22 +18,84 @@ #include "qgsservercachemanager.h" -QByteArray QgsServerCacheManager::getCachedDocument( const QgsProject *project, const QgsServerRequest &request, const QString &key ) const +QgsServerCacheManager::QgsServerCacheManager() { + mPluginsServerCaches.reset( new QgsServerCacheFilterMap() ); +} + +QgsServerCacheManager::QgsServerCacheManager( const QgsServerCacheManager © ) +{ + if ( copy.mPluginsServerCaches ) + { + mPluginsServerCaches.reset( new QgsServerCacheFilterMap( *copy.mPluginsServerCaches ) ); + } + else + { + mPluginsServerCaches.reset( nullptr ); + } +} + +QgsServerCacheManager &QgsServerCacheManager::operator=( const QgsServerCacheManager © ) +{ + if ( copy.mPluginsServerCaches ) + { + mPluginsServerCaches.reset( new QgsServerCacheFilterMap( *copy.mPluginsServerCaches ) ); + } + else + { + mPluginsServerCaches.reset( nullptr ); + } + return *this; +} + +QgsServerCacheManager::~QgsServerCacheManager() +{ + mPluginsServerCaches.reset(); +} + +bool QgsServerCacheManager::getCachedDocument( QDomDocument *doc, const QgsProject *project, const QgsServerRequest &request, QgsAccessControl *accessControl ) const +{ + bool cache = true; + QString key = getCacheKey( cache, accessControl ); + + if ( !cache ) + { + return false; + } + + QByteArray content; QgsServerCacheFilterMap::const_iterator scIterator; for ( scIterator = mPluginsServerCaches->constBegin(); scIterator != mPluginsServerCaches->constEnd(); ++scIterator ) { - QByteArray content = scIterator.value()->getCachedDocument( project, request, key ); + content = scIterator.value()->getCachedDocument( project, request, key ); if ( !content.isEmpty() ) { - return content; + break; } } - return QByteArray(); + if ( content.isEmpty() ) + { + return false; + } + + if ( !doc->setContent( content ) ) + { + return false; + } + + return true; } -bool QgsServerCacheManager::setCachedDocument( const QDomDocument *doc, const QgsProject *project, const QgsServerRequest &request, const QString &key ) const +bool QgsServerCacheManager::setCachedDocument( const QDomDocument *doc, const QgsProject *project, const QgsServerRequest &request, QgsAccessControl *accessControl ) const { + bool cache = true; + QString key = getCacheKey( cache, accessControl ); + + if ( !cache ) + { + return false; + } + QgsServerCacheFilterMap::const_iterator scIterator; for ( scIterator = mPluginsServerCaches->constBegin(); scIterator != mPluginsServerCaches->constEnd(); ++scIterator ) { @@ -45,8 +107,11 @@ bool QgsServerCacheManager::setCachedDocument( const QDomDocument *doc, const Qg return false; } -bool QgsServerCacheManager::deleteCachedDocument( const QgsProject *project, const QgsServerRequest &request, const QString &key ) const +bool QgsServerCacheManager::deleteCachedDocument( const QgsProject *project, const QgsServerRequest &request, QgsAccessControl *accessControl ) const { + bool cache = true; + QString key = getCacheKey( cache, accessControl ); + QgsServerCacheFilterMap::const_iterator scIterator; for ( scIterator = mPluginsServerCaches->constBegin(); scIterator != mPluginsServerCaches->constEnd(); ++scIterator ) { @@ -71,8 +136,11 @@ bool QgsServerCacheManager::deleteCachedDocuments( const QgsProject *project ) c return false; } -QByteArray QgsServerCacheManager::getCachedImage( const QgsProject *project, const QgsServerRequest &request, const QString &key ) const +QByteArray QgsServerCacheManager::getCachedImage( const QgsProject *project, const QgsServerRequest &request, QgsAccessControl *accessControl ) const { + bool cache = true; + QString key = getCacheKey( cache, accessControl ); + QgsServerCacheFilterMap::const_iterator scIterator; for ( scIterator = mPluginsServerCaches->constBegin(); scIterator != mPluginsServerCaches->constEnd(); ++scIterator ) { @@ -85,8 +153,11 @@ QByteArray QgsServerCacheManager::getCachedImage( const QgsProject *project, con return QByteArray(); } -bool QgsServerCacheManager::setCachedImage( const QByteArray *img, const QgsProject *project, const QgsServerRequest &request, const QString &key ) const +bool QgsServerCacheManager::setCachedImage( const QByteArray *img, const QgsProject *project, const QgsServerRequest &request, QgsAccessControl *accessControl ) const { + bool cache = true; + QString key = getCacheKey( cache, accessControl ); + QgsServerCacheFilterMap::const_iterator scIterator; for ( scIterator = mPluginsServerCaches->constBegin(); scIterator != mPluginsServerCaches->constEnd(); ++scIterator ) { @@ -98,8 +169,11 @@ bool QgsServerCacheManager::setCachedImage( const QByteArray *img, const QgsProj return false; } -bool QgsServerCacheManager::deleteCachedImage( const QgsProject *project, const QgsServerRequest &request, const QString &key ) const +bool QgsServerCacheManager::deleteCachedImage( const QgsProject *project, const QgsServerRequest &request, QgsAccessControl *accessControl ) const { + bool cache = true; + QString key = getCacheKey( cache, accessControl ); + QgsServerCacheFilterMap::const_iterator scIterator; for ( scIterator = mPluginsServerCaches->constBegin(); scIterator != mPluginsServerCaches->constEnd(); ++scIterator ) { @@ -128,3 +202,17 @@ void QgsServerCacheManager::registerServerCache( QgsServerCacheFilter *serverCac { mPluginsServerCaches->insert( priority, serverCache ); } + +QString QgsServerCacheManager::getCacheKey( bool &cache, QgsAccessControl *accessControl ) const +{ + QStringList cacheKeyList; + if ( accessControl ) + { + cache = accessControl->fillCacheKey( cacheKeyList ); + } + else + { + cache = true; + } + return cacheKeyList.join( '-' ); +} diff --git a/src/server/qgsservercachemanager.h b/src/server/qgsservercachemanager.h index 812e7babe8f7..935117d6af22 100644 --- a/src/server/qgsservercachemanager.h +++ b/src/server/qgsservercachemanager.h @@ -20,6 +20,7 @@ #define QGSSERVERCACHEMANAGER_H #include "qgsservercachefilter.h" +#include "qgsaccesscontrol.h" #include "qgsserverrequest.h" #include @@ -47,70 +48,45 @@ class SERVER_EXPORT QgsServerCacheManager public: //! Constructor - QgsServerCacheManager() - { - mPluginsServerCaches.reset( new QgsServerCacheFilterMap() ); - } + QgsServerCacheManager(); //! Copy constructor - QgsServerCacheManager( const QgsServerCacheManager © ) - { - if ( copy.mPluginsServerCaches ) - { - mPluginsServerCaches.reset( new QgsServerCacheFilterMap( *copy.mPluginsServerCaches ) ); - } - else - { - mPluginsServerCaches.reset( nullptr ); - } - } + QgsServerCacheManager( const QgsServerCacheManager © ); + //! Assignment operator - QgsServerCacheManager &operator=( const QgsServerCacheManager © ) - { - if ( copy.mPluginsServerCaches ) - { - mPluginsServerCaches.reset( new QgsServerCacheFilterMap( *copy.mPluginsServerCaches ) ); - } - else - { - mPluginsServerCaches.reset( nullptr ); - } - return *this; - } - - - ~QgsServerCacheManager() - { - mPluginsServerCaches.reset(); - } + QgsServerCacheManager &operator=( const QgsServerCacheManager © ); + + //! Destructor + ~QgsServerCacheManager(); /** * Returns cached document (or 0 if document not in cache) like capabilities + * \param doc the document to update by content found in cache * \param project the project used to generate the document to provide path * \param request the request used to generate the document to provider parameters or data - * \param key the key provided by the access control to identify different documents for the same request - * \returns the cached document or 0 if no corresponding document found + * \param accessControl the access control to identify different documents for the same request provided by server interface + * \returns true if the document has been found in cache and the document's content set */ - QByteArray getCachedDocument( const QgsProject *project, const QgsServerRequest &request, const QString &key ) const; + bool getCachedDocument( QDomDocument *doc, const QgsProject *project, const QgsServerRequest &request, QgsAccessControl *accessControl ) const; /** * Updates or inserts the document in cache like capabilities * \param doc the document to cache * \param project the project used to generate the document to provide path * \param request the request used to generate the document to provider parameters or data - * \param key the key provided by the access control to identify different documents for the same request + * \param accessControl the access control to identify different documents for the same request provided by server interface * \returns true if the document has been cached */ - bool setCachedDocument( const QDomDocument *doc, const QgsProject *project, const QgsServerRequest &request, const QString &key ) const; + bool setCachedDocument( const QDomDocument *doc, const QgsProject *project, const QgsServerRequest &request, QgsAccessControl *accessControl ) const; /** * Deletes the cached document * \param project the project used to generate the document to provide path * \param request the request used to generate the document to provider parameters or data - * \param key the key provided by the access control to identify different documents for the same request + * \param accessControl the access control to identify different documents for the same request provided by server interface * \returns true if the document has been deleted */ - bool deleteCachedDocument( const QgsProject *project, const QgsServerRequest &request, const QString &key ) const; + bool deleteCachedDocument( const QgsProject *project, const QgsServerRequest &request, QgsAccessControl *accessControl ) const; /** * Deletes all cached documents for a QGIS project @@ -123,29 +99,29 @@ class SERVER_EXPORT QgsServerCacheManager * Returns cached image (or 0 if image not in cache) like tiles * \param project the project used to generate the image to provide path * \param request the request used to generate the image to provider parameters or data - * \param key the key provided by the access control to identify different images for the same request + * \param accessControl the access control to identify different documents for the same request provided by server interface * \returns the cached image or 0 if no corresponding image found */ - QByteArray getCachedImage( const QgsProject *project, const QgsServerRequest &request, const QString &key ) const; + QByteArray getCachedImage( const QgsProject *project, const QgsServerRequest &request, QgsAccessControl *accessControl ) const; /** * Updates or inserts the image in cache like tiles * \param img the image to cache * \param project the project used to generate the image to provide path * \param request the request used to generate the image to provider parameters or data - * \param key the key provided by the access control to identify different images for the same request + * \param accessControl the access control to identify different documents for the same request provided by server interface * \returns true if the image has been cached */ - bool setCachedImage( const QByteArray *img, const QgsProject *project, const QgsServerRequest &request, const QString &key ) const; + bool setCachedImage( const QByteArray *img, const QgsProject *project, const QgsServerRequest &request, QgsAccessControl *accessControl ) const; /** * Deletes the cached image * \param project the project used to generate the image to provide path * \param request the request used to generate the image to provider parameters or data - * \param key the key provided by the access control to identify different images for the same request + * \param accessControl the access control to identify different documents for the same request provided by server interface * \returns true if the image has been deleted */ - bool deleteCachedImage( const QgsProject *project, const QgsServerRequest &request, const QString &key ) const; + bool deleteCachedImage( const QgsProject *project, const QgsServerRequest &request, QgsAccessControl *accessControl ) const; /** * Deletes all cached images for a QGIS project @@ -162,6 +138,7 @@ class SERVER_EXPORT QgsServerCacheManager void registerServerCache( QgsServerCacheFilter *serverCache, int priority = 0 ); private: + QString getCacheKey( bool &cache, QgsAccessControl *accessControl ) const; //! The ServerCache plugins registry std::unique_ptr mPluginsServerCaches = nullptr; }; diff --git a/src/server/services/wcs/qgswcsgetcapabilities.cpp b/src/server/services/wcs/qgswcsgetcapabilities.cpp index 4ebed4cf693f..01da88ce2c07 100644 --- a/src/server/services/wcs/qgswcsgetcapabilities.cpp +++ b/src/server/services/wcs/qgswcsgetcapabilities.cpp @@ -37,49 +37,25 @@ namespace QgsWcs void writeGetCapabilities( QgsServerInterface *serverIface, const QgsProject *project, const QString &version, const QgsServerRequest &request, QgsServerResponse &response ) { - QStringList cacheKeyList; - bool cache = true; - QgsAccessControl *accessControl = serverIface->accessControls(); - if ( accessControl ) - cache = accessControl->fillCacheKey( cacheKeyList ); QDomDocument doc; - QString cacheKey = cacheKeyList.join( '-' ); const QDomDocument *capabilitiesDocument = nullptr; QgsServerCacheManager *cacheManager = serverIface->cacheManager(); - if ( cacheManager && cache ) + if ( cacheManager && cacheManager->getCachedDocument( &doc, project, request, accessControl ) ) { - QByteArray content = cacheManager->getCachedDocument( project, request, cacheKey ); - if ( !content.isEmpty() && doc.setContent( content ) ) - { - doc = doc.cloneNode().toDocument(); - capabilitiesDocument = &doc; - } + capabilitiesDocument = &doc; } - - if ( !capabilitiesDocument ) //capabilities xml not in cache. Create a new one + else //capabilities xml not in cache. Create a new one { doc = createGetCapabilitiesDocument( serverIface, project, version, request ); - if ( cache && cacheManager ) - { - if ( cacheManager->setCachedDocument( &doc, project, request, cacheKey ) ) - { - QByteArray content = cacheManager->getCachedDocument( project, request, cacheKey ); - if ( !content.isEmpty() && doc.setContent( content ) ) - { - doc = doc.cloneNode().toDocument(); - capabilitiesDocument = &doc; - } - } - } - if ( !capabilitiesDocument ) + if ( cacheManager ) { - doc = doc.cloneNode().toDocument(); - capabilitiesDocument = &doc; + cacheManager->setCachedDocument( &doc, project, request, accessControl ); } + capabilitiesDocument = &doc; } response.setHeader( QStringLiteral( "Content-Type" ), QStringLiteral( "text/xml; charset=utf-8" ) ); diff --git a/src/server/services/wfs/qgswfsgetcapabilities.cpp b/src/server/services/wfs/qgswfsgetcapabilities.cpp index 0a3f84dece65..31199878746d 100644 --- a/src/server/services/wfs/qgswfsgetcapabilities.cpp +++ b/src/server/services/wfs/qgswfsgetcapabilities.cpp @@ -41,49 +41,25 @@ namespace QgsWfs void writeGetCapabilities( QgsServerInterface *serverIface, const QgsProject *project, const QString &version, const QgsServerRequest &request, QgsServerResponse &response ) { - QStringList cacheKeyList; - bool cache = true; - QgsAccessControl *accessControl = serverIface->accessControls(); - if ( accessControl ) - cache = accessControl->fillCacheKey( cacheKeyList ); QDomDocument doc; - QString cacheKey = cacheKeyList.join( '-' ); const QDomDocument *capabilitiesDocument = nullptr; QgsServerCacheManager *cacheManager = serverIface->cacheManager(); - if ( cacheManager && cache ) + if ( cacheManager && cacheManager->getCachedDocument( &doc, project, request, accessControl ) ) { - QByteArray content = cacheManager->getCachedDocument( project, request, cacheKey ); - if ( !content.isEmpty() && doc.setContent( content ) ) - { - doc = doc.cloneNode().toDocument(); - capabilitiesDocument = &doc; - } + capabilitiesDocument = &doc; } - - if ( !capabilitiesDocument ) //capabilities xml not in cache. Create a new one + else //capabilities xml not in cache. Create a new one { doc = createGetCapabilitiesDocument( serverIface, project, version, request ); - if ( cache && cacheManager ) - { - if ( cacheManager->setCachedDocument( &doc, project, request, cacheKey ) ) - { - QByteArray content = cacheManager->getCachedDocument( project, request, cacheKey ); - if ( !content.isEmpty() && doc.setContent( content ) ) - { - doc = doc.cloneNode().toDocument(); - capabilitiesDocument = &doc; - } - } - } - if ( !capabilitiesDocument ) + if ( cacheManager ) { - doc = doc.cloneNode().toDocument(); - capabilitiesDocument = &doc; + cacheManager->setCachedDocument( &doc, project, request, accessControl ); } + capabilitiesDocument = &doc; } response.setHeader( QStringLiteral( "Content-Type" ), QStringLiteral( "text/xml; charset=utf-8" ) ); diff --git a/src/server/services/wfs/qgswfsgetcapabilities_1_0_0.cpp b/src/server/services/wfs/qgswfsgetcapabilities_1_0_0.cpp index 474d53e2a29e..dc0e9c4a28a4 100644 --- a/src/server/services/wfs/qgswfsgetcapabilities_1_0_0.cpp +++ b/src/server/services/wfs/qgswfsgetcapabilities_1_0_0.cpp @@ -43,49 +43,25 @@ namespace QgsWfs void writeGetCapabilities( QgsServerInterface *serverIface, const QgsProject *project, const QString &version, const QgsServerRequest &request, QgsServerResponse &response ) { - QStringList cacheKeyList; - bool cache = true; - QgsAccessControl *accessControl = serverIface->accessControls(); - if ( accessControl ) - cache = accessControl->fillCacheKey( cacheKeyList ); QDomDocument doc; - QString cacheKey = cacheKeyList.join( '-' ); const QDomDocument *capabilitiesDocument = nullptr; QgsServerCacheManager *cacheManager = serverIface->cacheManager(); - if ( cacheManager && cache ) + if ( cacheManager && cacheManager->getCachedDocument( &doc, project, request, accessControl ) ) { - QByteArray content = cacheManager->getCachedDocument( project, request, cacheKey ); - if ( !content.isEmpty() && doc.setContent( content ) ) - { - doc = doc.cloneNode().toDocument(); - capabilitiesDocument = &doc; - } + capabilitiesDocument = &doc; } - - if ( !capabilitiesDocument ) //capabilities xml not in cache. Create a new one + else //capabilities xml not in cache. Create a new one { doc = createGetCapabilitiesDocument( serverIface, project, version, request ); - if ( cache && cacheManager ) - { - if ( cacheManager->setCachedDocument( &doc, project, request, cacheKey ) ) - { - QByteArray content = cacheManager->getCachedDocument( project, request, cacheKey ); - if ( !content.isEmpty() && doc.setContent( content ) ) - { - doc = doc.cloneNode().toDocument(); - capabilitiesDocument = &doc; - } - } - } - if ( !capabilitiesDocument ) + if ( cacheManager ) { - doc = doc.cloneNode().toDocument(); - capabilitiesDocument = &doc; + cacheManager->setCachedDocument( &doc, project, request, accessControl ); } + capabilitiesDocument = &doc; } response.setHeader( QStringLiteral( "Content-Type" ), QStringLiteral( "text/xml; charset=utf-8" ) ); diff --git a/src/server/services/wms/qgswmsgetcapabilities.cpp b/src/server/services/wms/qgswmsgetcapabilities.cpp index 0b02e21299a5..1ce23832e8f6 100644 --- a/src/server/services/wms/qgswmsgetcapabilities.cpp +++ b/src/server/services/wms/qgswmsgetcapabilities.cpp @@ -93,88 +93,61 @@ namespace QgsWms const QString &version, const QgsServerRequest &request, QgsServerResponse &response, bool projectSettings ) { + QgsAccessControl *accessControl = serverIface->accessControls(); + + QDomDocument doc; + const QDomDocument *capabilitiesDocument = nullptr; + + // Data for WMS capabilities server memory cache QString configFilePath = serverIface->configFilePath(); QgsCapabilitiesCache *capabilitiesCache = serverIface->capabilitiesCache(); - QStringList cacheKeyList; cacheKeyList << ( projectSettings ? QStringLiteral( "projectSettings" ) : version ); cacheKeyList << request.url().host(); bool cache = true; - - QgsAccessControl *accessControl = serverIface->accessControls(); if ( accessControl ) cache = accessControl->fillCacheKey( cacheKeyList ); - - - QDomDocument doc; QString cacheKey = cacheKeyList.join( '-' ); - const QDomDocument *capabilitiesDocument = nullptr; QgsServerCacheManager *cacheManager = serverIface->cacheManager(); - if ( cacheManager && cache ) + if ( cacheManager && cacheManager->getCachedDocument( &doc, project, request, accessControl ) ) { - QByteArray content; - if ( cacheKeyList.count() == 2 ) - content = cacheManager->getCachedDocument( project, request, QString() ); - else if ( cacheKeyList.count() > 2 ) - content = cacheManager->getCachedDocument( project, request, cacheKeyList.at( 3 ) ); - - if ( !content.isEmpty() && doc.setContent( content ) ) - { - QgsMessageLog::logMessage( QStringLiteral( "Found capabilities document in cache manager" ) ); - doc = doc.cloneNode().toDocument(); - capabilitiesDocument = &doc; - } - else - { - QgsMessageLog::logMessage( QStringLiteral( "Capabilities document not found in cache manager" ) ); - } + capabilitiesDocument = &doc; } - if ( !capabilitiesDocument ) //capabilities xml not in cache plugins + if ( !capabilitiesDocument && cache ) //capabilities xml not in cache plugins + { capabilitiesDocument = capabilitiesCache->searchCapabilitiesDocument( configFilePath, cacheKey ); + } + if ( !capabilitiesDocument ) //capabilities xml not in cache. Create a new one { - QgsMessageLog::logMessage( QStringLiteral( "Capabilities document not found in cache" ) ); + QgsMessageLog::logMessage( QStringLiteral( "WMS capabilities document not found in cache" ) ); doc = getCapabilities( serverIface, project, version, request, projectSettings ); - if ( cache ) + if ( cacheManager && + cacheManager->setCachedDocument( &doc, project, request, accessControl ) ) { - if ( cacheManager ) - { - QByteArray content; - if ( cacheKeyList.count() == 2 && - cacheManager->setCachedDocument( &doc, project, request, QString() ) ) - { - content = cacheManager->getCachedDocument( project, request, QString() ); - } - else if ( cacheKeyList.count() > 2 && - cacheManager->setCachedDocument( &doc, project, request, cacheKeyList.at( 3 ) ) ) - { - content = cacheManager->getCachedDocument( project, request, cacheKeyList.at( 3 ) ); - } - if ( !content.isEmpty() && doc.setContent( content ) ) - { - doc = doc.cloneNode().toDocument(); - capabilitiesDocument = &doc; - } - } - else - { - capabilitiesCache->insertCapabilitiesDocument( configFilePath, cacheKey, &doc ); - capabilitiesDocument = capabilitiesCache->searchCapabilitiesDocument( configFilePath, cacheKey ); - } + capabilitiesDocument = &doc; + } + else if ( cache ) + { + capabilitiesCache->insertCapabilitiesDocument( configFilePath, cacheKey, &doc ); + capabilitiesDocument = capabilitiesCache->searchCapabilitiesDocument( configFilePath, cacheKey ); } if ( !capabilitiesDocument ) { - doc = doc.cloneNode().toDocument(); capabilitiesDocument = &doc; } + else + { + QgsMessageLog::logMessage( QStringLiteral( "Set WMS capabilities document in cache" ) ); + } } else { - QgsMessageLog::logMessage( QStringLiteral( "Found capabilities document in cache" ) ); + QgsMessageLog::logMessage( QStringLiteral( "Found WMS capabilities document in cache" ) ); } response.setHeader( QStringLiteral( "Content-Type" ), QStringLiteral( "text/xml; charset=utf-8" ) ); diff --git a/src/server/services/wmts/qgswmtsgetcapabilities.cpp b/src/server/services/wmts/qgswmtsgetcapabilities.cpp index d010c5f7609e..cd6147581329 100644 --- a/src/server/services/wmts/qgswmtsgetcapabilities.cpp +++ b/src/server/services/wmts/qgswmtsgetcapabilities.cpp @@ -48,49 +48,25 @@ namespace QgsWmts void writeGetCapabilities( QgsServerInterface *serverIface, const QgsProject *project, const QString &version, const QgsServerRequest &request, QgsServerResponse &response ) { - QStringList cacheKeyList; - bool cache = true; - QgsAccessControl *accessControl = serverIface->accessControls(); - if ( accessControl ) - cache = accessControl->fillCacheKey( cacheKeyList ); QDomDocument doc; - QString cacheKey = cacheKeyList.join( '-' ); const QDomDocument *capabilitiesDocument = nullptr; QgsServerCacheManager *cacheManager = serverIface->cacheManager(); - if ( cacheManager && cache ) + if ( cacheManager && cacheManager->getCachedDocument( &doc, project, request, accessControl ) ) { - QByteArray content = cacheManager->getCachedDocument( project, request, cacheKey ); - if ( !content.isEmpty() && doc.setContent( content ) ) - { - doc = doc.cloneNode().toDocument(); - capabilitiesDocument = &doc; - } + capabilitiesDocument = &doc; } - - if ( !capabilitiesDocument ) //capabilities xml not in cache. Create a new one + else //capabilities xml not in cache. Create a new one { doc = createGetCapabilitiesDocument( serverIface, project, version, request ); - if ( cache && cacheManager ) - { - if ( cacheManager->setCachedDocument( &doc, project, request, cacheKey ) ) - { - QByteArray content = cacheManager->getCachedDocument( project, request, cacheKey ); - if ( !content.isEmpty() && doc.setContent( content ) ) - { - doc = doc.cloneNode().toDocument(); - capabilitiesDocument = &doc; - } - } - } - if ( !capabilitiesDocument ) + if ( cacheManager ) { - doc = doc.cloneNode().toDocument(); - capabilitiesDocument = &doc; + cacheManager->setCachedDocument( &doc, project, request, accessControl ); } + capabilitiesDocument = &doc; } response.setHeader( QStringLiteral( "Content-Type" ), QStringLiteral( "text/xml; charset=utf-8" ) ); diff --git a/src/server/services/wmts/qgswmtsgettile.cpp b/src/server/services/wmts/qgswmtsgettile.cpp index 5da4ad27184d..e2d64a6dcb32 100644 --- a/src/server/services/wmts/qgswmtsgettile.cpp +++ b/src/server/services/wmts/qgswmtsgettile.cpp @@ -28,23 +28,15 @@ namespace QgsWmts QgsServerResponse &response ) { Q_UNUSED( version ); - //QgsServerRequest::Parameters params = request.parameters(); const QgsWmtsParameters params( QUrlQuery( request.url() ) ); // WMS query QUrlQuery query = translateWmtsParamToWmsQueryItem( QStringLiteral( "GetMap" ), params, project, serverIface ); // Get cached image - QStringList cacheKeyList; - bool cache = true; - QgsAccessControl *accessControl = serverIface->accessControls(); - if ( accessControl ) - cache = accessControl->fillCacheKey( cacheKeyList ); - - QString cacheKey = cacheKeyList.join( '-' ); QgsServerCacheManager *cacheManager = serverIface->cacheManager(); - if ( cacheManager && cache ) + if ( cacheManager ) { QgsWmtsParameters::Format f = params.format(); QString contentType; @@ -63,7 +55,7 @@ namespace QgsWmts image = qgis::make_unique( 256, 256, QImage::Format_ARGB32_Premultiplied ); } - QByteArray content = cacheManager->getCachedImage( project, request, cacheKey ); + QByteArray content = cacheManager->getCachedImage( project, request, accessControl ); if ( !content.isEmpty() && image->loadFromData( content ) ) { response.setHeader( QStringLiteral( "Content-Type" ), contentType ); @@ -77,11 +69,11 @@ namespace QgsWmts QgsServerRequest wmsRequest( "?" + query.query( QUrl::FullyDecoded ) ); QgsService *service = serverIface->serviceRegistry()->getService( wmsParams.service(), wmsParams.version() ); service->executeRequest( wmsRequest, response, project ); - if ( cache && cacheManager ) + if ( cacheManager ) { QByteArray content = response.data(); if ( !content.isEmpty() ) - cacheManager->setCachedImage( &content, project, request, cacheKey ); + cacheManager->setCachedImage( &content, project, request, accessControl ); } } diff --git a/tests/src/python/test_qgsserver_cachemanager.py b/tests/src/python/test_qgsserver_cachemanager.py index 9c2288ff9c1f..2b89a8c768a3 100644 --- a/tests/src/python/test_qgsserver_cachemanager.py +++ b/tests/src/python/test_qgsserver_cachemanager.py @@ -69,6 +69,9 @@ def getCachedDocument(self, project, request, key): return doc.toByteArray() def setCachedDocument(self, doc, project, request, key): + if not doc: + print("Could not cache None document") + return False m = hashlib.md5() paramMap = request.parameters() urlParam = "&".join(["%s=%s" % (k, paramMap[k]) for k in paramMap.keys()]) @@ -261,18 +264,14 @@ def test_getcapabilities(self): query_string = '?MAP=%s&SERVICE=WMS&VERSION=1.3.0&REQUEST=%s' % (urllib.parse.quote(project), 'GetCapabilities') request = QgsBufferServerRequest(query_string, QgsServerRequest.GetMethod, {}, None) - cContent = cacheManager.getCachedDocument(prj, request, '') + accessControls = self._server_iface.accessControls() - self.assertTrue(cContent.isEmpty(), 'getCachedDocument is not None') - - self.assertTrue(cacheManager.setCachedDocument(doc, prj, request, ''), 'setCachedDocument false') - - cContent = cacheManager.getCachedDocument(prj, request, '') + cDoc = QDomDocument("wms_getcapabilities_130.xml") + self.assertFalse(cacheManager.getCachedDocument(cDoc, prj, request, accessControls), 'getCachedDocument is not None') - self.assertFalse(cContent.isEmpty(), 'getCachedDocument is empty') + self.assertTrue(cacheManager.setCachedDocument(doc, prj, request, accessControls), 'setCachedDocument false') - cDoc = QDomDocument("wms_getcapabilities_130.xml") - self.assertTrue(cDoc.setContent(cContent), 'cachedDocument not XML doc') + self.assertTrue(cacheManager.getCachedDocument(cDoc, prj, request, accessControls), 'getCachedDocument is None') self.assertEqual(doc.documentElement().tagName(), cDoc.documentElement().tagName(), 'cachedDocument not equal to provide document') self.assertTrue(cacheManager.deleteCachedDocuments(None), 'deleteCachedDocuments does not return True') From 1bae625aca7f102494869ff68fe751b53964a561 Mon Sep 17 00:00:00 2001 From: rldhont Date: Tue, 21 Aug 2018 13:55:11 +0200 Subject: [PATCH 24/33] [Server] Fix: QUrl::FullyDecoded is not permitted when reconstructing the full URL --- src/server/services/wcs/qgswcsutils.cpp | 2 +- src/server/services/wfs/qgswfsutils.cpp | 2 +- src/server/services/wms/qgswmsdescribelayer.cpp | 2 +- src/server/services/wms/qgswmsgetcapabilities.cpp | 6 +++--- src/server/services/wms/qgswmsgetcontext.cpp | 2 +- src/server/services/wmts/qgswmtsutils.cpp | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/server/services/wcs/qgswcsutils.cpp b/src/server/services/wcs/qgswcsutils.cpp index b569b9a759a7..ae9b32fdff0d 100644 --- a/src/server/services/wcs/qgswcsutils.cpp +++ b/src/server/services/wcs/qgswcsutils.cpp @@ -251,7 +251,7 @@ namespace QgsWcs q.removeAllQueryItems( QStringLiteral( "_DC" ) ); url.setQuery( q ); - href = url.toString( QUrl::FullyDecoded ); + href = url.toString(); } diff --git a/src/server/services/wfs/qgswfsutils.cpp b/src/server/services/wfs/qgswfsutils.cpp index 84051b1c5c16..238eaddbe2b9 100644 --- a/src/server/services/wfs/qgswfsutils.cpp +++ b/src/server/services/wfs/qgswfsutils.cpp @@ -53,7 +53,7 @@ namespace QgsWfs params.remove( QgsServerParameter::SERVICE ); url.setQuery( params.urlQuery() ); - href = url.toString( QUrl::FullyDecoded ); + href = url.toString(); } return href; diff --git a/src/server/services/wms/qgswmsdescribelayer.cpp b/src/server/services/wms/qgswmsdescribelayer.cpp index 760115450737..a533aa17edb3 100644 --- a/src/server/services/wms/qgswmsdescribelayer.cpp +++ b/src/server/services/wms/qgswmsdescribelayer.cpp @@ -84,7 +84,7 @@ namespace QgsWms // get the wms service url defined in project or keep the one from the // request url - QString wmsHrefString = serviceUrl( request, project ).toString( QUrl::FullyDecoded ); + QString wmsHrefString = serviceUrl( request, project ).toString(); // get the wfs service url defined in project or take the same as the // wms service url diff --git a/src/server/services/wms/qgswmsgetcapabilities.cpp b/src/server/services/wms/qgswmsgetcapabilities.cpp index 1ce23832e8f6..15de95417491 100644 --- a/src/server/services/wms/qgswmsgetcapabilities.cpp +++ b/src/server/services/wms/qgswmsgetcapabilities.cpp @@ -167,7 +167,7 @@ namespace QgsWms QUrl href = serviceUrl( request, project ); //href needs to be a prefix - QString hrefString = href.toString( QUrl::FullyDecoded ); + QString hrefString = href.toString(); hrefString.append( href.hasQuery() ? "&" : "?" ); // XML declaration @@ -407,7 +407,7 @@ namespace QgsWms QUrl href = serviceUrl( request, project ); //href needs to be a prefix - QString hrefString = href.toString( QUrl::FullyDecoded ); + QString hrefString = href.toString(); hrefString.append( href.hasQuery() ? "&" : "?" ); QDomElement capabilityElem = doc.createElement( QStringLiteral( "Capability" )/*wms:Capability*/ ); @@ -1140,7 +1140,7 @@ namespace QgsWms QUrl href = serviceUrl( request, project ); //href needs to be a prefix - QString hrefString = href.toString( QUrl::FullyDecoded ); + QString hrefString = href.toString(); hrefString.append( href.hasQuery() ? "&" : "?" ); for ( const QString &styleName : currentLayer->styleManager()->styles() ) { diff --git a/src/server/services/wms/qgswmsgetcontext.cpp b/src/server/services/wms/qgswmsgetcontext.cpp index 36cb0ea3bcbd..2c215e374eb9 100644 --- a/src/server/services/wms/qgswmsgetcontext.cpp +++ b/src/server/services/wms/qgswmsgetcontext.cpp @@ -311,7 +311,7 @@ namespace QgsWms QUrl href = serviceUrl( request, project ); //href needs to be a prefix - QString hrefString = href.toString( QUrl::FullyDecoded ); + QString hrefString = href.toString(); hrefString.append( href.hasQuery() ? "&" : "?" ); // COntext Server Element with WMS service URL diff --git a/src/server/services/wmts/qgswmtsutils.cpp b/src/server/services/wmts/qgswmtsutils.cpp index 5d5f209d846e..e865399bd2ae 100644 --- a/src/server/services/wmts/qgswmtsutils.cpp +++ b/src/server/services/wmts/qgswmtsutils.cpp @@ -74,7 +74,7 @@ namespace QgsWmts params.remove( QgsServerParameter::SERVICE ); url.setQuery( params.urlQuery() ); - href = url.toString( QUrl::FullyDecoded ); + href = url.toString(); } return href; From d9095e0b1eea7f6780ea689c0ff24d4aef4f6168 Mon Sep 17 00:00:00 2001 From: rldhont Date: Thu, 23 Aug 2018 11:30:36 +0200 Subject: [PATCH 25/33] [Server] Various code cleaning for server cache manager and WMTS service --- .../services/wmts/qgswmtsgetcapabilities.cpp | 22 +++++----- .../services/wmts/qgswmtsgetfeatureinfo.cpp | 8 ++-- .../services/wmts/qgswmtsparameters.cpp | 15 +++++++ src/server/services/wmts/qgswmtsparameters.h | 43 +++++++++++++++++++ src/server/services/wmts/qgswmtsutils.cpp | 26 +++++------ 5 files changed, 86 insertions(+), 28 deletions(-) diff --git a/src/server/services/wmts/qgswmtsgetcapabilities.cpp b/src/server/services/wmts/qgswmtsgetcapabilities.cpp index cd6147581329..3ad7d659edab 100644 --- a/src/server/services/wmts/qgswmtsgetcapabilities.cpp +++ b/src/server/services/wmts/qgswmtsgetcapabilities.cpp @@ -147,10 +147,10 @@ namespace QgsWmts if ( !keywords.isEmpty() ) { QDomElement keywordsElem = doc.createElement( QStringLiteral( "ows:Keywords" ) ); - for ( int i = 0; i < keywords.size(); ++i ) + for ( const QString k : keywords ) { QDomElement keywordElem = doc.createElement( QStringLiteral( "ows:Keyword" ) ); - QDomText keywordText = doc.createTextNode( keywords.at( i ) ); + QDomText keywordText = doc.createTextNode( k ); keywordElem.appendChild( keywordText ); keywordsElem.appendChild( keywordElem ); } @@ -389,7 +389,7 @@ namespace QgsWmts QStringList wmtsPngGroupNameList = project->readListEntry( QStringLiteral( "WMTSPngLayers" ), QStringLiteral( "Group" ) ); QStringList wmtsJpegGroupNameList = project->readListEntry( QStringLiteral( "WMTSJpegLayers" ), QStringLiteral( "Group" ) ); - for ( QString gName : wmtsGroupNameList ) + for ( const QString gName : wmtsGroupNameList ) { QgsLayerTreeGroup *treeGroup = treeRoot->findGroup( gName ); if ( !treeGroup ) @@ -450,7 +450,7 @@ namespace QgsWmts QStringList wmtsPngLayerIdList = project->readListEntry( QStringLiteral( "WMTSPngLayers" ), QStringLiteral( "Layer" ) ); QStringList wmtsJpegLayerIdList = project->readListEntry( QStringLiteral( "WMTSJpegLayers" ), QStringLiteral( "Layer" ) ); - for ( QString lId : wmtsLayerIdList ) + for ( const QString lId : wmtsLayerIdList ) { QgsMapLayer *l = project->mapLayer( lId ); if ( !l ) @@ -511,7 +511,7 @@ namespace QgsWmts elem.appendChild( formatElem ); }; - for ( layerDef wmtsLayer : wmtsLayers ) + for ( const layerDef wmtsLayer : wmtsLayers ) { if ( wmtsLayer.id.isEmpty() ) continue; @@ -554,7 +554,7 @@ namespace QgsWmts layerElem.appendChild( wgs84BBoxElement ); // Other bounding boxes - for ( tileMatrixSetDef tms : tmsList ) + for ( const tileMatrixSetDef tms : tmsList ) { if ( tms.ref == QLatin1String( "EPSG:4326" ) ) continue; @@ -597,7 +597,7 @@ namespace QgsWmts layerStyleElem.appendChild( layerStyleTitleElem ); layerElem.appendChild( layerStyleElem ); - for ( QString format : wmtsLayer.formats ) + for ( const QString format : wmtsLayer.formats ) { QDomElement layerFormatElem = doc.createElement( QStringLiteral( "Format" ) ); QDomText layerFormatText = doc.createTextNode( format ); @@ -614,7 +614,7 @@ namespace QgsWmts appendInfoFormat( layerElem, QStringLiteral( "application/vnd.ogc.gml/3.1.1" ) ); } - for ( tileMatrixSetDef tms : tmsList ) + for ( const tileMatrixSetDef tms : tmsList ) { if ( tms.ref != QLatin1String( "EPSG:4326" ) ) { @@ -642,7 +642,7 @@ namespace QgsWmts //wmts:TileMatrixSetLimits QDomElement tmsLimitsElement = doc.createElement( QStringLiteral( "TileMatrixSetLimits" )/*wmts:TileMatrixSetLimits*/ ); int tmIdx = 0; - for ( tileMatrixDef tm : tms.tileMatrixList ) + for ( const tileMatrixDef tm : tms.tileMatrixList ) { QDomElement tmLimitsElement = doc.createElement( QStringLiteral( "TileMatrixLimits" )/*wmts:TileMatrixLimits*/ ); @@ -687,7 +687,7 @@ namespace QgsWmts void appendTileMatrixSetElements( QDomDocument &doc, QDomElement &contentsElement, QList< tileMatrixSetDef > tmsList ) { - for ( tileMatrixSetDef tms : tmsList ) + for ( const tileMatrixSetDef tms : tmsList ) { //wmts:TileMatrixSet QDomElement tmsElement = doc.createElement( QStringLiteral( "TileMatrixSet" )/*wmts:TileMatrixSet*/ ); @@ -704,7 +704,7 @@ namespace QgsWmts //wmts:TileMatrix int tmIdx = 0; - for ( tileMatrixDef tm : tms.tileMatrixList ) + for ( const tileMatrixDef tm : tms.tileMatrixList ) { QDomElement tmElement = doc.createElement( QStringLiteral( "TileMatrix" )/*wmts:TileMatrix*/ ); diff --git a/src/server/services/wmts/qgswmtsgetfeatureinfo.cpp b/src/server/services/wmts/qgswmtsgetfeatureinfo.cpp index 1b86a2d600fb..029fce2cfe4b 100644 --- a/src/server/services/wmts/qgswmtsgetfeatureinfo.cpp +++ b/src/server/services/wmts/qgswmtsgetfeatureinfo.cpp @@ -34,10 +34,10 @@ namespace QgsWmts QUrlQuery query = translateWmtsParamToWmsQueryItem( QStringLiteral( "GetFeatureInfo" ), params, project, serverIface ); // GetFeatureInfo query items - query.addQueryItem( QStringLiteral( "query_layers" ), params.layer() ); - query.addQueryItem( QgsWmtsParameter::name( QgsWmtsParameter::I ), params.i() ); - query.addQueryItem( QgsWmtsParameter::name( QgsWmtsParameter::J ), params.j() ); - query.addQueryItem( QStringLiteral( "info_format" ), params.infoFormatAsString() ); + query.addQueryItem( QgsWmsParameter::name( QgsWmsParameter::QUERY_LAYERS ), params.layer() ); + query.addQueryItem( QgsWmsParameter::name( QgsWmsParameter::I ), params.i() ); + query.addQueryItem( QgsWmsParameter::name( QgsWmsParameter::J ), params.j() ); + query.addQueryItem( QgsWmsParameter::name( QgsWmsParameter::INFO_FORMAT ), params.infoFormatAsString() ); QgsServerParameters wmsParams( query ); QgsServerRequest wmsRequest( "?" + query.query( QUrl::FullyDecoded ) ); diff --git a/src/server/services/wmts/qgswmtsparameters.cpp b/src/server/services/wmts/qgswmtsparameters.cpp index a61761b87715..c8f6588a9a92 100644 --- a/src/server/services/wmts/qgswmtsparameters.cpp +++ b/src/server/services/wmts/qgswmtsparameters.cpp @@ -21,6 +21,21 @@ namespace QgsWmts { + // + // QgsWmsParameter + // + QString QgsWmsParameter::name( const QgsWmsParameter::Name name ) + { + const QMetaEnum metaEnum( QMetaEnum::fromType() ); + return metaEnum.valueToKey( name ); + } + + QgsWmsParameter::Name QgsWmsParameter::name( const QString &name ) + { + const QMetaEnum metaEnum( QMetaEnum::fromType() ); + return ( QgsWmsParameter::Name ) metaEnum.keyToValue( name.toUpper().toStdString().c_str() ); + } + // // QgsWmtsParameter // diff --git a/src/server/services/wmts/qgswmtsparameters.h b/src/server/services/wmts/qgswmtsparameters.h index 187fb86a9885..e1782d0fd1ca 100644 --- a/src/server/services/wmts/qgswmtsparameters.h +++ b/src/server/services/wmts/qgswmtsparameters.h @@ -30,6 +30,49 @@ namespace QgsWmts { + /** + * \ingroup server + * \class QgsWmts::QgsWmsParameter + * \brief WMS parameter used by WMTS service. + * \since QGIS 3.4 + */ + class QgsWmsParameter : public QgsServerParameterDefinition + { + Q_GADGET + + public: + //! Available parameters for translating WMTS requests to WMS requests + enum Name + { + UNKNOWN, + LAYERS, + STYLES, + CRS, + BBOX, + WIDTH, + HEIGHT, + FORMAT, + TRANSPARENT, + DPI, + QUERY_LAYERS, + I, + J, + INFO_FORMAT + }; + Q_ENUM( Name ) + + /** + * Converts a parameter's name into its string representation. + */ + static QString name( const QgsWmsParameter::Name ); + + /** + * Converts a string into a parameter's name (UNKNOWN in case of an + * invalid string). + */ + static QgsWmsParameter::Name name( const QString &name ); + }; + /** * \ingroup server * \class QgsWmts::QgsWmtsParameter diff --git a/src/server/services/wmts/qgswmtsutils.cpp b/src/server/services/wmts/qgswmtsutils.cpp index e865399bd2ae..89d5f76780d7 100644 --- a/src/server/services/wmts/qgswmtsutils.cpp +++ b/src/server/services/wmts/qgswmtsutils.cpp @@ -378,23 +378,23 @@ namespace QgsWmts QUrlQuery query; if ( !params.value( QStringLiteral( "MAP" ) ).isEmpty() ) { - query.addQueryItem( QStringLiteral( "map" ), params.value( QStringLiteral( "MAP" ) ) ); + query.addQueryItem( QgsServerParameter::name( QgsServerParameter::MAP ), params.value( QStringLiteral( "MAP" ) ) ); } - query.addQueryItem( QStringLiteral( "service" ), QStringLiteral( "WMS" ) ); - query.addQueryItem( QStringLiteral( "version" ), QStringLiteral( "1.3.0" ) ); - query.addQueryItem( QStringLiteral( "request" ), request ); - query.addQueryItem( QStringLiteral( "layers" ), layer ); - query.addQueryItem( QStringLiteral( "styles" ), QString() ); - query.addQueryItem( QStringLiteral( "crs" ), tms.ref ); - query.addQueryItem( QStringLiteral( "bbox" ), bbox ); - query.addQueryItem( QStringLiteral( "width" ), QStringLiteral( "256" ) ); - query.addQueryItem( QStringLiteral( "height" ), QStringLiteral( "256" ) ); - query.addQueryItem( QStringLiteral( "format" ), format ); + query.addQueryItem( QgsServerParameter::name( QgsServerParameter::SERVICE ), QStringLiteral( "WMS" ) ); + query.addQueryItem( QgsServerParameter::name( QgsServerParameter::VERSION_SERVICE ), QStringLiteral( "1.3.0" ) ); + query.addQueryItem( QgsServerParameter::name( QgsServerParameter::REQUEST ), request ); + query.addQueryItem( QgsWmsParameter::name( QgsWmsParameter::LAYERS ), layer ); + query.addQueryItem( QgsWmsParameter::name( QgsWmsParameter::STYLES ), QString() ); + query.addQueryItem( QgsWmsParameter::name( QgsWmsParameter::CRS ), tms.ref ); + query.addQueryItem( QgsWmsParameter::name( QgsWmsParameter::BBOX ), bbox ); + query.addQueryItem( QgsWmsParameter::name( QgsWmsParameter::WIDTH ), QStringLiteral( "256" ) ); + query.addQueryItem( QgsWmsParameter::name( QgsWmsParameter::HEIGHT ), QStringLiteral( "256" ) ); + query.addQueryItem( QgsWmsParameter::name( QgsWmsParameter::FORMAT ), format ); if ( params.format() == QgsWmtsParameters::Format::PNG ) { - query.addQueryItem( QStringLiteral( "transparent" ), QStringLiteral( "true" ) ); + query.addQueryItem( QgsWmsParameter::name( QgsWmsParameter::TRANSPARENT ), QStringLiteral( "true" ) ); } - query.addQueryItem( QStringLiteral( "dpi" ), QStringLiteral( "96" ) ); + query.addQueryItem( QgsWmsParameter::name( QgsWmsParameter::DPI ), QStringLiteral( "96" ) ); return query; } From df03b63bc4d6dd8a3fe9b51952718e0a1ef1d309 Mon Sep 17 00:00:00 2001 From: rldhont Date: Thu, 23 Aug 2018 12:23:16 +0200 Subject: [PATCH 26/33] [Server][Feature][needs-docs] Add ability to define ResourceURL for WMTS --- .../auto_generated/qgsserverprojectutils.sip.in | 9 +++++++++ src/app/qgsprojectproperties.cpp | 3 +++ src/server/qgsserverprojectutils.cpp | 5 +++++ src/server/qgsserverprojectutils.h | 7 +++++++ src/server/services/wmts/qgswmtsutils.cpp | 2 +- src/ui/qgsprojectpropertiesbase.ui | 14 ++++++++++++++ 6 files changed, 39 insertions(+), 1 deletion(-) diff --git a/python/server/auto_generated/qgsserverprojectutils.sip.in b/python/server/auto_generated/qgsserverprojectutils.sip.in index 5d976f083f52..dd0993521fae 100644 --- a/python/server/auto_generated/qgsserverprojectutils.sip.in +++ b/python/server/auto_generated/qgsserverprojectutils.sip.in @@ -413,6 +413,15 @@ Returns the Layer ids list defined in a QGIS project as published in WCS. :param project: the QGIS project :return: the Layer ids list. +%End + + QString wmtsServiceUrl( const QgsProject &project ); +%Docstring +Returns the WMTS service url defined in a QGIS project. + +:param project: the QGIS project + +:return: url if defined in project, an empty string otherwise. %End }; diff --git a/src/app/qgsprojectproperties.cpp b/src/app/qgsprojectproperties.cpp index 0ad40c06dd4f..9cab7ee95927 100644 --- a/src/app/qgsprojectproperties.cpp +++ b/src/app/qgsprojectproperties.cpp @@ -662,6 +662,8 @@ QgsProjectProperties::QgsProjectProperties( QgsMapCanvas *mapCanvas, QWidget *pa mWMSImageQualitySpinBox->setValue( imageQuality ); } + mWMTSUrlLineEdit->setText( QgsProject::instance()->readEntry( QStringLiteral( "WMTSUrl" ), QStringLiteral( "/" ), QLatin1String( "" ) ) ); + bool wmtsProject = QgsProject::instance()->readBoolEntry( QStringLiteral( "WMTSLayers" ), QStringLiteral( "Project" ) ); bool wmtsPngProject = QgsProject::instance()->readBoolEntry( QStringLiteral( "WMTSPngLayers" ), QStringLiteral( "Project" ) ); bool wmtsJpegProject = QgsProject::instance()->readBoolEntry( QStringLiteral( "WMTSJpegLayers" ), QStringLiteral( "Project" ) ); @@ -1262,6 +1264,7 @@ void QgsProjectProperties::apply() QgsProject::instance()->writeEntry( QStringLiteral( "WMSImageQuality" ), QStringLiteral( "/" ), imageQualityValue ); } + QgsProject::instance()->writeEntry( QStringLiteral( "WMTSUrl" ), QStringLiteral( "/" ), mWMTSUrlLineEdit->text() ); bool wmtsProject = false; bool wmtsPngProject = false; bool wmtsJpegProject = false; diff --git a/src/server/qgsserverprojectutils.cpp b/src/server/qgsserverprojectutils.cpp index 9ec7ca1facd3..d89fb3d53eac 100644 --- a/src/server/qgsserverprojectutils.cpp +++ b/src/server/qgsserverprojectutils.cpp @@ -331,3 +331,8 @@ QStringList QgsServerProjectUtils::wcsLayerIds( const QgsProject &project ) { return project.readListEntry( QStringLiteral( "WCSLayers" ), QStringLiteral( "/" ) ); } + +QString QgsServerProjectUtils::wmtsServiceUrl( const QgsProject &project ) +{ + return project.readEntry( QStringLiteral( "WMTSSUrl" ), QStringLiteral( "/" ), "" ); +} diff --git a/src/server/qgsserverprojectutils.h b/src/server/qgsserverprojectutils.h index 9bc4bcdaee5a..72593edde7fb 100644 --- a/src/server/qgsserverprojectutils.h +++ b/src/server/qgsserverprojectutils.h @@ -347,6 +347,13 @@ namespace QgsServerProjectUtils * \returns the Layer ids list. */ SERVER_EXPORT QStringList wcsLayerIds( const QgsProject &project ); + + /** + * Returns the WMTS service url defined in a QGIS project. + * \param project the QGIS project + * \returns url if defined in project, an empty string otherwise. + */ + SERVER_EXPORT QString wmtsServiceUrl( const QgsProject &project ); }; #endif diff --git a/src/server/services/wmts/qgswmtsutils.cpp b/src/server/services/wmts/qgswmtsutils.cpp index 89d5f76780d7..ba2d7c88e5fe 100644 --- a/src/server/services/wmts/qgswmtsutils.cpp +++ b/src/server/services/wmts/qgswmtsutils.cpp @@ -59,7 +59,7 @@ namespace QgsWmts QString href; if ( project ) { - href = QgsServerProjectUtils::wmsServiceUrl( *project ); + href = QgsServerProjectUtils::wmtsServiceUrl( *project ); } // Build default url diff --git a/src/ui/qgsprojectpropertiesbase.ui b/src/ui/qgsprojectpropertiesbase.ui index da42f424d332..d98571533e65 100644 --- a/src/ui/qgsprojectpropertiesbase.ui +++ b/src/ui/qgsprojectpropertiesbase.ui @@ -2502,6 +2502,20 @@ + + + + + + Advertised URL + + + + + + + + From 355a3871fc753cafc90cb08996cbc46314a09e09 Mon Sep 17 00:00:00 2001 From: rldhont Date: Thu, 23 Aug 2018 13:43:16 +0200 Subject: [PATCH 27/33] [Server][Feature][needs-docs] Add ability to define min. scale for WMTS --- src/app/qgsprojectproperties.cpp | 2 ++ src/server/services/wmts/qgswmtsutils.cpp | 6 ++++- src/ui/qgsprojectpropertiesbase.ui | 27 +++++++++++++++++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/app/qgsprojectproperties.cpp b/src/app/qgsprojectproperties.cpp index 9cab7ee95927..5974ee840d7d 100644 --- a/src/app/qgsprojectproperties.cpp +++ b/src/app/qgsprojectproperties.cpp @@ -663,6 +663,7 @@ QgsProjectProperties::QgsProjectProperties( QgsMapCanvas *mapCanvas, QWidget *pa } mWMTSUrlLineEdit->setText( QgsProject::instance()->readEntry( QStringLiteral( "WMTSUrl" ), QStringLiteral( "/" ), QLatin1String( "" ) ) ); + mWMTSMinScaleLineEdit->setValue( QgsProject::instance()->readNumEntry( QStringLiteral( "WMTSMinScale" ), QStringLiteral( "/" ), 5000 ) ); bool wmtsProject = QgsProject::instance()->readBoolEntry( QStringLiteral( "WMTSLayers" ), QStringLiteral( "Project" ) ); bool wmtsPngProject = QgsProject::instance()->readBoolEntry( QStringLiteral( "WMTSPngLayers" ), QStringLiteral( "Project" ) ); @@ -1265,6 +1266,7 @@ void QgsProjectProperties::apply() } QgsProject::instance()->writeEntry( QStringLiteral( "WMTSUrl" ), QStringLiteral( "/" ), mWMTSUrlLineEdit->text() ); + QgsProject::instance()->writeEntry( QStringLiteral( "WMTSMinScale" ), QStringLiteral( "/" ), mWMTSMinScaleLineEdit->value() ); bool wmtsProject = false; bool wmtsPngProject = false; bool wmtsJpegProject = false; diff --git a/src/server/services/wmts/qgswmtsutils.cpp b/src/server/services/wmts/qgswmtsutils.cpp index ba2d7c88e5fe..cb404de8221a 100644 --- a/src/server/services/wmts/qgswmtsutils.cpp +++ b/src/server/services/wmts/qgswmtsutils.cpp @@ -192,7 +192,11 @@ namespace QgsWmts { QList< tileMatrixSetDef > tmsList; - double minScale = getProjectMinScale( project ); + double minScale = project->readNumEntry( QStringLiteral( "WMTSMinScale" ), QStringLiteral( "/" ), -1.0 ); + if ( minScale == -1.0 ) + { + minScale = getProjectMinScale( project ); + } QStringList crsList = QgsServerProjectUtils::wmsOutputCrsList( *project ); for ( const QString &crsStr : crsList ) diff --git a/src/ui/qgsprojectpropertiesbase.ui b/src/ui/qgsprojectpropertiesbase.ui index d98571533e65..8ad7cd99322b 100644 --- a/src/ui/qgsprojectpropertiesbase.ui +++ b/src/ui/qgsprojectpropertiesbase.ui @@ -2503,6 +2503,30 @@ + + + + + Minimum scale + + + + + + + 1 + + + 1000000000 + + + 5000 + + + + + + @@ -2968,6 +2992,9 @@ mMaxWidthLineEdit mMaxHeightLineEdit mWMSImageQualitySpinBox + twWmtsLayers + mWMTSMinScaleLineEdit + mWMTSUrlLineEdit twWFSLayers pbnWFSLayersSelectAll pbnWFSLayersDeselectAll From ba246532b1c7f4d35203537b36dfef1b478c328f Mon Sep 17 00:00:00 2001 From: rldhont Date: Fri, 24 Aug 2018 15:05:52 +0200 Subject: [PATCH 28/33] [Server][Feature][needs-docs] Enhancing TileMatrixSetLimits --- .../services/wmts/qgswmtsgetcapabilities.cpp | 208 +---- src/server/services/wmts/qgswmtsutils.cpp | 289 +++++- src/server/services/wmts/qgswmtsutils.h | 25 + .../qgis_server/wmts_getcapabilities.txt | 864 ++++-------------- 4 files changed, 499 insertions(+), 887 deletions(-) diff --git a/src/server/services/wmts/qgswmtsgetcapabilities.cpp b/src/server/services/wmts/qgswmtsgetcapabilities.cpp index 3ad7d659edab..04007aba2df8 100644 --- a/src/server/services/wmts/qgswmtsgetcapabilities.cpp +++ b/src/server/services/wmts/qgswmtsgetcapabilities.cpp @@ -32,8 +32,6 @@ namespace QgsWmts { namespace { - QList< layerDef > getWmtsLayerList( QgsServerInterface *serverIface, const QgsProject *project ); - void appendLayerElements( QDomDocument &doc, QDomElement &contentsElement, QList< layerDef > wmtsLayers, QList< tileMatrixSetDef > tmsList, const QgsProject *project ); @@ -322,182 +320,6 @@ namespace QgsWmts } namespace { - QList< layerDef > getWmtsLayerList( QgsServerInterface *serverIface, const QgsProject *project ) - { - QList< layerDef > wmtsLayers; -#ifdef HAVE_SERVER_PYTHON_PLUGINS - QgsAccessControl *accessControl = serverIface->accessControls(); -#endif - QgsCoordinateReferenceSystem wgs84 = QgsCoordinateReferenceSystem::fromOgcWmsCrs( GEO_EPSG_CRS_AUTHID ); - - QStringList nonIdentifiableLayers = project->nonIdentifiableLayers(); - - // WMTS Project configuration - bool wmtsProject = project->readBoolEntry( QStringLiteral( "WMTSLayers" ), QStringLiteral( "Project" ) ); - - // Root Layer name - QString rootLayerName = QgsServerProjectUtils::wmsRootName( *project ); - if ( rootLayerName.isEmpty() && !project->title().isEmpty() ) - { - rootLayerName = project->title(); - } - - if ( wmtsProject && !rootLayerName.isEmpty() ) - { - layerDef pLayer; - pLayer.id = rootLayerName; - - if ( !project->title().isEmpty() ) - { - pLayer.title = project->title(); - pLayer.abstract = project->title(); - } - - //transform the project native CRS into WGS84 - QgsRectangle projRect = QgsServerProjectUtils::wmsExtent( *project ); - QgsCoordinateReferenceSystem projCrs = project->crs(); - QgsCoordinateTransform exGeoTransform( projCrs, wgs84, project ); - try - { - pLayer.wgs84BoundingRect = exGeoTransform.transformBoundingBox( projRect ); - } - catch ( const QgsCsException & ) - { - pLayer.wgs84BoundingRect = QgsRectangle( -180, -90, 180, 90 ); - } - - // Formats - bool wmtsPngProject = project->readBoolEntry( QStringLiteral( "WMTSPngLayers" ), QStringLiteral( "Project" ) ); - if ( wmtsPngProject ) - pLayer.formats << QStringLiteral( "image/png" ); - bool wmtsJpegProject = project->readBoolEntry( QStringLiteral( "WMTSJpegLayers" ), QStringLiteral( "Project" ) ); - if ( wmtsJpegProject ) - pLayer.formats << QStringLiteral( "image/jpeg" ); - - // Project is not queryable in WMS - //pLayer.queryable = ( nonIdentifiableLayers.count() != project->count() ); - pLayer.queryable = false; - - wmtsLayers.append( pLayer ); - } - - QStringList wmtsGroupNameList = project->readListEntry( QStringLiteral( "WMTSLayers" ), QStringLiteral( "Group" ) ); - if ( !wmtsGroupNameList.isEmpty() ) - { - QgsLayerTreeGroup *treeRoot = project->layerTreeRoot(); - - QStringList wmtsPngGroupNameList = project->readListEntry( QStringLiteral( "WMTSPngLayers" ), QStringLiteral( "Group" ) ); - QStringList wmtsJpegGroupNameList = project->readListEntry( QStringLiteral( "WMTSJpegLayers" ), QStringLiteral( "Group" ) ); - - for ( const QString gName : wmtsGroupNameList ) - { - QgsLayerTreeGroup *treeGroup = treeRoot->findGroup( gName ); - if ( !treeGroup ) - { - continue; - } - - layerDef pLayer; - pLayer.id = treeGroup->customProperty( QStringLiteral( "wmsShortName" ) ).toString(); - if ( pLayer.id.isEmpty() ) - pLayer.id = gName; - - pLayer.title = treeGroup->customProperty( QStringLiteral( "wmsTitle" ) ).toString(); - if ( pLayer.title.isEmpty() ) - pLayer.title = gName; - - pLayer.abstract = treeGroup->customProperty( QStringLiteral( "wmsAbstract" ) ).toString(); - - QgsRectangle wgs84BoundingRect; - bool queryable = false; - for ( QgsLayerTreeLayer *layer : treeGroup->findLayers() ) - { - QgsMapLayer *l = layer->layer(); - if ( !l ) - { - continue; - } - //transform the layer native CRS into WGS84 - QgsCoordinateReferenceSystem layerCrs = l->crs(); - QgsCoordinateTransform exGeoTransform( layerCrs, wgs84, project ); - try - { - wgs84BoundingRect.combineExtentWith( exGeoTransform.transformBoundingBox( l->extent() ) ); - } - catch ( const QgsCsException & ) - { - wgs84BoundingRect.combineExtentWith( QgsRectangle( -180, -90, 180, 90 ) ); - } - if ( !queryable && !nonIdentifiableLayers.contains( l->id() ) ) - { - queryable = true; - } - } - pLayer.wgs84BoundingRect = wgs84BoundingRect; - pLayer.queryable = queryable; - - // Formats - if ( wmtsPngGroupNameList.contains( gName ) ) - pLayer.formats << QStringLiteral( "image/png" ); - if ( wmtsJpegGroupNameList.contains( gName ) ) - pLayer.formats << QStringLiteral( "image/jpeg" ); - - wmtsLayers.append( pLayer ); - } - } - - QStringList wmtsLayerIdList = project->readListEntry( QStringLiteral( "WMTSLayers" ), QStringLiteral( "Layer" ) ); - QStringList wmtsPngLayerIdList = project->readListEntry( QStringLiteral( "WMTSPngLayers" ), QStringLiteral( "Layer" ) ); - QStringList wmtsJpegLayerIdList = project->readListEntry( QStringLiteral( "WMTSJpegLayers" ), QStringLiteral( "Layer" ) ); - - for ( const QString lId : wmtsLayerIdList ) - { - QgsMapLayer *l = project->mapLayer( lId ); - if ( !l ) - { - continue; - } -#ifdef HAVE_SERVER_PYTHON_PLUGINS - if ( !accessControl->layerReadPermission( l ) ) - { - continue; - } -#endif - - layerDef pLayer; - pLayer.id = l->name(); - if ( !l->shortName().isEmpty() ) - pLayer.id = l->shortName(); - pLayer.id = pLayer.id.replace( ' ', '_' ); - - pLayer.title = l->title(); - pLayer.abstract = l->abstract(); - - //transform the layer native CRS into WGS84 - QgsCoordinateReferenceSystem layerCrs = l->crs(); - QgsCoordinateTransform exGeoTransform( layerCrs, wgs84, project ); - try - { - pLayer.wgs84BoundingRect = exGeoTransform.transformBoundingBox( l->extent() ); - } - catch ( const QgsCsException & ) - { - pLayer.wgs84BoundingRect = QgsRectangle( -180, -90, 180, 90 ); - } - - // Formats - if ( wmtsPngLayerIdList.contains( lId ) ) - pLayer.formats << QStringLiteral( "image/png" ); - if ( wmtsJpegLayerIdList.contains( lId ) ) - pLayer.formats << QStringLiteral( "image/jpeg" ); - - pLayer.queryable = ( !nonIdentifiableLayers.contains( l->id() ) ); - - wmtsLayers.append( pLayer ); - } - return wmtsLayers; - } - void appendLayerElements( QDomDocument &doc, QDomElement &contentsElement, QList< layerDef > wmtsLayers, QList< tileMatrixSetDef > tmsList, const QgsProject *project ) @@ -616,19 +438,10 @@ namespace QgsWmts for ( const tileMatrixSetDef tms : tmsList ) { - if ( tms.ref != QLatin1String( "EPSG:4326" ) ) + tileMatrixSetLinkDef tmsl = getLayerTileMatrixSetLink( wmtsLayer, tms, project ); + if ( tmsl.ref.isEmpty() || tmsl.ref != tms.ref ) { - QgsRectangle rect; - QgsCoordinateReferenceSystem crs = QgsCoordinateReferenceSystem::fromOgcWmsCrs( tms.ref ); - QgsCoordinateTransform exGeoTransform( wgs84, crs, project ); - try - { - rect = exGeoTransform.transformBoundingBox( wmtsLayer.wgs84BoundingRect ); - } - catch ( const QgsCsException & ) - { - continue; - } + continue; } //wmts:TileMatrixSetLink @@ -641,8 +454,7 @@ namespace QgsWmts //wmts:TileMatrixSetLimits QDomElement tmsLimitsElement = doc.createElement( QStringLiteral( "TileMatrixSetLimits" )/*wmts:TileMatrixSetLimits*/ ); - int tmIdx = 0; - for ( const tileMatrixDef tm : tms.tileMatrixList ) + for ( int tmIdx : tmsl.tileMatrixLimits.keys() ) { QDomElement tmLimitsElement = doc.createElement( QStringLiteral( "TileMatrixLimits" )/*wmts:TileMatrixLimits*/ ); @@ -651,29 +463,29 @@ namespace QgsWmts tmIdentifierElem.appendChild( tmIdentifierText ); tmLimitsElement.appendChild( tmIdentifierElem ); + tileMatrixLimitDef tml = tmsl.tileMatrixLimits[tmIdx]; + QDomElement minTileColElem = doc.createElement( QStringLiteral( "MinTileCol" ) ); - QDomText minTileColText = doc.createTextNode( QString::number( 0 ) ); + QDomText minTileColText = doc.createTextNode( QString::number( tml.minCol ) ); minTileColElem.appendChild( minTileColText ); tmLimitsElement.appendChild( minTileColElem ); QDomElement maxTileColElem = doc.createElement( QStringLiteral( "MaxTileCol" ) ); - QDomText maxTileColText = doc.createTextNode( QString::number( tm.col ) ); + QDomText maxTileColText = doc.createTextNode( QString::number( tml.maxCol ) ); maxTileColElem.appendChild( maxTileColText ); tmLimitsElement.appendChild( maxTileColElem ); QDomElement minTileRowElem = doc.createElement( QStringLiteral( "MinTileRow" ) ); - QDomText minTileRowText = doc.createTextNode( QString::number( 0 ) ); + QDomText minTileRowText = doc.createTextNode( QString::number( tml.minRow ) ); minTileRowElem.appendChild( minTileRowText ); tmLimitsElement.appendChild( minTileRowElem ); QDomElement maxTileRowElem = doc.createElement( QStringLiteral( "MaxTileRow" ) ); - QDomText maxTileRowText = doc.createTextNode( QString::number( tm.row ) ); + QDomText maxTileRowText = doc.createTextNode( QString::number( tml.maxRow ) ); maxTileRowElem.appendChild( maxTileRowText ); tmLimitsElement.appendChild( maxTileRowElem ); tmsLimitsElement.appendChild( tmLimitsElement ); - - ++tmIdx; } tmslElement.appendChild( tmsLimitsElement ); diff --git a/src/server/services/wmts/qgswmtsutils.cpp b/src/server/services/wmts/qgswmtsutils.cpp index cb404de8221a..a19ec05dfff7 100644 --- a/src/server/services/wmts/qgswmtsutils.cpp +++ b/src/server/services/wmts/qgswmtsutils.cpp @@ -105,13 +105,44 @@ namespace QgsWmts double scaleDenominator = 0.0; int colRes = ( tmi.extent.xMaximum() - tmi.extent.xMinimum() ) / tileWidth; int rowRes = ( tmi.extent.yMaximum() - tmi.extent.yMinimum() ) / tileHeight; - if ( colRes < rowRes ) - scaleDenominator = colRes * INCHES_PER_UNIT[ tmi.unit ] * METERS_PER_INCH / 0.00028; + if ( colRes > rowRes ) + scaleDenominator = std::ceil( colRes * INCHES_PER_UNIT[ tmi.unit ] * METERS_PER_INCH / 0.00028 ); else - scaleDenominator = rowRes * INCHES_PER_UNIT[ tmi.unit ] * METERS_PER_INCH / 0.00028; + scaleDenominator = std::ceil( rowRes * INCHES_PER_UNIT[ tmi.unit ] * METERS_PER_INCH / 0.00028 ); + + // Update extent to get a square one + QgsRectangle extent = tmi.extent; + double res = 0.00028 * scaleDenominator / METERS_PER_INCH / INCHES_PER_UNIT[ tmi.unit ]; + int col = std::ceil( ( extent.xMaximum() - extent.xMinimum() ) / ( tileWidth * res ) ); + int row = std::ceil( ( extent.yMaximum() - extent.yMinimum() ) / ( tileHeight * res ) ); + if ( col > 1 || row > 1 ) + { + // Update scale + if ( col > row ) + { + res = col * res; + scaleDenominator = col * scaleDenominator; + } + else + { + res = row * res; + scaleDenominator = row * scaleDenominator; + } + // set col and row to 1 for the square + col = 1; + row = 1; + } + // Calculate extent + double left = ( extent.xMinimum() + ( extent.xMaximum() - extent.xMinimum() ) / 2.0 ) - ( col / 2.0 ) * ( tileWidth * res ); + double bottom = ( extent.yMinimum() + ( extent.yMaximum() - extent.yMinimum() ) / 2.0 ) - ( row / 2.0 ) * ( tileHeight * res ); + double right = ( extent.xMinimum() + ( extent.xMaximum() - extent.xMinimum() ) / 2.0 ) + ( col / 2.0 ) * ( tileWidth * res ); + double top = ( extent.yMinimum() + ( extent.yMaximum() - extent.yMinimum() ) / 2.0 ) + ( row / 2.0 ) * ( tileHeight * res ); + tmi.extent = QgsRectangle( left, bottom, right, top ); + tmi.scaleDenominator = scaleDenominator; tileMatrixInfoMap[crsStr] = tmi; + return tmi; } @@ -126,8 +157,8 @@ namespace QgsWmts { double scale = scaleDenominator; double res = 0.00028 * scale / METERS_PER_INCH / INCHES_PER_UNIT[ unit ]; - int col = std::round( ( extent.xMaximum() - extent.xMinimum() ) / ( tileWidth * res ) ); - int row = std::round( ( extent.yMaximum() - extent.yMinimum() ) / ( tileHeight * res ) ); + int col = std::ceil( ( extent.xMaximum() - extent.xMinimum() ) / ( tileWidth * res ) ); + int row = std::ceil( ( extent.yMaximum() - extent.yMinimum() ) / ( tileHeight * res ) ); double left = ( extent.xMinimum() + ( extent.xMaximum() - extent.xMinimum() ) / 2.0 ) - ( col / 2.0 ) * ( tileWidth * res ); double top = ( extent.yMinimum() + ( extent.yMaximum() - extent.yMinimum() ) / 2.0 ) + ( row / 2.0 ) * ( tileHeight * res ); @@ -211,6 +242,254 @@ namespace QgsWmts return tmsList; } + QList< layerDef > getWmtsLayerList( QgsServerInterface *serverIface, const QgsProject *project ) + { + QList< layerDef > wmtsLayers; +#ifdef HAVE_SERVER_PYTHON_PLUGINS + QgsAccessControl *accessControl = serverIface->accessControls(); +#endif + QgsCoordinateReferenceSystem wgs84 = QgsCoordinateReferenceSystem::fromOgcWmsCrs( GEO_EPSG_CRS_AUTHID ); + + QStringList nonIdentifiableLayers = project->nonIdentifiableLayers(); + + // WMTS Project configuration + bool wmtsProject = project->readBoolEntry( QStringLiteral( "WMTSLayers" ), QStringLiteral( "Project" ) ); + + // Root Layer name + QString rootLayerName = QgsServerProjectUtils::wmsRootName( *project ); + if ( rootLayerName.isEmpty() && !project->title().isEmpty() ) + { + rootLayerName = project->title(); + } + + if ( wmtsProject && !rootLayerName.isEmpty() ) + { + layerDef pLayer; + pLayer.id = rootLayerName; + + if ( !project->title().isEmpty() ) + { + pLayer.title = project->title(); + pLayer.abstract = project->title(); + } + + //transform the project native CRS into WGS84 + QgsRectangle projRect = QgsServerProjectUtils::wmsExtent( *project ); + QgsCoordinateReferenceSystem projCrs = project->crs(); + QgsCoordinateTransform exGeoTransform( projCrs, wgs84, project ); + try + { + pLayer.wgs84BoundingRect = exGeoTransform.transformBoundingBox( projRect ); + } + catch ( const QgsCsException & ) + { + pLayer.wgs84BoundingRect = QgsRectangle( -180, -90, 180, 90 ); + } + + // Formats + bool wmtsPngProject = project->readBoolEntry( QStringLiteral( "WMTSPngLayers" ), QStringLiteral( "Project" ) ); + if ( wmtsPngProject ) + pLayer.formats << QStringLiteral( "image/png" ); + bool wmtsJpegProject = project->readBoolEntry( QStringLiteral( "WMTSJpegLayers" ), QStringLiteral( "Project" ) ); + if ( wmtsJpegProject ) + pLayer.formats << QStringLiteral( "image/jpeg" ); + + // Project is not queryable in WMS + //pLayer.queryable = ( nonIdentifiableLayers.count() != project->count() ); + pLayer.queryable = false; + + wmtsLayers.append( pLayer ); + } + + QStringList wmtsGroupNameList = project->readListEntry( QStringLiteral( "WMTSLayers" ), QStringLiteral( "Group" ) ); + if ( !wmtsGroupNameList.isEmpty() ) + { + QgsLayerTreeGroup *treeRoot = project->layerTreeRoot(); + + QStringList wmtsPngGroupNameList = project->readListEntry( QStringLiteral( "WMTSPngLayers" ), QStringLiteral( "Group" ) ); + QStringList wmtsJpegGroupNameList = project->readListEntry( QStringLiteral( "WMTSJpegLayers" ), QStringLiteral( "Group" ) ); + + for ( const QString gName : wmtsGroupNameList ) + { + QgsLayerTreeGroup *treeGroup = treeRoot->findGroup( gName ); + if ( !treeGroup ) + { + continue; + } + + layerDef pLayer; + pLayer.id = treeGroup->customProperty( QStringLiteral( "wmsShortName" ) ).toString(); + if ( pLayer.id.isEmpty() ) + pLayer.id = gName; + + pLayer.title = treeGroup->customProperty( QStringLiteral( "wmsTitle" ) ).toString(); + if ( pLayer.title.isEmpty() ) + pLayer.title = gName; + + pLayer.abstract = treeGroup->customProperty( QStringLiteral( "wmsAbstract" ) ).toString(); + + QgsRectangle wgs84BoundingRect; + bool queryable = false; + double maxScale = 0.0; + double minScale = 0.0; + for ( QgsLayerTreeLayer *layer : treeGroup->findLayers() ) + { + QgsMapLayer *l = layer->layer(); + if ( !l ) + { + continue; + } + //transform the layer native CRS into WGS84 + QgsCoordinateReferenceSystem layerCrs = l->crs(); + QgsCoordinateTransform exGeoTransform( layerCrs, wgs84, project ); + try + { + wgs84BoundingRect.combineExtentWith( exGeoTransform.transformBoundingBox( l->extent() ) ); + } + catch ( const QgsCsException & ) + { + wgs84BoundingRect.combineExtentWith( QgsRectangle( -180, -90, 180, 90 ) ); + } + if ( !queryable && !nonIdentifiableLayers.contains( l->id() ) ) + { + queryable = true; + } + + double lMaxScale = l->maximumScale(); + if ( lMaxScale > 0.0 && lMaxScale > maxScale ) + { + maxScale = lMaxScale; + } + double lMinScale = l->minimumScale(); + if ( lMinScale > 0.0 && ( minScale == 0.0 || lMinScale < minScale ) ) + { + minScale = lMinScale; + } + } + pLayer.wgs84BoundingRect = wgs84BoundingRect; + pLayer.queryable = queryable; + pLayer.maxScale = maxScale; + pLayer.minScale = minScale; + + // Formats + if ( wmtsPngGroupNameList.contains( gName ) ) + pLayer.formats << QStringLiteral( "image/png" ); + if ( wmtsJpegGroupNameList.contains( gName ) ) + pLayer.formats << QStringLiteral( "image/jpeg" ); + + wmtsLayers.append( pLayer ); + } + } + + QStringList wmtsLayerIdList = project->readListEntry( QStringLiteral( "WMTSLayers" ), QStringLiteral( "Layer" ) ); + QStringList wmtsPngLayerIdList = project->readListEntry( QStringLiteral( "WMTSPngLayers" ), QStringLiteral( "Layer" ) ); + QStringList wmtsJpegLayerIdList = project->readListEntry( QStringLiteral( "WMTSJpegLayers" ), QStringLiteral( "Layer" ) ); + + for ( const QString lId : wmtsLayerIdList ) + { + QgsMapLayer *l = project->mapLayer( lId ); + if ( !l ) + { + continue; + } +#ifdef HAVE_SERVER_PYTHON_PLUGINS + if ( !accessControl->layerReadPermission( l ) ) + { + continue; + } +#endif + + layerDef pLayer; + pLayer.id = l->name(); + if ( !l->shortName().isEmpty() ) + pLayer.id = l->shortName(); + pLayer.id = pLayer.id.replace( ' ', '_' ); + + pLayer.title = l->title(); + pLayer.abstract = l->abstract(); + + //transform the layer native CRS into WGS84 + QgsCoordinateReferenceSystem layerCrs = l->crs(); + QgsCoordinateTransform exGeoTransform( layerCrs, wgs84, project ); + try + { + pLayer.wgs84BoundingRect = exGeoTransform.transformBoundingBox( l->extent() ); + } + catch ( const QgsCsException & ) + { + pLayer.wgs84BoundingRect = QgsRectangle( -180, -90, 180, 90 ); + } + + // Formats + if ( wmtsPngLayerIdList.contains( lId ) ) + pLayer.formats << QStringLiteral( "image/png" ); + if ( wmtsJpegLayerIdList.contains( lId ) ) + pLayer.formats << QStringLiteral( "image/jpeg" ); + + pLayer.queryable = ( !nonIdentifiableLayers.contains( l->id() ) ); + + pLayer.maxScale = l->maximumScale(); + pLayer.minScale = l->minimumScale(); + + wmtsLayers.append( pLayer ); + } + return wmtsLayers; + } + + tileMatrixSetLinkDef getLayerTileMatrixSetLink( const layerDef layer, const tileMatrixSetDef tms, const QgsProject *project ) + { + tileMatrixSetLinkDef tmsl; + + QMap< int, tileMatrixLimitDef > tileMatrixLimits; + + QgsRectangle rect( layer.wgs84BoundingRect ); + if ( tms.ref != QLatin1String( "EPSG:4326" ) ) + { + QgsCoordinateReferenceSystem crs = QgsCoordinateReferenceSystem::fromOgcWmsCrs( tms.ref ); + QgsCoordinateReferenceSystem wgs84 = QgsCoordinateReferenceSystem::fromOgcWmsCrs( GEO_EPSG_CRS_AUTHID ); + QgsCoordinateTransform exGeoTransform( wgs84, crs, project ); + try + { + rect = exGeoTransform.transformBoundingBox( layer.wgs84BoundingRect ); + } + catch ( const QgsCsException & ) + { + return tmsl; + } + } + tmsl.ref = tms.ref; + + rect = rect.intersect( tms.extent ); + + int tmIdx = -1; + for ( const tileMatrixDef tm : tms.tileMatrixList ) + { + ++tmIdx; + + if ( layer.maxScale > 0.0 && tm.scaleDenominator > layer.maxScale ) + { + continue; + } + if ( layer.minScale > 0.0 && tm.scaleDenominator < layer.minScale ) + { + continue; + } + + double res = tm.resolution; + + tileMatrixLimitDef tml; + tml.minCol = std::floor( ( rect.xMinimum() - tm.left ) / ( tileWidth * res ) ); + tml.maxCol = std::ceil( ( rect.xMaximum() - tm.left ) / ( tileWidth * res ) ) - 1; + tml.minRow = std::floor( ( tm.top - rect.yMaximum() ) / ( tileHeight * res ) ); + tml.maxRow = std::ceil( ( tm.top - rect.yMinimum() ) / ( tileHeight * res ) ) - 1; + + tileMatrixLimits[tmIdx] = tml; + } + + tmsl.tileMatrixLimits = tileMatrixLimits; + return tmsl; + } + QUrlQuery translateWmtsParamToWmsQueryItem( const QString &request, const QgsWmtsParameters ¶ms, const QgsProject *project, QgsServerInterface *serverIface ) { diff --git a/src/server/services/wmts/qgswmtsutils.h b/src/server/services/wmts/qgswmtsutils.h index a6d5a439012b..146c03195d4b 100644 --- a/src/server/services/wmts/qgswmtsutils.h +++ b/src/server/services/wmts/qgswmtsutils.h @@ -72,6 +72,24 @@ namespace QgsWmts QList< tileMatrixDef > tileMatrixList; }; + struct tileMatrixLimitDef + { + int minCol; + + int maxCol; + + int minRow; + + int maxRow; + }; + + struct tileMatrixSetLinkDef + { + QString ref; + + QMap< int, tileMatrixLimitDef > tileMatrixLimits; + }; + struct layerDef { QString id; @@ -85,6 +103,10 @@ namespace QgsWmts QStringList formats; bool queryable = false; + + double maxScale = 0.0; + + double minScale = 0.0; }; /** @@ -107,6 +129,9 @@ namespace QgsWmts double getProjectMinScale( const QgsProject *project ); QList< tileMatrixSetDef > getTileMatrixSetList( const QgsProject *project ); + QList< layerDef > getWmtsLayerList( QgsServerInterface *serverIface, const QgsProject *project ); + tileMatrixSetLinkDef getLayerTileMatrixSetLink( const layerDef layer, const tileMatrixSetDef tms, const QgsProject *project ); + /** * Translate WMTS parameters to WMS query item */ diff --git a/tests/testdata/qgis_server/wmts_getcapabilities.txt b/tests/testdata/qgis_server/wmts_getcapabilities.txt index 4a8ae38ff319..90c1e18898c8 100644 --- a/tests/testdata/qgis_server/wmts_getcapabilities.txt +++ b/tests/testdata/qgis_server/wmts_getcapabilities.txt @@ -1,7 +1,7 @@ Content-Type: text/xml; charset=utf-8 - + OGC WMTS 1.0.0 @@ -63,149 +63,149 @@ Content-Type: text/xml; charset=utf-8 0 0 - 1 + 0 0 - 1 + 0 1 0 - 2 + 1 0 - 2 + 1 2 0 - 4 + 3 0 - 4 + 3 3 0 - 8 + 7 0 - 8 + 6 4 0 - 16 + 15 0 - 16 + 12 5 0 - 32 + 31 0 - 32 + 24 6 0 - 64 - 0 - 64 + 63 + 1 + 49 7 - 0 - 128 - 0 - 128 + 1 + 127 + 2 + 99 8 - 0 - 256 - 0 - 256 + 3 + 254 + 5 + 198 9 - 0 - 512 - 0 - 512 + 7 + 509 + 11 + 397 10 - 0 - 1024 - 0 - 1024 + 14 + 1018 + 22 + 794 11 - 0 - 2048 - 0 - 2048 + 29 + 2036 + 45 + 1588 12 - 0 - 4096 - 0 - 4096 + 59 + 4072 + 91 + 3177 13 - 0 - 8192 - 0 - 8192 + 119 + 8144 + 182 + 6355 14 - 0 - 16384 - 0 - 16384 + 238 + 16289 + 365 + 12711 15 - 0 - 32768 - 0 - 32768 + 476 + 32579 + 730 + 25423 16 - 0 - 65536 - 0 - 65536 + 952 + 65159 + 1461 + 50846 17 - 0 - 131072 - 0 - 131072 + 1905 + 130318 + 2923 + 101693 18 - 0 - 262144 - 0 - 262144 + 3810 + 260637 + 5846 + 203386 19 - 0 - 524288 - 0 - 524288 + 7621 + 521274 + 11692 + 406772 20 - 0 - 1048576 - 0 - 1048576 + 15243 + 1042549 + 23384 + 813545 @@ -215,142 +215,142 @@ Content-Type: text/xml; charset=utf-8 0 0 - 2 + 1 0 - 1 + 0 1 0 - 4 + 3 0 - 2 + 1 2 0 - 8 + 7 0 - 4 + 3 3 0 - 16 + 15 0 - 8 + 7 4 0 - 32 + 31 0 - 16 + 14 5 0 - 64 - 0 - 32 + 63 + 1 + 28 6 - 0 - 128 - 0 - 64 + 1 + 127 + 2 + 56 7 - 0 - 256 - 0 - 128 + 3 + 254 + 4 + 113 8 - 0 - 511 - 0 - 256 + 7 + 508 + 8 + 227 9 - 0 - 1022 - 0 - 511 + 14 + 1016 + 16 + 454 10 - 0 - 2044 - 0 - 1022 + 29 + 2032 + 32 + 908 11 - 0 - 4089 - 0 - 2044 + 59 + 4065 + 64 + 1816 12 - 0 - 8177 - 0 - 4089 + 118 + 8130 + 129 + 3633 13 - 0 - 16354 - 0 - 8177 + 237 + 16260 + 258 + 7266 14 - 0 - 32709 - 0 - 16354 + 475 + 32520 + 517 + 14533 15 - 0 - 65418 - 0 - 32709 + 951 + 65041 + 1034 + 29066 16 - 0 - 130836 - 0 - 65418 + 1902 + 130083 + 2068 + 58133 17 - 0 - 261672 - 0 - 130836 + 3804 + 260167 + 4137 + 116267 18 - 0 - 523344 - 0 - 261672 + 7608 + 520335 + 8274 + 232535 19 - 0 - 1046687 - 0 - 523344 + 15216 + 1040671 + 16549 + 465071 @@ -382,149 +382,23 @@ Content-Type: text/xml; charset=utf-8 0 0 - 1 + 0 0 - 1 + 0 1 0 - 2 + 1 0 - 2 + 1 2 0 - 4 - 0 - 4 - - - 3 - 0 - 8 - 0 - 8 - - - 4 - 0 - 16 - 0 - 16 - - - 5 - 0 - 32 - 0 - 32 - - - 6 - 0 - 64 - 0 - 64 - - - 7 - 0 - 128 - 0 - 128 - - - 8 - 0 - 256 - 0 - 256 - - - 9 - 0 - 512 - 0 - 512 - - - 10 - 0 - 1024 - 0 - 1024 - - - 11 - 0 - 2048 - 0 - 2048 - - - 12 - 0 - 4096 + 3 0 - 4096 - - - 13 - 0 - 8192 - 0 - 8192 - - - 14 - 0 - 16384 - 0 - 16384 - - - 15 - 0 - 32768 - 0 - 32768 - - - 16 - 0 - 65536 - 0 - 65536 - - - 17 - 0 - 131072 - 0 - 131072 - - - 18 - 0 - 262144 - 0 - 262144 - - - 19 - 0 - 524288 - 0 - 524288 - - - 20 - 0 - 1048576 - 0 - 1048576 + 3 @@ -534,142 +408,16 @@ Content-Type: text/xml; charset=utf-8 0 0 - 2 + 1 0 - 1 + 0 1 0 - 4 - 0 - 2 - - - 2 - 0 - 8 - 0 - 4 - - - 3 - 0 - 16 - 0 - 8 - - - 4 - 0 - 32 - 0 - 16 - - - 5 - 0 - 64 - 0 - 32 - - - 6 - 0 - 128 - 0 - 64 - - - 7 - 0 - 256 - 0 - 128 - - - 8 - 0 - 511 - 0 - 256 - - - 9 - 0 - 1022 - 0 - 511 - - - 10 - 0 - 2044 - 0 - 1022 - - - 11 - 0 - 4089 - 0 - 2044 - - - 12 - 0 - 8177 - 0 - 4089 - - - 13 - 0 - 16354 - 0 - 8177 - - - 14 - 0 - 32709 - 0 - 16354 - - - 15 - 0 - 65418 + 3 0 - 32709 - - - 16 - 0 - 130836 - 0 - 65418 - - - 17 - 0 - 261672 - 0 - 130836 - - - 18 - 0 - 523344 - 0 - 261672 - - - 19 - 0 - 1046687 - 0 - 523344 + 1 @@ -700,149 +448,23 @@ Content-Type: text/xml; charset=utf-8 0 0 - 1 + 0 0 - 1 + 0 1 0 - 2 + 1 0 - 2 + 1 2 0 - 4 - 0 - 4 - - - 3 - 0 - 8 - 0 - 8 - - - 4 - 0 - 16 - 0 - 16 - - - 5 - 0 - 32 - 0 - 32 - - - 6 - 0 - 64 - 0 - 64 - - - 7 - 0 - 128 - 0 - 128 - - - 8 - 0 - 256 - 0 - 256 - - - 9 - 0 - 512 - 0 - 512 - - - 10 - 0 - 1024 - 0 - 1024 - - - 11 - 0 - 2048 - 0 - 2048 - - - 12 - 0 - 4096 - 0 - 4096 - - - 13 - 0 - 8192 - 0 - 8192 - - - 14 - 0 - 16384 - 0 - 16384 - - - 15 - 0 - 32768 - 0 - 32768 - - - 16 - 0 - 65536 - 0 - 65536 - - - 17 - 0 - 131072 - 0 - 131072 - - - 18 - 0 - 262144 - 0 - 262144 - - - 19 - 0 - 524288 + 3 0 - 524288 - - - 20 - 0 - 1048576 - 0 - 1048576 + 2 @@ -852,142 +474,16 @@ Content-Type: text/xml; charset=utf-8 0 0 - 2 + 1 0 - 1 + 0 1 0 - 4 - 0 - 2 - - - 2 - 0 - 8 - 0 - 4 - - - 3 - 0 - 16 - 0 - 8 - - - 4 - 0 - 32 - 0 - 16 - - - 5 - 0 - 64 - 0 - 32 - - - 6 - 0 - 128 - 0 - 64 - - - 7 - 0 - 256 - 0 - 128 - - - 8 - 0 - 511 - 0 - 256 - - - 9 - 0 - 1022 - 0 - 511 - - - 10 - 0 - 2044 - 0 - 1022 - - - 11 - 0 - 4089 + 3 0 - 2044 - - - 12 - 0 - 8177 - 0 - 4089 - - - 13 - 0 - 16354 - 0 - 8177 - - - 14 - 0 - 32709 - 0 - 16354 - - - 15 - 0 - 65418 - 0 - 32709 - - - 16 - 0 - 130836 - 0 - 65418 - - - 17 - 0 - 261672 - 0 - 130836 - - - 18 - 0 - 523344 - 0 - 261672 - - - 19 - 0 - 1046687 - 0 - 523344 + 1 @@ -1263,65 +759,65 @@ Content-Type: text/xml; charset=utf-8 8 1091957.546931 - -179.972618 90 + -180 90 256 256 - 511 + 512 256 9 545978.773466 - -179.972618 89.986309 + -180 90 256 256 - 1022 - 511 + 1023 + 512 10 272989.386733 - -179.972618 89.986309 + -180 90 256 256 - 2044 - 1022 + 2045 + 1023 11 136494.693366 - -180 89.986309 + -180 90 256 256 4089 - 2044 + 2045 12 68247.346683 - -179.99463 90 + -180 90 256 256 - 8177 + 8178 4089 13 34123.673342 - -179.99463 89.997315 + -180 90 256 256 - 16354 - 8177 + 16355 + 8178 14 17061.836671 - -180 89.997315 + -180 90 256 256 32709 - 16354 + 16355 15 @@ -1362,10 +858,10 @@ Content-Type: text/xml; charset=utf-8 19 533.182396 - -179.999961 90 + -180 90 256 256 - 1046687 + 1046688 523344 From 6cb9997a0d183e4cbb7a9ae4451d4a9310e6e7fa Mon Sep 17 00:00:00 2001 From: rldhont Date: Fri, 24 Aug 2018 15:50:29 +0200 Subject: [PATCH 29/33] [Server][Feature][needs-docs] Enhancing WMTS GetTile parameters check with tests --- src/server/services/wmts/qgswmtsutils.cpp | 18 +---- tests/src/python/test_qgsserver_wmts.py | 91 +++++++++++++++++++++++ 2 files changed, 94 insertions(+), 15 deletions(-) diff --git a/src/server/services/wmts/qgswmtsutils.cpp b/src/server/services/wmts/qgswmtsutils.cpp index a19ec05dfff7..f1e825322844 100644 --- a/src/server/services/wmts/qgswmtsutils.cpp +++ b/src/server/services/wmts/qgswmtsutils.cpp @@ -601,11 +601,7 @@ namespace QgsWmts //difining TileMatrix idx int tm_idx = params.tileMatrixAsInt(); //read TileMatrix - if ( tm_idx == -1 ) - { - throw QgsRequestNotWellFormedException( QStringLiteral( "TileMatrix is mandatory" ) ); - } - if ( tms.tileMatrixList.count() < tm_idx ) + if ( tm_idx < 0 || tms.tileMatrixList.count() < tm_idx ) { throw QgsRequestNotWellFormedException( QStringLiteral( "TileMatrix is unknown" ) ); } @@ -614,11 +610,7 @@ namespace QgsWmts //defining TileRow int tr = params.tileRowAsInt(); //read TileRow - if ( tr == -1 ) - { - throw QgsRequestNotWellFormedException( QStringLiteral( "TileRow is mandatory" ) ); - } - if ( tm.row <= tr ) + if ( tr < 0 || tm.row <= tr ) { throw QgsRequestNotWellFormedException( QStringLiteral( "TileRow is unknown" ) ); } @@ -626,11 +618,7 @@ namespace QgsWmts //defining TileCol int tc = params.tileColAsInt(); //read TileCol - if ( tc == -1 ) - { - throw QgsRequestNotWellFormedException( QStringLiteral( "TileCol is mandatory" ) ); - } - if ( tm.col <= tc ) + if ( tc < 0 || tm.col <= tc ) { throw QgsRequestNotWellFormedException( QStringLiteral( "TileCol is unknown" ) ); } diff --git a/tests/src/python/test_qgsserver_wmts.py b/tests/src/python/test_qgsserver_wmts.py index a640c9d18b67..648960809b6f 100644 --- a/tests/src/python/test_qgsserver_wmts.py +++ b/tests/src/python/test_qgsserver_wmts.py @@ -190,6 +190,97 @@ def test_wmts_gettile(self): r, h = self._result(self._execute_request(qs)) self._img_diff_error(r, h, "WMTS_GetTile_Hello_4326_0", 20000) + def test_wmts_gettile_invalid_parameters(self): + qs = "?" + "&".join(["%s=%s" % i for i in list({ + "MAP": urllib.parse.quote(self.projectGroupsPath), + "SERVICE": "WMTS", + "VERSION": "1.0.0", + "REQUEST": "GetTile", + "LAYER": "Hello", + "STYLE": "", + "TILEMATRIXSET": "EPSG:3857", + "TILEMATRIX": "0", + "TILEROW": "0", + "TILECOL": "FOO", + "FORMAT": "image/png" + }.items())]) + + r, h = self._result(self._execute_request(qs)) + err = b"TILECOL (\'FOO\') cannot be converted into int" in r + self.assertTrue(err) + + qs = "?" + "&".join(["%s=%s" % i for i in list({ + "MAP": urllib.parse.quote(self.projectGroupsPath), + "SERVICE": "WMTS", + "VERSION": "1.0.0", + "REQUEST": "GetTile", + "LAYER": "Hello", + "STYLE": "", + "TILEMATRIXSET": "EPSG:3857", + "TILEMATRIX": "0", + "TILEROW": "0", + "TILECOL": "1", + "FORMAT": "image/png" + }.items())]) + + r, h = self._result(self._execute_request(qs)) + err = b"TileCol is unknown" in r + self.assertTrue(err) + + qs = "?" + "&".join(["%s=%s" % i for i in list({ + "MAP": urllib.parse.quote(self.projectGroupsPath), + "SERVICE": "WMTS", + "VERSION": "1.0.0", + "REQUEST": "GetTile", + "LAYER": "Hello", + "STYLE": "", + "TILEMATRIXSET": "EPSG:3857", + "TILEMATRIX": "0", + "TILEROW": "0", + "TILECOL": "-1", + "FORMAT": "image/png" + }.items())]) + + r, h = self._result(self._execute_request(qs)) + err = b"TileCol is unknown" in r + self.assertTrue(err) + + qs = "?" + "&".join(["%s=%s" % i for i in list({ + "MAP": urllib.parse.quote(self.projectGroupsPath), + "SERVICE": "WMTS", + "VERSION": "1.0.0", + "REQUEST": "GetTile", + "LAYER": "dem", + "STYLE": "", + "TILEMATRIXSET": "EPSG:3857", + "TILEMATRIX": "0", + "TILEROW": "0", + "TILECOL": "0", + "FORMAT": "image/png" + }.items())]) + + r, h = self._result(self._execute_request(qs)) + err = b"Layer \'dem\' not found" in r + self.assertTrue(err) + + qs = "?" + "&".join(["%s=%s" % i for i in list({ + "MAP": urllib.parse.quote(self.projectGroupsPath), + "SERVICE": "WMTS", + "VERSION": "1.0.0", + "REQUEST": "GetTile", + "LAYER": "Hello", + "STYLE": "", + "TILEMATRIXSET": "EPSG:2154", + "TILEMATRIX": "0", + "TILEROW": "0", + "TILECOL": "0", + "FORMAT": "image/png" + }.items())]) + + r, h = self._result(self._execute_request(qs)) + err = b"TileMatrixSet is unknown" in r + self.assertTrue(err) + if __name__ == '__main__': unittest.main() From 3626cf11a0c441a323f19c8fd574e0649282857b Mon Sep 17 00:00:00 2001 From: rldhont Date: Fri, 24 Aug 2018 15:55:30 +0200 Subject: [PATCH 30/33] [Server][Feature][needs-docs] Testing that exceptions are not cached --- .../src/python/test_qgsserver_cachemanager.py | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/src/python/test_qgsserver_cachemanager.py b/tests/src/python/test_qgsserver_cachemanager.py index 2b89a8c768a3..478bee8da843 100644 --- a/tests/src/python/test_qgsserver_cachemanager.py +++ b/tests/src/python/test_qgsserver_cachemanager.py @@ -390,6 +390,31 @@ def test_gettile(self): filelist = [f for f in os.listdir(self._servercache._tile_cache_dir) if f.endswith(".png")] self.assertEqual(len(filelist), 0, 'All images in cache are not deleted ') + def test_gettile_invalid_parameters(self): + project = self._project_path + assert os.path.exists(project), "Project file not found: " + project + + qs = "?" + "&".join(["%s=%s" % i for i in list({ + "MAP": urllib.parse.quote(project), + "SERVICE": "WMTS", + "VERSION": "1.0.0", + "REQUEST": "GetTile", + "LAYER": "Country", + "STYLE": "", + "TILEMATRIXSET": "EPSG:3857", + "TILEMATRIX": "0", + "TILEROW": "0", + "TILECOL": "FOO", + "FORMAT": "image/png" + }.items())]) + + r, h = self._result(self._execute_request(qs)) + err = b"TILECOL (\'FOO\') cannot be converted into int" in r + self.assertTrue(err) + + filelist = [f for f in os.listdir(self._servercache._tile_cache_dir) if f.endswith(".png")] + self.assertEqual(len(filelist), 0, 'Exception has been cached ') + if __name__ == "__main__": unittest.main() From b82c30e2d6070d1cc7c7020a0dd18a5126c264e1 Mon Sep 17 00:00:00 2001 From: rldhont Date: Tue, 28 Aug 2018 14:17:10 +0200 Subject: [PATCH 31/33] [Server] Enhancing loop for --- src/server/services/wms/qgswmsrenderer.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/server/services/wms/qgswmsrenderer.cpp b/src/server/services/wms/qgswmsrenderer.cpp index e7e3e4c88836..8d6d2b1de511 100644 --- a/src/server/services/wms/qgswmsrenderer.cpp +++ b/src/server/services/wms/qgswmsrenderer.cpp @@ -332,7 +332,7 @@ namespace QgsWms { checkLayerReadPermissions( layer ); - for ( QgsWmsParametersLayer param : params ) + for ( const QgsWmsParametersLayer ¶m : params ) { if ( param.mNickname == layerNickname( *layer ) ) { @@ -681,7 +681,7 @@ namespace QgsWms { checkLayerReadPermissions( layer ); - for ( const QgsWmsParametersLayer param : params ) + for ( const QgsWmsParametersLayer ¶m : params ) { if ( param.mNickname == layerNickname( *layer ) ) { @@ -784,7 +784,7 @@ namespace QgsWms checkLayerReadPermissions( layer ); - for ( QgsWmsParametersLayer param : params ) + for ( const QgsWmsParametersLayer ¶m : params ) { if ( param.mNickname == layerNickname( *layer ) ) { @@ -955,7 +955,7 @@ namespace QgsWms { checkLayerReadPermissions( layer ); - for ( QgsWmsParametersLayer param : params ) + for ( const QgsWmsParametersLayer ¶m : params ) { if ( param.mNickname == layerNickname( *layer ) ) { @@ -1456,7 +1456,7 @@ namespace QgsWms mAccessControl->filterFeatures( layer, fReq ); QStringList attributes; - for ( QgsField field : layer->fields().toList() ) + for ( const QgsField &field : layer->fields().toList() ) { attributes.append( field.name() ); } @@ -2468,7 +2468,7 @@ namespace QgsWms // try to create highlight layer for each geometry QString crs = mWmsParameters.crs(); - for ( QgsWmsParametersHighlightLayer param : params ) + for ( const QgsWmsParametersHighlightLayer ¶m : params ) { // create sld document from symbology QDomDocument sldDoc; @@ -2674,7 +2674,7 @@ namespace QgsWms { QList layers; - for ( QgsWmsParametersLayer param : params ) + for ( const QgsWmsParametersLayer ¶m : params ) { QString nickname = param.mNickname; QString style = param.mStyle; From 6b8d04b081348c36515ed06469c5c1dcf9ccb81b Mon Sep 17 00:00:00 2001 From: rldhont Date: Tue, 28 Aug 2018 14:22:24 +0200 Subject: [PATCH 32/33] [Server] Various code cleaning for server cache manager and WMTS service --- .../qgsservercachemanager.sip.in | 2 -- .../qgsserverprojectutils.sip.in | 2 ++ src/server/qgsservercachefilter.h | 6 ++-- src/server/qgsservercachemanager.h | 3 -- src/server/qgsserverinterfaceimpl.h | 2 +- src/server/qgsserverprojectutils.h | 7 +++-- .../services/wmts/qgswmtsgetcapabilities.cpp | 14 +++++----- .../services/wmts/qgswmtsgetfeatureinfo.cpp | 8 +++--- .../services/wmts/qgswmtsparameters.cpp | 12 ++++---- src/server/services/wmts/qgswmtsparameters.h | 8 +++--- src/server/services/wmts/qgswmtsutils.cpp | 28 +++++++++---------- 11 files changed, 45 insertions(+), 47 deletions(-) diff --git a/python/server/auto_generated/qgsservercachemanager.sip.in b/python/server/auto_generated/qgsservercachemanager.sip.in index 1a7bbbd4a20f..c08238d23d29 100644 --- a/python/server/auto_generated/qgsservercachemanager.sip.in +++ b/python/server/auto_generated/qgsservercachemanager.sip.in @@ -11,8 +11,6 @@ - - class QgsServerCacheManager { %Docstring diff --git a/python/server/auto_generated/qgsserverprojectutils.sip.in b/python/server/auto_generated/qgsserverprojectutils.sip.in index dd0993521fae..8b8fe7d6b14b 100644 --- a/python/server/auto_generated/qgsserverprojectutils.sip.in +++ b/python/server/auto_generated/qgsserverprojectutils.sip.in @@ -422,6 +422,8 @@ Returns the WMTS service url defined in a QGIS project. :param project: the QGIS project :return: url if defined in project, an empty string otherwise. + +.. versionadded:: 3.4 %End }; diff --git a/src/server/qgsservercachefilter.h b/src/server/qgsservercachefilter.h index e6efd86edc96..65d17814654b 100644 --- a/src/server/qgsservercachefilter.h +++ b/src/server/qgsservercachefilter.h @@ -17,8 +17,8 @@ * * ***************************************************************************/ -#ifndef QGSSERVERCACHEPLUGIN_H -#define QGSSERVERCACHEPLUGIN_H +#ifndef QGSSERVERCACHEFILTER_H +#define QGSSERVERCACHEFILTER_H #include #include @@ -133,4 +133,4 @@ class SERVER_EXPORT QgsServerCacheFilter //! The registry definition typedef QMultiMap QgsServerCacheFilterMap; -#endif // QGSSERVERCACHEPLUGIN_H +#endif // QGSSERVERCACHEFILTER_H diff --git a/src/server/qgsservercachemanager.h b/src/server/qgsservercachemanager.h index 935117d6af22..5e1c315f7948 100644 --- a/src/server/qgsservercachemanager.h +++ b/src/server/qgsservercachemanager.h @@ -31,9 +31,6 @@ SIP_IF_MODULE( HAVE_SERVER_PYTHON_PLUGINS ) -class QgsServerCachePlugin; - - /** * \ingroup server * \class QgsServerCacheManager diff --git a/src/server/qgsserverinterfaceimpl.h b/src/server/qgsserverinterfaceimpl.h index a258171e8ede..1bad84c4abf1 100644 --- a/src/server/qgsserverinterfaceimpl.h +++ b/src/server/qgsserverinterfaceimpl.h @@ -62,7 +62,7 @@ class QgsServerInterfaceImpl : public QgsServerInterface /** - * Register a server cache filter + * Registers a server cache filter * \param serverCache the server cache to register * \param priority the priority used to order them * \since QGIS 3.4 diff --git a/src/server/qgsserverprojectutils.h b/src/server/qgsserverprojectutils.h index 72593edde7fb..7d51c5a66972 100644 --- a/src/server/qgsserverprojectutils.h +++ b/src/server/qgsserverprojectutils.h @@ -350,9 +350,10 @@ namespace QgsServerProjectUtils /** * Returns the WMTS service url defined in a QGIS project. - * \param project the QGIS project - * \returns url if defined in project, an empty string otherwise. - */ + * \param project the QGIS project + * \returns url if defined in project, an empty string otherwise. + * \since QGIS 3.4 + */ SERVER_EXPORT QString wmtsServiceUrl( const QgsProject &project ); }; diff --git a/src/server/services/wmts/qgswmtsgetcapabilities.cpp b/src/server/services/wmts/qgswmtsgetcapabilities.cpp index 04007aba2df8..a5a79248913e 100644 --- a/src/server/services/wmts/qgswmtsgetcapabilities.cpp +++ b/src/server/services/wmts/qgswmtsgetcapabilities.cpp @@ -145,7 +145,7 @@ namespace QgsWmts if ( !keywords.isEmpty() ) { QDomElement keywordsElem = doc.createElement( QStringLiteral( "ows:Keywords" ) ); - for ( const QString k : keywords ) + for ( const QString &k : keywords ) { QDomElement keywordElem = doc.createElement( QStringLiteral( "ows:Keyword" ) ); QDomText keywordText = doc.createTextNode( k ); @@ -333,7 +333,7 @@ namespace QgsWmts elem.appendChild( formatElem ); }; - for ( const layerDef wmtsLayer : wmtsLayers ) + for ( const layerDef &wmtsLayer : wmtsLayers ) { if ( wmtsLayer.id.isEmpty() ) continue; @@ -376,7 +376,7 @@ namespace QgsWmts layerElem.appendChild( wgs84BBoxElement ); // Other bounding boxes - for ( const tileMatrixSetDef tms : tmsList ) + for ( const tileMatrixSetDef &tms : tmsList ) { if ( tms.ref == QLatin1String( "EPSG:4326" ) ) continue; @@ -419,7 +419,7 @@ namespace QgsWmts layerStyleElem.appendChild( layerStyleTitleElem ); layerElem.appendChild( layerStyleElem ); - for ( const QString format : wmtsLayer.formats ) + for ( const QString &format : wmtsLayer.formats ) { QDomElement layerFormatElem = doc.createElement( QStringLiteral( "Format" ) ); QDomText layerFormatText = doc.createTextNode( format ); @@ -436,7 +436,7 @@ namespace QgsWmts appendInfoFormat( layerElem, QStringLiteral( "application/vnd.ogc.gml/3.1.1" ) ); } - for ( const tileMatrixSetDef tms : tmsList ) + for ( const tileMatrixSetDef &tms : tmsList ) { tileMatrixSetLinkDef tmsl = getLayerTileMatrixSetLink( wmtsLayer, tms, project ); if ( tmsl.ref.isEmpty() || tmsl.ref != tms.ref ) @@ -499,7 +499,7 @@ namespace QgsWmts void appendTileMatrixSetElements( QDomDocument &doc, QDomElement &contentsElement, QList< tileMatrixSetDef > tmsList ) { - for ( const tileMatrixSetDef tms : tmsList ) + for ( const tileMatrixSetDef &tms : tmsList ) { //wmts:TileMatrixSet QDomElement tmsElement = doc.createElement( QStringLiteral( "TileMatrixSet" )/*wmts:TileMatrixSet*/ ); @@ -516,7 +516,7 @@ namespace QgsWmts //wmts:TileMatrix int tmIdx = 0; - for ( const tileMatrixDef tm : tms.tileMatrixList ) + for ( const tileMatrixDef &tm : tms.tileMatrixList ) { QDomElement tmElement = doc.createElement( QStringLiteral( "TileMatrix" )/*wmts:TileMatrix*/ ); diff --git a/src/server/services/wmts/qgswmtsgetfeatureinfo.cpp b/src/server/services/wmts/qgswmtsgetfeatureinfo.cpp index 029fce2cfe4b..307291b49f42 100644 --- a/src/server/services/wmts/qgswmtsgetfeatureinfo.cpp +++ b/src/server/services/wmts/qgswmtsgetfeatureinfo.cpp @@ -34,10 +34,10 @@ namespace QgsWmts QUrlQuery query = translateWmtsParamToWmsQueryItem( QStringLiteral( "GetFeatureInfo" ), params, project, serverIface ); // GetFeatureInfo query items - query.addQueryItem( QgsWmsParameter::name( QgsWmsParameter::QUERY_LAYERS ), params.layer() ); - query.addQueryItem( QgsWmsParameter::name( QgsWmsParameter::I ), params.i() ); - query.addQueryItem( QgsWmsParameter::name( QgsWmsParameter::J ), params.j() ); - query.addQueryItem( QgsWmsParameter::name( QgsWmsParameter::INFO_FORMAT ), params.infoFormatAsString() ); + query.addQueryItem( QgsWmsParameterForWmts::name( QgsWmsParameterForWmts::QUERY_LAYERS ), params.layer() ); + query.addQueryItem( QgsWmsParameterForWmts::name( QgsWmsParameterForWmts::I ), params.i() ); + query.addQueryItem( QgsWmsParameterForWmts::name( QgsWmsParameterForWmts::J ), params.j() ); + query.addQueryItem( QgsWmsParameterForWmts::name( QgsWmsParameterForWmts::INFO_FORMAT ), params.infoFormatAsString() ); QgsServerParameters wmsParams( query ); QgsServerRequest wmsRequest( "?" + query.query( QUrl::FullyDecoded ) ); diff --git a/src/server/services/wmts/qgswmtsparameters.cpp b/src/server/services/wmts/qgswmtsparameters.cpp index c8f6588a9a92..ddaa05f75b04 100644 --- a/src/server/services/wmts/qgswmtsparameters.cpp +++ b/src/server/services/wmts/qgswmtsparameters.cpp @@ -22,18 +22,18 @@ namespace QgsWmts { // - // QgsWmsParameter + // QgsWmsParameterForWmts // - QString QgsWmsParameter::name( const QgsWmsParameter::Name name ) + QString QgsWmsParameterForWmts::name( const QgsWmsParameterForWmts::Name name ) { - const QMetaEnum metaEnum( QMetaEnum::fromType() ); + const QMetaEnum metaEnum( QMetaEnum::fromType() ); return metaEnum.valueToKey( name ); } - QgsWmsParameter::Name QgsWmsParameter::name( const QString &name ) + QgsWmsParameterForWmts::Name QgsWmsParameterForWmts::name( const QString &name ) { - const QMetaEnum metaEnum( QMetaEnum::fromType() ); - return ( QgsWmsParameter::Name ) metaEnum.keyToValue( name.toUpper().toStdString().c_str() ); + const QMetaEnum metaEnum( QMetaEnum::fromType() ); + return ( QgsWmsParameterForWmts::Name ) metaEnum.keyToValue( name.toUpper().toStdString().c_str() ); } // diff --git a/src/server/services/wmts/qgswmtsparameters.h b/src/server/services/wmts/qgswmtsparameters.h index e1782d0fd1ca..412bc41adc25 100644 --- a/src/server/services/wmts/qgswmtsparameters.h +++ b/src/server/services/wmts/qgswmtsparameters.h @@ -32,11 +32,11 @@ namespace QgsWmts /** * \ingroup server - * \class QgsWmts::QgsWmsParameter + * \class QgsWmts::QgsWmsParameterForWmts * \brief WMS parameter used by WMTS service. * \since QGIS 3.4 */ - class QgsWmsParameter : public QgsServerParameterDefinition + class QgsWmsParameterForWmts : public QgsServerParameterDefinition { Q_GADGET @@ -64,13 +64,13 @@ namespace QgsWmts /** * Converts a parameter's name into its string representation. */ - static QString name( const QgsWmsParameter::Name ); + static QString name( const QgsWmsParameterForWmts::Name ); /** * Converts a string into a parameter's name (UNKNOWN in case of an * invalid string). */ - static QgsWmsParameter::Name name( const QString &name ); + static QgsWmsParameterForWmts::Name name( const QString &name ); }; /** diff --git a/src/server/services/wmts/qgswmtsutils.cpp b/src/server/services/wmts/qgswmtsutils.cpp index f1e825322844..b6bdb3ed08ef 100644 --- a/src/server/services/wmts/qgswmtsutils.cpp +++ b/src/server/services/wmts/qgswmtsutils.cpp @@ -309,7 +309,7 @@ namespace QgsWmts QStringList wmtsPngGroupNameList = project->readListEntry( QStringLiteral( "WMTSPngLayers" ), QStringLiteral( "Group" ) ); QStringList wmtsJpegGroupNameList = project->readListEntry( QStringLiteral( "WMTSJpegLayers" ), QStringLiteral( "Group" ) ); - for ( const QString gName : wmtsGroupNameList ) + for ( const QString &gName : wmtsGroupNameList ) { QgsLayerTreeGroup *treeGroup = treeRoot->findGroup( gName ); if ( !treeGroup ) @@ -385,7 +385,7 @@ namespace QgsWmts QStringList wmtsPngLayerIdList = project->readListEntry( QStringLiteral( "WMTSPngLayers" ), QStringLiteral( "Layer" ) ); QStringList wmtsJpegLayerIdList = project->readListEntry( QStringLiteral( "WMTSJpegLayers" ), QStringLiteral( "Layer" ) ); - for ( const QString lId : wmtsLayerIdList ) + for ( const QString &lId : wmtsLayerIdList ) { QgsMapLayer *l = project->mapLayer( lId ); if ( !l ) @@ -462,7 +462,7 @@ namespace QgsWmts rect = rect.intersect( tms.extent ); int tmIdx = -1; - for ( const tileMatrixDef tm : tms.tileMatrixList ) + for ( const tileMatrixDef &tm : tms.tileMatrixList ) { ++tmIdx; @@ -521,7 +521,7 @@ namespace QgsWmts if ( !wmtsGroupNameList.isEmpty() ) { QgsLayerTreeGroup *treeRoot = project->layerTreeRoot(); - for ( QString gName : wmtsGroupNameList ) + for ( const QString &gName : wmtsGroupNameList ) { QgsLayerTreeGroup *treeGroup = treeRoot->findGroup( gName ); if ( !treeGroup ) @@ -541,7 +541,7 @@ namespace QgsWmts #ifdef HAVE_SERVER_PYTHON_PLUGINS QgsAccessControl *accessControl = serverIface->accessControls(); #endif - for ( QString lId : wmtsLayerIdList ) + for ( const QString &lId : wmtsLayerIdList ) { QgsMapLayer *l = project->mapLayer( lId ); if ( !l ) @@ -654,18 +654,18 @@ namespace QgsWmts query.addQueryItem( QgsServerParameter::name( QgsServerParameter::SERVICE ), QStringLiteral( "WMS" ) ); query.addQueryItem( QgsServerParameter::name( QgsServerParameter::VERSION_SERVICE ), QStringLiteral( "1.3.0" ) ); query.addQueryItem( QgsServerParameter::name( QgsServerParameter::REQUEST ), request ); - query.addQueryItem( QgsWmsParameter::name( QgsWmsParameter::LAYERS ), layer ); - query.addQueryItem( QgsWmsParameter::name( QgsWmsParameter::STYLES ), QString() ); - query.addQueryItem( QgsWmsParameter::name( QgsWmsParameter::CRS ), tms.ref ); - query.addQueryItem( QgsWmsParameter::name( QgsWmsParameter::BBOX ), bbox ); - query.addQueryItem( QgsWmsParameter::name( QgsWmsParameter::WIDTH ), QStringLiteral( "256" ) ); - query.addQueryItem( QgsWmsParameter::name( QgsWmsParameter::HEIGHT ), QStringLiteral( "256" ) ); - query.addQueryItem( QgsWmsParameter::name( QgsWmsParameter::FORMAT ), format ); + query.addQueryItem( QgsWmsParameterForWmts::name( QgsWmsParameterForWmts::LAYERS ), layer ); + query.addQueryItem( QgsWmsParameterForWmts::name( QgsWmsParameterForWmts::STYLES ), QString() ); + query.addQueryItem( QgsWmsParameterForWmts::name( QgsWmsParameterForWmts::CRS ), tms.ref ); + query.addQueryItem( QgsWmsParameterForWmts::name( QgsWmsParameterForWmts::BBOX ), bbox ); + query.addQueryItem( QgsWmsParameterForWmts::name( QgsWmsParameterForWmts::WIDTH ), QStringLiteral( "256" ) ); + query.addQueryItem( QgsWmsParameterForWmts::name( QgsWmsParameterForWmts::HEIGHT ), QStringLiteral( "256" ) ); + query.addQueryItem( QgsWmsParameterForWmts::name( QgsWmsParameterForWmts::FORMAT ), format ); if ( params.format() == QgsWmtsParameters::Format::PNG ) { - query.addQueryItem( QgsWmsParameter::name( QgsWmsParameter::TRANSPARENT ), QStringLiteral( "true" ) ); + query.addQueryItem( QgsWmsParameterForWmts::name( QgsWmsParameterForWmts::TRANSPARENT ), QStringLiteral( "true" ) ); } - query.addQueryItem( QgsWmsParameter::name( QgsWmsParameter::DPI ), QStringLiteral( "96" ) ); + query.addQueryItem( QgsWmsParameterForWmts::name( QgsWmsParameterForWmts::DPI ), QStringLiteral( "96" ) ); return query; } From b3f9898ce7badf2feadb356d699af8aa0ac45d24 Mon Sep 17 00:00:00 2001 From: rldhont Date: Tue, 28 Aug 2018 14:26:18 +0200 Subject: [PATCH 33/33] [Server] Fix weird docs indetation --- src/server/qgsserverprojectutils.h | 266 ++++++++++++++--------------- 1 file changed, 133 insertions(+), 133 deletions(-) diff --git a/src/server/qgsserverprojectutils.h b/src/server/qgsserverprojectutils.h index 7d51c5a66972..21a320b26be4 100644 --- a/src/server/qgsserverprojectutils.h +++ b/src/server/qgsserverprojectutils.h @@ -40,312 +40,312 @@ namespace QgsServerProjectUtils /** * Returns if owsService capabilities are enabled. - * \param project the QGIS project - * \returns if owsService capabilities are enabled. - */ + * \param project the QGIS project + * \returns if owsService capabilities are enabled. + */ SERVER_EXPORT bool owsServiceCapabilities( const QgsProject &project ); /** * Returns the owsService title defined in project. - * \param project the QGIS project - * \returns the owsService title if defined in project. - */ + * \param project the QGIS project + * \returns the owsService title if defined in project. + */ SERVER_EXPORT QString owsServiceTitle( const QgsProject &project ); /** * Returns the owsService abstract defined in project. - * \param project the QGIS project - * \returns the owsService abstract if defined in project. - */ + * \param project the QGIS project + * \returns the owsService abstract if defined in project. + */ SERVER_EXPORT QString owsServiceAbstract( const QgsProject &project ); /** * Returns the owsService keywords defined in project. - * \param project the QGIS project - * \returns the owsService keywords if defined in project. - */ + * \param project the QGIS project + * \returns the owsService keywords if defined in project. + */ SERVER_EXPORT QStringList owsServiceKeywords( const QgsProject &project ); /** * Returns the owsService online resource defined in project. - * \param project the QGIS project - * \returns the owsService online resource if defined in project. - */ + * \param project the QGIS project + * \returns the owsService online resource if defined in project. + */ SERVER_EXPORT QString owsServiceOnlineResource( const QgsProject &project ); /** * Returns the owsService contact organization defined in project. - * \param project the QGIS project - * \returns the owsService contact organization if defined in project. - */ + * \param project the QGIS project + * \returns the owsService contact organization if defined in project. + */ SERVER_EXPORT QString owsServiceContactOrganization( const QgsProject &project ); /** * Returns the owsService contact position defined in project. - * \param project the QGIS project - * \returns the owsService contact position if defined in project. - */ + * \param project the QGIS project + * \returns the owsService contact position if defined in project. + */ SERVER_EXPORT QString owsServiceContactPosition( const QgsProject &project ); /** * Returns the owsService contact person defined in project. - * \param project the QGIS project - * \returns the owsService contact person if defined in project. - */ + * \param project the QGIS project + * \returns the owsService contact person if defined in project. + */ SERVER_EXPORT QString owsServiceContactPerson( const QgsProject &project ); /** * Returns the owsService contact mail defined in project. - * \param project the QGIS project - * \returns the owsService contact mail if defined in project. - */ + * \param project the QGIS project + * \returns the owsService contact mail if defined in project. + */ SERVER_EXPORT QString owsServiceContactMail( const QgsProject &project ); /** * Returns the owsService contact phone defined in project. - * \param project the QGIS project - * \returns the owsService contact phone if defined in project. - */ + * \param project the QGIS project + * \returns the owsService contact phone if defined in project. + */ SERVER_EXPORT QString owsServiceContactPhone( const QgsProject &project ); /** * Returns the owsService fees defined in project. - * \param project the QGIS project - * \returns the owsService fees if defined in project. - */ + * \param project the QGIS project + * \returns the owsService fees if defined in project. + */ SERVER_EXPORT QString owsServiceFees( const QgsProject &project ); /** * Returns the owsService access constraints defined in project. - * \param project the QGIS project - * \returns the owsService access constraints if defined in project. - */ + * \param project the QGIS project + * \returns the owsService access constraints if defined in project. + */ SERVER_EXPORT QString owsServiceAccessConstraints( const QgsProject &project ); /** * Returns the maximum width for WMS images defined in a QGIS project. - * \param project the QGIS project - * \returns width if defined in project, -1 otherwise. - */ + * \param project the QGIS project + * \returns width if defined in project, -1 otherwise. + */ SERVER_EXPORT int wmsMaxWidth( const QgsProject &project ); /** * Returns the maximum height for WMS images defined in a QGIS project. - * \param project the QGIS project - * \returns height if defined in project, -1 otherwise. - */ + * \param project the QGIS project + * \returns height if defined in project, -1 otherwise. + */ SERVER_EXPORT int wmsMaxHeight( const QgsProject &project ); /** * Returns the quality for WMS images defined in a QGIS project. - * \param project the QGIS project - * \returns quality if defined in project, -1 otherwise. - */ + * \param project the QGIS project + * \returns quality if defined in project, -1 otherwise. + */ SERVER_EXPORT int wmsImageQuality( const QgsProject &project ); /** * Returns if layer ids are used as name in WMS. - * \param project the QGIS project - * \returns if layer ids are used as name. - */ + * \param project the QGIS project + * \returns if layer ids are used as name. + */ SERVER_EXPORT bool wmsUseLayerIds( const QgsProject &project ); /** * Returns if the info format is SIA20145. - * \param project the QGIS project - * \returns if the info format is SIA20145. - */ + * \param project the QGIS project + * \returns if the info format is SIA20145. + */ SERVER_EXPORT bool wmsInfoFormatSia2045( const QgsProject &project ); /** * Returns if the geometry is displayed as Well Known Text in GetFeatureInfo request. - * \param project the QGIS project - * \returns if the geometry is displayed as Well Known Text in GetFeatureInfo request. - */ + * \param project the QGIS project + * \returns if the geometry is displayed as Well Known Text in GetFeatureInfo request. + */ SERVER_EXPORT bool wmsFeatureInfoAddWktGeometry( const QgsProject &project ); /** * Returns if the geometry has to be segmentize in GetFeatureInfo request. - * \param project the QGIS project - * \returns if the geometry has to be segmentize in GetFeatureInfo request. - */ + * \param project the QGIS project + * \returns if the geometry has to be segmentize in GetFeatureInfo request. + */ SERVER_EXPORT bool wmsFeatureInfoSegmentizeWktGeometry( const QgsProject &project ); /** * Returns the geometry precision for GetFeatureInfo request. - * \param project the QGIS project - * \returns the geometry precision for GetFeatureInfo request. - */ + * \param project the QGIS project + * \returns the geometry precision for GetFeatureInfo request. + */ SERVER_EXPORT int wmsFeatureInfoPrecision( const QgsProject &project ); /** * Returns the document element name for XML GetFeatureInfo request. - * \param project the QGIS project - * \returns the document element name for XML GetFeatureInfo request. - */ + * \param project the QGIS project + * \returns the document element name for XML GetFeatureInfo request. + */ SERVER_EXPORT QString wmsFeatureInfoDocumentElement( const QgsProject &project ); /** * Returns the document element namespace for XML GetFeatureInfo request. - * \param project the QGIS project - * \returns the document element namespace for XML GetFeatureInfo request. - */ + * \param project the QGIS project + * \returns the document element namespace for XML GetFeatureInfo request. + */ SERVER_EXPORT QString wmsFeatureInfoDocumentElementNs( const QgsProject &project ); /** * Returns the schema URL for XML GetFeatureInfo request. - * \param project the QGIS project - * \returns the schema URL for XML GetFeatureInfo request. - */ + * \param project the QGIS project + * \returns the schema URL for XML GetFeatureInfo request. + */ SERVER_EXPORT QString wmsFeatureInfoSchema( const QgsProject &project ); /** * Returns the mapping between layer name and wms layer name for GetFeatureInfo request. - * \param project the QGIS project - * \returns the mapping between layer name and wms layer name for GetFeatureInfo request. - */ + * \param project the QGIS project + * \returns the mapping between layer name and wms layer name for GetFeatureInfo request. + */ SERVER_EXPORT QHash wmsFeatureInfoLayerAliasMap( const QgsProject &project ); /** * Returns if Inspire is activated. - * \param project the QGIS project - * \returns if Inspire is activated. - */ + * \param project the QGIS project + * \returns if Inspire is activated. + */ SERVER_EXPORT bool wmsInspireActivate( const QgsProject &project ); /** * Returns the Inspire language. - * \param project the QGIS project - * \returns the Inspire language if defined in project. - */ + * \param project the QGIS project + * \returns the Inspire language if defined in project. + */ SERVER_EXPORT QString wmsInspireLanguage( const QgsProject &project ); /** * Returns the Inspire metadata URL. - * \param project the QGIS project - * \returns the Inspire metadata URL if defined in project. - */ + * \param project the QGIS project + * \returns the Inspire metadata URL if defined in project. + */ SERVER_EXPORT QString wmsInspireMetadataUrl( const QgsProject &project ); /** * Returns the Inspire metadata URL type. - * \param project the QGIS project - * \returns the Inspire metadata URL type if defined in project. - */ + * \param project the QGIS project + * \returns the Inspire metadata URL type if defined in project. + */ SERVER_EXPORT QString wmsInspireMetadataUrlType( const QgsProject &project ); /** * Returns the Inspire temporal reference. - * \param project the QGIS project - * \returns the Inspire temporal reference if defined in project. - */ + * \param project the QGIS project + * \returns the Inspire temporal reference if defined in project. + */ SERVER_EXPORT QString wmsInspireTemporalReference( const QgsProject &project ); /** * Returns the Inspire metadata date. - * \param project the QGIS project - * \returns the Inspire metadata date if defined in project. - */ + * \param project the QGIS project + * \returns the Inspire metadata date if defined in project. + */ SERVER_EXPORT QString wmsInspireMetadataDate( const QgsProject &project ); /** * Returns the restricted composer list. - * \param project the QGIS project - * \returns the restricted composer list if defined in project. - */ + * \param project the QGIS project + * \returns the restricted composer list if defined in project. + */ SERVER_EXPORT QStringList wmsRestrictedComposers( const QgsProject &project ); /** * Returns the WMS service url defined in a QGIS project. - * \param project the QGIS project - * \returns url if defined in project, an empty string otherwise. - */ + * \param project the QGIS project + * \returns url if defined in project, an empty string otherwise. + */ SERVER_EXPORT QString wmsServiceUrl( const QgsProject &project ); /** * Returns the WMS root layer name defined in a QGIS project. - * \param project the QGIS project - * \returns root layer name if defined in project, an empty string otherwise. - */ + * \param project the QGIS project + * \returns root layer name if defined in project, an empty string otherwise. + */ SERVER_EXPORT QString wmsRootName( const QgsProject &project ); /** * Returns the restricted layer name list. - * \param project the QGIS project - * \returns the restricted layer name list if defined in project. - */ + * \param project the QGIS project + * \returns the restricted layer name list if defined in project. + */ SERVER_EXPORT QStringList wmsRestrictedLayers( const QgsProject &project ); /** * Returns the WMS output CRS list. - * \param project the QGIS project - * \returns the WMS output CRS list. - */ + * \param project the QGIS project + * \returns the WMS output CRS list. + */ SERVER_EXPORT QStringList wmsOutputCrsList( const QgsProject &project ); /** * Returns the WMS Extent restriction. - * \param project the QGIS project - * \returns the WMS Extent restriction. - */ + * \param project the QGIS project + * \returns the WMS Extent restriction. + */ SERVER_EXPORT QgsRectangle wmsExtent( const QgsProject &project ); /** * Returns the WFS service url defined in a QGIS project. - * \param project the QGIS project - * \returns url if defined in project, an empty string otherwise. - */ + * \param project the QGIS project + * \returns url if defined in project, an empty string otherwise. + */ SERVER_EXPORT QString wfsServiceUrl( const QgsProject &project ); /** * Returns the Layer ids list defined in a QGIS project as published in WFS. - * \param project the QGIS project - * \return the Layer ids list. - */ + * \param project the QGIS project + * \return the Layer ids list. + */ SERVER_EXPORT QStringList wfsLayerIds( const QgsProject &project ); /** * Returns the Layer precision defined in a QGIS project for the WFS GetFeature. - * \param project the QGIS project - * \param layerId the layer id in the project - * \return the layer precision for WFS GetFeature. - */ + * \param project the QGIS project + * \param layerId the layer id in the project + * \return the layer precision for WFS GetFeature. + */ SERVER_EXPORT int wfsLayerPrecision( const QgsProject &project, const QString &layerId ); /** * Returns the Layer ids list defined in a QGIS project as published as WFS-T with update capabilities. - * \param project the QGIS project - * \return the Layer ids list. - */ + * \param project the QGIS project + * \return the Layer ids list. + */ SERVER_EXPORT QStringList wfstUpdateLayerIds( const QgsProject &project ); /** * Returns the Layer ids list defined in a QGIS project as published as WFS-T with insert capabilities. - * \param project the QGIS project - * \return the Layer ids list. - */ + * \param project the QGIS project + * \return the Layer ids list. + */ SERVER_EXPORT QStringList wfstInsertLayerIds( const QgsProject &project ); /** * Returns the Layer ids list defined in a QGIS project as published as WFS-T with delete capabilities. - * \param project the QGIS project - * \return the Layer ids list. - */ + * \param project the QGIS project + * \return the Layer ids list. + */ SERVER_EXPORT QStringList wfstDeleteLayerIds( const QgsProject &project ); /** * Returns the WCS service url defined in a QGIS project. - * \param project the QGIS project - * \returns url if defined in project, an empty string otherwise. - */ + * \param project the QGIS project + * \returns url if defined in project, an empty string otherwise. + */ SERVER_EXPORT QString wcsServiceUrl( const QgsProject &project ); /** * Returns the Layer ids list defined in a QGIS project as published in WCS. - * \param project the QGIS project - * \returns the Layer ids list. - */ + * \param project the QGIS project + * \returns the Layer ids list. + */ SERVER_EXPORT QStringList wcsLayerIds( const QgsProject &project ); /**