mirror of
https://github.com/qgis/QGIS.git
synced 2025-03-28 00:04:04 -04:00
4289 lines
152 KiB
C++
4289 lines
152 KiB
C++
/***************************************************************************
|
|
qgswmsprovider.cpp - QGIS Data provider for
|
|
OGC Web Map Service layers
|
|
-------------------
|
|
begin : 17 Mar, 2005
|
|
copyright : (C) 2005 by Brendan Morley
|
|
email : morb at ozemail dot com dot au
|
|
|
|
wms-c/wmts support : Jürgen E. Fischer < jef at norbit dot de >, norBIT GmbH
|
|
|
|
tile retry support : Luigi Pirelli < luipir at gmail dot com >
|
|
(funded by Regione Toscana-SITA)
|
|
|
|
contextual wms legend: Sandro Santilli < strk at keybit dot net >
|
|
|
|
***************************************************************************/
|
|
|
|
/***************************************************************************
|
|
* *
|
|
* 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 "qgslogger.h"
|
|
#include "qgswmsprovider.h"
|
|
#include "qgswmsconnection.h"
|
|
#include "qgscoordinatetransform.h"
|
|
#include "qgswmsdataitems.h"
|
|
#include "qgsdatasourceuri.h"
|
|
#include "qgsfeaturestore.h"
|
|
#include "qgsgeometry.h"
|
|
#include "qgsrasteridentifyresult.h"
|
|
#include "qgsrasterlayer.h"
|
|
#include "qgsrectangle.h"
|
|
#include "qgscoordinatereferencesystem.h"
|
|
#include "qgsmapsettings.h"
|
|
#include "qgsmessageoutput.h"
|
|
#include "qgsmessagelog.h"
|
|
#include "qgsnetworkaccessmanager.h"
|
|
#include "qgsnetworkreplyparser.h"
|
|
#include "qgstilecache.h"
|
|
#include "qgsgml.h"
|
|
#include "qgsgmlschema.h"
|
|
#include "qgswmscapabilities.h"
|
|
#include "qgsexception.h"
|
|
#include "qgssettings.h"
|
|
#include "qgsogrutils.h"
|
|
|
|
#include <QNetworkRequest>
|
|
#include <QNetworkReply>
|
|
#include <QNetworkProxy>
|
|
#include <QUrl>
|
|
#include <QImage>
|
|
#include <QImageReader>
|
|
#include <QPainter>
|
|
#include <QEventLoop>
|
|
#include <QTextCodec>
|
|
#include <QThread>
|
|
#include <QNetworkDiskCache>
|
|
#include <QTimer>
|
|
#include <QStringBuilder>
|
|
|
|
#include <ogr_api.h>
|
|
|
|
#ifdef QGISDEBUG
|
|
#include <QFile>
|
|
#include <QDir>
|
|
#endif
|
|
|
|
#define ERR(message) QGS_ERROR_MESSAGE(message,"WMS provider")
|
|
#define QGS_ERROR(message) QgsError(message,"WMS provider")
|
|
|
|
QString QgsWmsProvider::WMS_KEY = QStringLiteral( "wms" );
|
|
QString QgsWmsProvider::WMS_DESCRIPTION = QStringLiteral( "OGC Web Map Service version 1.3 data provider" );
|
|
|
|
static QString DEFAULT_LATLON_CRS = QStringLiteral( "CRS:84" );
|
|
|
|
QMap<QString, QgsWmsStatistics::Stat> QgsWmsStatistics::sData;
|
|
|
|
//! a helper class for ordering tile requests according to the distance from view center
|
|
struct LessThanTileRequest
|
|
{
|
|
QgsPointXY center;
|
|
bool operator()( const QgsWmsProvider::TileRequest &req1, const QgsWmsProvider::TileRequest &req2 )
|
|
{
|
|
QPointF p1 = req1.rect.center();
|
|
QPointF p2 = req2.rect.center();
|
|
// using chessboard distance (loading order more natural than euclidean/manhattan distance)
|
|
double d1 = std::max( std::fabs( center.x() - p1.x() ), std::fabs( center.y() - p1.y() ) );
|
|
double d2 = std::max( std::fabs( center.x() - p2.x() ), std::fabs( center.y() - p2.y() ) );
|
|
return d1 < d2;
|
|
}
|
|
};
|
|
|
|
|
|
QgsWmsProvider::QgsWmsProvider( QString const &uri, const ProviderOptions &options, const QgsWmsCapabilities *capabilities )
|
|
: QgsRasterDataProvider( uri, options )
|
|
, mHttpGetLegendGraphicResponse( nullptr )
|
|
, mImageCrs( DEFAULT_LATLON_CRS )
|
|
{
|
|
QgsDebugMsgLevel( "constructing with uri '" + uri + "'.", 4 );
|
|
|
|
mSupportedGetFeatureFormats = QStringList() << QStringLiteral( "text/html" ) << QStringLiteral( "text/plain" ) << QStringLiteral( "text/xml" ) << QStringLiteral( "application/vnd.ogc.gml" ) << QStringLiteral( "application/json" );
|
|
|
|
mValid = false;
|
|
|
|
// URL may contain username/password information for a WMS
|
|
// requiring authentication. In this case the URL is prefixed
|
|
// with username=user,password=pass,url=http://xxx.xxx.xx/yyy...
|
|
|
|
if ( !mSettings.parseUri( uri ) )
|
|
{
|
|
appendError( ERR( tr( "Cannot parse URI" ) ) );
|
|
return;
|
|
}
|
|
|
|
if ( !addLayers() )
|
|
return;
|
|
|
|
if ( mSettings.mXyz )
|
|
{
|
|
// we are working with XYZ tiles
|
|
// no need to get capabilities, the whole definition is in URI
|
|
// so we just generate a dummy WMTS definition
|
|
setupXyzCapabilities( uri );
|
|
}
|
|
else
|
|
{
|
|
// we are working with WMS / WMTS server
|
|
|
|
// if there are already parsed capabilities, use them!
|
|
if ( capabilities )
|
|
mCaps = *capabilities;
|
|
|
|
// Make sure we have capabilities - other functions here may need them
|
|
if ( !retrieveServerCapabilities() )
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
// setImageCrs is using mTiled !!!
|
|
if ( !setImageCrs( mSettings.mCrsId ) )
|
|
{
|
|
appendError( ERR( tr( "Cannot set CRS" ) ) );
|
|
return;
|
|
}
|
|
mCrs = QgsCoordinateReferenceSystem::fromOgcWmsCrs( mSettings.mCrsId );
|
|
|
|
if ( !calculateExtent() || mLayerExtent.isEmpty() )
|
|
{
|
|
appendError( ERR( tr( "Cannot calculate extent" ) ) );
|
|
return;
|
|
}
|
|
|
|
// URL can be in 3 forms:
|
|
// 1) http://xxx.xxx.xx/yyy/yyy
|
|
// 2) http://xxx.xxx.xx/yyy/yyy?
|
|
// 3) http://xxx.xxx.xx/yyy/yyy?zzz=www
|
|
|
|
mValid = true;
|
|
QgsDebugMsgLevel( QStringLiteral( "exiting constructor." ), 4 );
|
|
}
|
|
|
|
|
|
QString QgsWmsProvider::prepareUri( QString uri )
|
|
{
|
|
// some services provide a percent/url encoded (legend) uri string, always decode here
|
|
uri = QUrl::fromPercentEncoding( uri.toUtf8() );
|
|
|
|
if ( uri.contains( QLatin1String( "SERVICE=WMTS" ) ) || uri.contains( QLatin1String( "/WMTSCapabilities.xml" ) ) )
|
|
{
|
|
return uri;
|
|
}
|
|
|
|
if ( !uri.contains( QLatin1String( "?" ) ) )
|
|
{
|
|
uri.append( '?' );
|
|
}
|
|
else if ( uri.right( 1 ) != QLatin1String( "?" ) && uri.right( 1 ) != QLatin1String( "&" ) )
|
|
{
|
|
uri.append( '&' );
|
|
}
|
|
|
|
return uri;
|
|
}
|
|
|
|
QgsWmsProvider::~QgsWmsProvider()
|
|
{
|
|
QgsDebugMsgLevel( QStringLiteral( "deconstructing." ), 4 );
|
|
}
|
|
|
|
QgsWmsProvider *QgsWmsProvider::clone() const
|
|
{
|
|
QgsDataProvider::ProviderOptions options;
|
|
QgsWmsProvider *provider = new QgsWmsProvider( dataSourceUri(), options, mCaps.isValid() ? &mCaps : nullptr );
|
|
provider->copyBaseSettings( *this );
|
|
return provider;
|
|
}
|
|
|
|
|
|
QString QgsWmsProvider::getMapUrl() const
|
|
{
|
|
return mCaps.mCapabilities.capability.request.getMap.dcpType.isEmpty()
|
|
? mSettings.mBaseUrl
|
|
: prepareUri( mCaps.mCapabilities.capability.request.getMap.dcpType.front().http.get.onlineResource.xlinkHref );
|
|
}
|
|
|
|
|
|
QString QgsWmsProvider::getFeatureInfoUrl() const
|
|
{
|
|
return mCaps.mCapabilities.capability.request.getFeatureInfo.dcpType.isEmpty()
|
|
? mSettings.mBaseUrl
|
|
: prepareUri( mCaps.mCapabilities.capability.request.getFeatureInfo.dcpType.front().http.get.onlineResource.xlinkHref );
|
|
}
|
|
|
|
QString QgsWmsProvider::getTileUrl() const
|
|
{
|
|
if ( mCaps.mCapabilities.capability.request.getTile.dcpType.isEmpty() ||
|
|
( !mCaps.mCapabilities.capability.request.getTile.allowedEncodings.isEmpty() &&
|
|
!mCaps.mCapabilities.capability.request.getTile.allowedEncodings.contains( QStringLiteral( "KVP" ) ) ) )
|
|
{
|
|
return QString();
|
|
}
|
|
else
|
|
{
|
|
return prepareUri( mCaps.mCapabilities.capability.request.getTile.dcpType.front().http.get.onlineResource.xlinkHref );
|
|
}
|
|
}
|
|
|
|
static bool isValidLegend( const QgsWmsLegendUrlProperty &l )
|
|
{
|
|
return l.format.startsWith( QLatin1String( "image/" ) );
|
|
}
|
|
|
|
/**
|
|
* Picks a usable legend URL for a given style.
|
|
*/
|
|
static QString pickLegend( const QgsWmsStyleProperty &s )
|
|
{
|
|
QString url;
|
|
for ( int k = 0; k < s.legendUrl.size() && url.isEmpty(); k++ )
|
|
{
|
|
const QgsWmsLegendUrlProperty &l = s.legendUrl[k];
|
|
if ( isValidLegend( l ) )
|
|
{
|
|
url = l.onlineResource.xlinkHref;
|
|
}
|
|
}
|
|
return url;
|
|
}
|
|
|
|
static const QgsWmsStyleProperty *searchStyle( const QVector<QgsWmsStyleProperty> &styles, const QString &name )
|
|
{
|
|
const auto constStyles = styles;
|
|
for ( const QgsWmsStyleProperty &s : constStyles )
|
|
if ( s.name == name )
|
|
return &s;
|
|
return nullptr;
|
|
}
|
|
|
|
QString QgsWmsProvider::getLegendGraphicUrl() const
|
|
{
|
|
QString url;
|
|
|
|
for ( int i = 0; i < mCaps.mLayersSupported.size() && url.isEmpty(); i++ )
|
|
{
|
|
const QgsWmsLayerProperty &l = mCaps.mLayersSupported[i];
|
|
|
|
if ( l.name == mSettings.mActiveSubLayers[0] )
|
|
{
|
|
if ( !mSettings.mActiveSubStyles[0].isEmpty() && mSettings.mActiveSubStyles[0] != QLatin1String( "default" ) )
|
|
{
|
|
const QgsWmsStyleProperty *s = searchStyle( l.style, mSettings.mActiveSubStyles[0] );
|
|
if ( s )
|
|
url = pickLegend( *s );
|
|
}
|
|
else
|
|
{
|
|
// QGIS wants the default style, but GetCapabilities doesn't give us a
|
|
// way to know what is the default style. So we look for the onlineResource
|
|
// only if there is a single style available or if there is a style called "default".
|
|
if ( l.style.size() == 1 )
|
|
{
|
|
url = pickLegend( l.style[0] );
|
|
}
|
|
else
|
|
{
|
|
const QgsWmsStyleProperty *s = searchStyle( l.style, QStringLiteral( "default" ) );
|
|
if ( s )
|
|
url = pickLegend( *s );
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ( url.isEmpty() && !mCaps.mCapabilities.capability.request.getLegendGraphic.dcpType.isEmpty() )
|
|
{
|
|
url = mCaps.mCapabilities.capability.request.getLegendGraphic.dcpType.front().http.get.onlineResource.xlinkHref;
|
|
}
|
|
|
|
return url.isEmpty() ? url : prepareUri( url );
|
|
}
|
|
|
|
bool QgsWmsProvider::addLayers()
|
|
{
|
|
QgsDebugMsgLevel( "Entering: layers:" + mSettings.mActiveSubLayers.join( ", " ) + ", styles:" + mSettings.mActiveSubStyles.join( ", " ), 4 );
|
|
|
|
if ( mSettings.mActiveSubLayers.size() != mSettings.mActiveSubStyles.size() )
|
|
{
|
|
QgsMessageLog::logMessage( tr( "Number of layers and styles don't match" ), tr( "WMS" ) );
|
|
return false;
|
|
}
|
|
|
|
// Set the visibility of these new layers on by default
|
|
for ( const QString &layer : qgis::as_const( mSettings.mActiveSubLayers ) )
|
|
{
|
|
mActiveSubLayerVisibility[ layer ] = true;
|
|
QgsDebugMsgLevel( QStringLiteral( "set visibility of layer '%1' to true." ).arg( layer ), 3 );
|
|
}
|
|
|
|
// now that the layers have changed, the extent will as well.
|
|
mExtentDirty = true;
|
|
|
|
if ( mSettings.mTiled )
|
|
mTileLayer = nullptr;
|
|
|
|
QgsDebugMsgLevel( QStringLiteral( "Exiting." ), 4 );
|
|
|
|
return true;
|
|
}
|
|
|
|
void QgsWmsProvider::setConnectionName( QString const &connName )
|
|
{
|
|
mConnectionName = connName;
|
|
}
|
|
|
|
void QgsWmsProvider::setLayerOrder( QStringList const &layers )
|
|
{
|
|
QgsDebugMsg( QStringLiteral( "Entering." ) );
|
|
|
|
if ( layers.size() != mSettings.mActiveSubLayers.size() )
|
|
{
|
|
QgsDebugMsg( QStringLiteral( "Invalid layer list length" ) );
|
|
return;
|
|
}
|
|
|
|
QMap<QString, QString> styleMap;
|
|
for ( int i = 0; i < mSettings.mActiveSubLayers.size(); i++ )
|
|
{
|
|
styleMap.insert( mSettings.mActiveSubLayers[i], mSettings.mActiveSubStyles[i] );
|
|
}
|
|
|
|
for ( int i = 0; i < layers.size(); i++ )
|
|
{
|
|
if ( !styleMap.contains( layers[i] ) )
|
|
{
|
|
QgsDebugMsg( QStringLiteral( "Layer %1 not found" ).arg( layers[i] ) );
|
|
return;
|
|
}
|
|
}
|
|
|
|
mSettings.mActiveSubLayers = layers;
|
|
mSettings.mActiveSubStyles.clear();
|
|
for ( int i = 0; i < layers.size(); i++ )
|
|
{
|
|
mSettings.mActiveSubStyles.append( styleMap[ layers[i] ] );
|
|
}
|
|
|
|
QgsDebugMsg( QStringLiteral( "Exiting." ) );
|
|
}
|
|
|
|
|
|
void QgsWmsProvider::setSubLayerVisibility( QString const &name, bool vis )
|
|
{
|
|
if ( !mActiveSubLayerVisibility.contains( name ) )
|
|
{
|
|
QgsDebugMsg( QStringLiteral( "Layer %1 not found." ).arg( name ) );
|
|
return;
|
|
}
|
|
|
|
mActiveSubLayerVisibility[name] = vis;
|
|
}
|
|
|
|
|
|
bool QgsWmsProvider::setImageCrs( QString const &crs )
|
|
{
|
|
QgsDebugMsgLevel( "Setting image CRS to " + crs + '.', 3 );
|
|
|
|
if ( crs != mImageCrs && !crs.isEmpty() )
|
|
{
|
|
mExtentDirty = true;
|
|
mImageCrs = crs;
|
|
}
|
|
|
|
if ( mSettings.mTiled )
|
|
{
|
|
if ( mSettings.mActiveSubLayers.size() != 1 )
|
|
{
|
|
appendError( ERR( tr( "Number of tile layers must be one" ) ) );
|
|
return false;
|
|
}
|
|
|
|
QgsDebugMsgLevel( QStringLiteral( "mTileLayersSupported.size() = %1" ).arg( mCaps.mTileLayersSupported.size() ), 2 );
|
|
if ( mCaps.mTileLayersSupported.isEmpty() )
|
|
{
|
|
appendError( ERR( tr( "Tile layer not found" ) ) );
|
|
return false;
|
|
}
|
|
|
|
for ( int i = 0; i < mCaps.mTileLayersSupported.size(); i++ )
|
|
{
|
|
QgsWmtsTileLayer *tl = &mCaps.mTileLayersSupported[i];
|
|
|
|
if ( tl->identifier != mSettings.mActiveSubLayers[0] )
|
|
continue;
|
|
|
|
if ( mSettings.mTileMatrixSetId.isEmpty() && tl->setLinks.size() == 1 )
|
|
{
|
|
QString tms = tl->setLinks.keys()[0];
|
|
|
|
if ( !mCaps.mTileMatrixSets.contains( tms ) )
|
|
{
|
|
QgsDebugMsg( QStringLiteral( "tile matrix set '%1' not found." ).arg( tms ) );
|
|
continue;
|
|
}
|
|
|
|
if ( mCaps.mTileMatrixSets[ tms ].crs != mImageCrs )
|
|
{
|
|
QgsDebugMsg( QStringLiteral( "tile matrix set '%1' has crs %2 instead of %3." ).arg( tms, mCaps.mTileMatrixSets[ tms ].crs, mImageCrs ) );
|
|
continue;
|
|
}
|
|
|
|
// fill in generate matrix for WMS-C
|
|
mSettings.mTileMatrixSetId = tms;
|
|
}
|
|
|
|
mTileLayer = tl;
|
|
break;
|
|
}
|
|
|
|
mNativeResolutions.clear();
|
|
if ( mCaps.mTileMatrixSets.contains( mSettings.mTileMatrixSetId ) )
|
|
{
|
|
mTileMatrixSet = &mCaps.mTileMatrixSets[ mSettings.mTileMatrixSetId ];
|
|
QList<double> keys = mTileMatrixSet->tileMatrices.keys();
|
|
std::sort( keys.begin(), keys.end() );
|
|
const auto constKeys = keys;
|
|
for ( double key : constKeys )
|
|
{
|
|
mNativeResolutions << key;
|
|
}
|
|
if ( !mTileMatrixSet->tileMatrices.empty() )
|
|
{
|
|
setProperty( "tileWidth", mTileMatrixSet->tileMatrices.values().first().tileWidth );
|
|
setProperty( "tileHeight", mTileMatrixSet->tileMatrices.values().first().tileHeight );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
QgsDebugMsg( QStringLiteral( "Expected tile matrix set '%1' not found." ).arg( mSettings.mTileMatrixSetId ) );
|
|
mTileMatrixSet = nullptr;
|
|
}
|
|
|
|
if ( !mTileLayer || !mTileMatrixSet )
|
|
{
|
|
appendError( ERR( tr( "Tile layer or tile matrix set not found" ) ) );
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void QgsWmsProvider::setQueryItem( QUrl &url, const QString &item, const QString &value )
|
|
{
|
|
url.removeQueryItem( item );
|
|
if ( value.isNull() )
|
|
url.addQueryItem( item, "" );
|
|
else
|
|
url.addQueryItem( item, value );
|
|
}
|
|
|
|
void QgsWmsProvider::setFormatQueryItem( QUrl &url )
|
|
{
|
|
url.removeQueryItem( QStringLiteral( "FORMAT" ) );
|
|
if ( mSettings.mImageMimeType.contains( '+' ) )
|
|
{
|
|
QString format( mSettings.mImageMimeType );
|
|
format.replace( '+', QLatin1String( "%2b" ) );
|
|
url.addEncodedQueryItem( "FORMAT", format.toUtf8() );
|
|
}
|
|
else
|
|
setQueryItem( url, QStringLiteral( "FORMAT" ), mSettings.mImageMimeType );
|
|
}
|
|
|
|
|
|
static bool _fuzzyContainsRect( const QRectF &r1, const QRectF &r2 )
|
|
{
|
|
double significantDigits = std::log10( std::max( r1.width(), r1.height() ) );
|
|
double epsilon = std::pow( 10.0, significantDigits - 5 ); // floats have 6-9 significant digits
|
|
return r1.contains( r2.adjusted( epsilon, epsilon, -epsilon, -epsilon ) );
|
|
}
|
|
|
|
void QgsWmsProvider::fetchOtherResTiles( QgsTileMode tileMode, const QgsRectangle &viewExtent, int imageWidth, QList<QRectF> &missingRects, double tres, int resOffset, QList<TileImage> &otherResTiles )
|
|
{
|
|
if ( !mTileMatrixSet )
|
|
return; // there is no tile matrix set defined for ordinary WMS (with user-specified tile size)
|
|
|
|
const QgsWmtsTileMatrix *tmOther = mTileMatrixSet->findOtherResolution( tres, resOffset );
|
|
if ( !tmOther )
|
|
return;
|
|
|
|
QSet<TilePosition> tilesSet;
|
|
const auto constMissingRects = missingRects;
|
|
for ( const QRectF &missingTileRect : constMissingRects )
|
|
{
|
|
int c0, r0, c1, r1;
|
|
tmOther->viewExtentIntersection( QgsRectangle( missingTileRect ), nullptr, c0, r0, c1, r1 );
|
|
|
|
for ( int row = r0; row <= r1; row++ )
|
|
{
|
|
for ( int col = c0; col <= c1; col++ )
|
|
{
|
|
tilesSet << TilePosition( row, col );
|
|
}
|
|
}
|
|
}
|
|
|
|
// get URLs of tiles because their URLs are used as keys in the tile cache
|
|
TilePositions tiles = tilesSet.toList();
|
|
TileRequests requests;
|
|
switch ( tileMode )
|
|
{
|
|
case WMSC:
|
|
createTileRequestsWMSC( tmOther, tiles, requests );
|
|
break;
|
|
|
|
case WMTS:
|
|
createTileRequestsWMTS( tmOther, tiles, requests );
|
|
break;
|
|
|
|
case XYZ:
|
|
createTileRequestsXYZ( tmOther, tiles, requests );
|
|
break;
|
|
}
|
|
|
|
QList<QRectF> missingRectsToDelete;
|
|
const auto constRequests = requests;
|
|
for ( const TileRequest &r : constRequests )
|
|
{
|
|
QImage localImage;
|
|
if ( ! QgsTileCache::tile( r.url, localImage ) )
|
|
continue;
|
|
|
|
double cr = viewExtent.width() / imageWidth;
|
|
QRectF dst( ( r.rect.left() - viewExtent.xMinimum() ) / cr,
|
|
( viewExtent.yMaximum() - r.rect.bottom() ) / cr,
|
|
r.rect.width() / cr,
|
|
r.rect.height() / cr );
|
|
otherResTiles << TileImage( dst, localImage, false );
|
|
|
|
// see if there are any missing rects that are completely covered by this tile
|
|
const auto constMissingRects = missingRects;
|
|
for ( const QRectF &missingRect : constMissingRects )
|
|
{
|
|
// we need to do a fuzzy "contains" check because the coordinates may not align perfectly
|
|
// due to numerical errors and/or transform of coords from double to floats
|
|
if ( _fuzzyContainsRect( r.rect, missingRect ) )
|
|
{
|
|
missingRectsToDelete << missingRect;
|
|
}
|
|
}
|
|
}
|
|
|
|
// remove all the rectangles we have completely covered by tiles from this resolution
|
|
// so we will not use tiles from multiple resolutions for one missing tile (to save time)
|
|
const auto constMissingRectsToDelete = missingRectsToDelete;
|
|
for ( const QRectF &rectToDelete : constMissingRectsToDelete )
|
|
{
|
|
missingRects.removeOne( rectToDelete );
|
|
}
|
|
|
|
QgsDebugMsgLevel( QStringLiteral( "Other resolution tiles: offset %1, res %2, missing rects %3, remaining rects %4, added tiles %5" )
|
|
.arg( resOffset )
|
|
.arg( tmOther->tres )
|
|
.arg( missingRects.count() + missingRectsToDelete.count() )
|
|
.arg( missingRects.count() )
|
|
.arg( otherResTiles.count() ), 3 );
|
|
}
|
|
|
|
uint qHash( QgsWmsProvider::TilePosition tp )
|
|
{
|
|
return ( uint ) tp.col + ( ( uint ) tp.row << 16 );
|
|
}
|
|
|
|
static void _drawDebugRect( QPainter &p, const QRectF &rect, const QColor &color )
|
|
{
|
|
#if 0 // good for debugging how tiles from various resolutions are used
|
|
QPainter::CompositionMode oldMode = p.compositionMode();
|
|
p.setCompositionMode( QPainter::CompositionMode_SourceOver );
|
|
QColor c = color;
|
|
c.setAlpha( 100 );
|
|
p.fillRect( rect, QBrush( c, Qt::DiagCrossPattern ) );
|
|
p.setCompositionMode( oldMode );
|
|
#else
|
|
Q_UNUSED( p )
|
|
Q_UNUSED( rect )
|
|
Q_UNUSED( color )
|
|
#endif
|
|
}
|
|
|
|
QImage *QgsWmsProvider::draw( QgsRectangle const &viewExtent, int pixelWidth, int pixelHeight, QgsRasterBlockFeedback *feedback )
|
|
{
|
|
if ( qApp && qApp->thread() == QThread::currentThread() )
|
|
{
|
|
QgsDebugMsg( QStringLiteral( "Trying to draw a WMS image on the main thread. Stop it!" ) );
|
|
}
|
|
|
|
// compose the URL query string for the WMS server.
|
|
|
|
QImage *image = new QImage( pixelWidth, pixelHeight, QImage::Format_ARGB32 );
|
|
image->fill( 0 );
|
|
|
|
int maxWidth = mCaps.mCapabilities.service.maxWidth == 0 ? std::numeric_limits<int>::max() : mCaps.mCapabilities.service.maxWidth;
|
|
int maxHeight = mCaps.mCapabilities.service.maxHeight == 0 ? std::numeric_limits<int>::max() : mCaps.mCapabilities.service.maxHeight;
|
|
|
|
if ( !mSettings.mTiled && mSettings.mMaxWidth == 0 && mSettings.mMaxHeight == 0 && pixelWidth <= maxWidth && pixelHeight <= maxHeight )
|
|
{
|
|
QUrl url = createRequestUrlWMS( viewExtent, pixelWidth, pixelHeight );
|
|
|
|
// cache some details for if the user wants to do an identifyAsHtml() later
|
|
|
|
emit statusChanged( tr( "Getting map via WMS." ) );
|
|
|
|
QgsWmsImageDownloadHandler handler( dataSourceUri(), url, mSettings.authorization(), image, feedback );
|
|
handler.downloadBlocking();
|
|
}
|
|
else
|
|
{
|
|
mTileReqNo++;
|
|
|
|
double vres = viewExtent.width() / pixelWidth;
|
|
|
|
const QgsWmtsTileMatrix *tm = nullptr;
|
|
std::unique_ptr<QgsWmtsTileMatrix> tempTm;
|
|
enum QgsTileMode tileMode;
|
|
|
|
if ( mSettings.mTiled )
|
|
{
|
|
Q_ASSERT( mTileLayer );
|
|
Q_ASSERT( mTileMatrixSet );
|
|
if ( mTileMatrixSet->tileMatrices.isEmpty() )
|
|
{
|
|
QgsDebugMsg( QStringLiteral( "WMTS tile set is empty!" ) );
|
|
return image;
|
|
}
|
|
|
|
// if we know both source and output DPI, let's scale the tiles
|
|
if ( mDpi != -1 && mTileLayer->dpi != -1 )
|
|
vres *= static_cast<double>( mDpi ) / mTileLayer->dpi;
|
|
|
|
// find nearest resolution
|
|
tm = mTileMatrixSet->findNearestResolution( vres );
|
|
Q_ASSERT( tm );
|
|
|
|
tileMode = mTileLayer->tileMode;
|
|
}
|
|
else if ( ( mSettings.mMaxWidth != 0 && mSettings.mMaxHeight != 0 ) || pixelWidth > maxWidth || pixelHeight > maxHeight )
|
|
{
|
|
int w = mSettings.mMaxWidth != 0 && mSettings.mMaxWidth < maxWidth ? mSettings.mMaxWidth : maxWidth;
|
|
int h = mSettings.mMaxHeight != 0 && mSettings.mMaxHeight < maxHeight ? mSettings.mMaxHeight : maxHeight;
|
|
|
|
// this is an ordinary WMS server, but the user requested tiled approach
|
|
// so we will pretend it is a WMS-C server with just one tile matrix
|
|
tempTm.reset( new QgsWmtsTileMatrix );
|
|
tempTm->topLeft = QgsPointXY( mLayerExtent.xMinimum(), mLayerExtent.yMaximum() );
|
|
tempTm->tileWidth = w;
|
|
tempTm->tileHeight = h;
|
|
tempTm->matrixWidth = std::ceil( mLayerExtent.width() / w / vres );
|
|
tempTm->matrixHeight = std::ceil( mLayerExtent.height() / h / vres );
|
|
tempTm->tres = vres;
|
|
tm = tempTm.get();
|
|
|
|
tileMode = WMSC;
|
|
}
|
|
else
|
|
{
|
|
QgsDebugMsg( QStringLiteral( "empty tile size" ) );
|
|
return image;
|
|
}
|
|
|
|
QgsDebugMsgLevel( QStringLiteral( "layer extent: %1,%2 %3x%4" )
|
|
.arg( qgsDoubleToString( mLayerExtent.xMinimum() ),
|
|
qgsDoubleToString( mLayerExtent.yMinimum() ) )
|
|
.arg( mLayerExtent.width() )
|
|
.arg( mLayerExtent.height() ), 3
|
|
);
|
|
|
|
QgsDebugMsgLevel( QStringLiteral( "view extent: %1,%2 %3x%4 res:%5" )
|
|
.arg( qgsDoubleToString( viewExtent.xMinimum() ),
|
|
qgsDoubleToString( viewExtent.yMinimum() ) )
|
|
.arg( viewExtent.width() )
|
|
.arg( viewExtent.height() )
|
|
.arg( vres, 0, 'f' ), 3
|
|
);
|
|
|
|
QgsDebugMsgLevel( QStringLiteral( "tile matrix %1,%2 res:%3 tilesize:%4x%5 matrixsize:%6x%7 id:%8" )
|
|
.arg( tm->topLeft.x() ).arg( tm->topLeft.y() ).arg( tm->tres )
|
|
.arg( tm->tileWidth ).arg( tm->tileHeight )
|
|
.arg( tm->matrixWidth ).arg( tm->matrixHeight )
|
|
.arg( tm->identifier ), 3
|
|
);
|
|
|
|
const QgsWmtsTileMatrixLimits *tml = nullptr;
|
|
|
|
if ( mTileLayer &&
|
|
mTileLayer->setLinks.contains( mTileMatrixSet->identifier ) &&
|
|
mTileLayer->setLinks[ mTileMatrixSet->identifier ].limits.contains( tm->identifier ) )
|
|
{
|
|
tml = &mTileLayer->setLinks[ mTileMatrixSet->identifier ].limits[ tm->identifier ];
|
|
}
|
|
|
|
// calculate tile coordinates
|
|
int col0, col1, row0, row1;
|
|
tm->viewExtentIntersection( viewExtent, tml, col0, row0, col1, row1 );
|
|
|
|
#ifdef QGISDEBUG
|
|
int n = ( col1 - col0 + 1 ) * ( row1 - row0 + 1 );
|
|
QgsDebugMsgLevel( QStringLiteral( "tile number: %1x%2 = %3" ).arg( col1 - col0 + 1 ).arg( row1 - row0 + 1 ).arg( n ), 3 );
|
|
if ( n > 256 )
|
|
{
|
|
emit statusChanged( QStringLiteral( "current view would need %1 tiles. tile request per draw limited to 256." ).arg( n ) );
|
|
return image;
|
|
}
|
|
#endif
|
|
|
|
TilePositions tiles;
|
|
for ( int row = row0; row <= row1; row++ )
|
|
{
|
|
for ( int col = col0; col <= col1; col++ )
|
|
{
|
|
tiles << TilePosition( row, col );
|
|
}
|
|
}
|
|
|
|
TileRequests requests;
|
|
switch ( tileMode )
|
|
{
|
|
case WMSC:
|
|
createTileRequestsWMSC( tm, tiles, requests );
|
|
break;
|
|
|
|
case WMTS:
|
|
createTileRequestsWMTS( tm, tiles, requests );
|
|
break;
|
|
|
|
case XYZ:
|
|
createTileRequestsXYZ( tm, tiles, requests );
|
|
break;
|
|
|
|
default:
|
|
QgsDebugMsg( QStringLiteral( "unexpected tile mode %1" ).arg( mTileLayer->tileMode ) );
|
|
return image;
|
|
}
|
|
|
|
emit statusChanged( tr( "Getting tiles." ) );
|
|
|
|
QList<TileImage> tileImages; // in the correct resolution
|
|
QList<QRectF> missing; // rectangles (in map coords) of missing tiles for this view
|
|
|
|
QTime t;
|
|
t.start();
|
|
TileRequests requestsFinal;
|
|
const auto constRequests = requests;
|
|
for ( const TileRequest &r : constRequests )
|
|
{
|
|
QImage localImage;
|
|
if ( QgsTileCache::tile( r.url, localImage ) )
|
|
{
|
|
double cr = viewExtent.width() / image->width();
|
|
QRectF dst( ( r.rect.left() - viewExtent.xMinimum() ) / cr,
|
|
( viewExtent.yMaximum() - r.rect.bottom() ) / cr,
|
|
r.rect.width() / cr,
|
|
r.rect.height() / cr );
|
|
// if image size is "close enough" to destination size, don't smooth it out. Instead try for pixel-perfect placement!
|
|
bool disableSmoothing = ( qgsDoubleNear( dst.width(), tm->tileWidth, 2 ) && qgsDoubleNear( dst.height(), tm->tileHeight, 2 ) );
|
|
tileImages << TileImage( dst, localImage, !disableSmoothing );
|
|
}
|
|
else
|
|
{
|
|
missing << r.rect;
|
|
|
|
// need to make a request
|
|
requestsFinal << r;
|
|
}
|
|
}
|
|
int t0 = t.elapsed();
|
|
|
|
|
|
// draw other res tiles if preview
|
|
QPainter p( image );
|
|
if ( feedback && feedback->isPreviewOnly() && missing.count() > 0 )
|
|
{
|
|
// some tiles are still missing, so let's see if we have any cached tiles
|
|
// from lower or higher resolution available to give the user a bit of context
|
|
// while loading the right resolution
|
|
|
|
p.setCompositionMode( QPainter::CompositionMode_Source );
|
|
#if 0 // for debugging
|
|
p.fillRect( image->rect(), QBrush( Qt::lightGray, Qt::CrossPattern ) );
|
|
#endif
|
|
p.setRenderHint( QPainter::SmoothPixmapTransform, false ); // let's not waste time with bilinear filtering
|
|
|
|
QList<TileImage> lowerResTiles, lowerResTiles2, higherResTiles;
|
|
// first we check lower resolution tiles: one level back, then two levels back (if there is still some are not covered),
|
|
// finally (in the worst case we use one level higher resolution tiles). This heuristic should give
|
|
// good overviews while not spending too much time drawing cached tiles from resolutions far away.
|
|
fetchOtherResTiles( tileMode, viewExtent, image->width(), missing, tm->tres, 1, lowerResTiles );
|
|
fetchOtherResTiles( tileMode, viewExtent, image->width(), missing, tm->tres, 2, lowerResTiles2 );
|
|
fetchOtherResTiles( tileMode, viewExtent, image->width(), missing, tm->tres, -1, higherResTiles );
|
|
|
|
// draw the cached tiles lowest to highest resolution
|
|
const auto constLowerResTiles2 = lowerResTiles2;
|
|
for ( const TileImage &ti : constLowerResTiles2 )
|
|
{
|
|
p.drawImage( ti.rect, ti.img );
|
|
_drawDebugRect( p, ti.rect, Qt::blue );
|
|
}
|
|
const auto constLowerResTiles = lowerResTiles;
|
|
for ( const TileImage &ti : constLowerResTiles )
|
|
{
|
|
p.drawImage( ti.rect, ti.img );
|
|
_drawDebugRect( p, ti.rect, Qt::yellow );
|
|
}
|
|
const auto constHigherResTiles = higherResTiles;
|
|
for ( const TileImage &ti : constHigherResTiles )
|
|
{
|
|
p.drawImage( ti.rect, ti.img );
|
|
_drawDebugRect( p, ti.rect, Qt::red );
|
|
}
|
|
}
|
|
|
|
int t1 = t.elapsed() - t0;
|
|
|
|
// draw composite in this resolution
|
|
const auto constTileImages = tileImages;
|
|
for ( const TileImage &ti : constTileImages )
|
|
{
|
|
if ( ti.smooth && mSettings.mSmoothPixmapTransform )
|
|
p.setRenderHint( QPainter::SmoothPixmapTransform, true );
|
|
p.drawImage( ti.rect, ti.img );
|
|
|
|
if ( feedback && feedback->isPreviewOnly() )
|
|
_drawDebugRect( p, ti.rect, Qt::green );
|
|
}
|
|
p.end();
|
|
|
|
int t2 = t.elapsed() - t1;
|
|
Q_UNUSED( t2 ) // only used in debug build
|
|
|
|
if ( feedback && feedback->isPreviewOnly() )
|
|
{
|
|
QgsDebugMsgLevel( QStringLiteral( "PREVIEW - CACHED: %1 / MISSING: %2" ).arg( tileImages.count() ).arg( requests.count() - tileImages.count() ), 4 );
|
|
QgsDebugMsgLevel( QStringLiteral( "PREVIEW - TIME: this res %1 ms | other res %2 ms | TOTAL %3 ms" ).arg( t0 + t2 ).arg( t1 ).arg( t0 + t1 + t2 ), 4 );
|
|
}
|
|
else if ( !requestsFinal.isEmpty() )
|
|
{
|
|
// let the feedback object know about the tiles we have already
|
|
if ( feedback && feedback->renderPartialOutput() )
|
|
feedback->onNewData();
|
|
|
|
// order tile requests according to the distance from view center
|
|
LessThanTileRequest cmp;
|
|
cmp.center = viewExtent.center();
|
|
std::sort( requestsFinal.begin(), requestsFinal.end(), cmp );
|
|
|
|
QgsWmsTiledImageDownloadHandler handler( dataSourceUri(), mSettings.authorization(), mTileReqNo, requestsFinal, image, viewExtent, mSettings.mSmoothPixmapTransform, feedback );
|
|
handler.downloadBlocking();
|
|
}
|
|
|
|
QgsDebugMsgLevel( QStringLiteral( "TILE CACHE total: %1 / %2" ).arg( QgsTileCache::totalCost() ).arg( QgsTileCache::maxCost() ), 3 );
|
|
|
|
#if 0
|
|
const QgsWmsStatistics::Stat &stat = QgsWmsStatistics::statForUri( dataSourceUri() );
|
|
emit statusChanged( tr( "%n tile requests in background", "tile request count", requests.count() )
|
|
+ tr( ", %n cache hits", "tile cache hits", stat.cacheHits )
|
|
+ tr( ", %n cache misses.", "tile cache missed", stat.cacheMisses )
|
|
+ tr( ", %n errors.", "errors", stat.errors )
|
|
);
|
|
#endif
|
|
}
|
|
|
|
return image;
|
|
}
|
|
|
|
bool QgsWmsProvider::readBlock( int bandNo, QgsRectangle const &viewExtent, int pixelWidth, int pixelHeight, void *block, QgsRasterBlockFeedback *feedback )
|
|
{
|
|
Q_UNUSED( bandNo )
|
|
// TODO: optimize to avoid writing to QImage
|
|
std::unique_ptr< QImage > image( draw( viewExtent, pixelWidth, pixelHeight, feedback ) );
|
|
if ( !image ) // should not happen
|
|
{
|
|
QgsMessageLog::logMessage( tr( "image is NULL" ), tr( "WMS" ) );
|
|
return false;
|
|
}
|
|
|
|
QgsDebugMsgLevel( QStringLiteral( "image height = %1 bytesPerLine = %2" ).arg( image->height() ) . arg( image->bytesPerLine() ), 3 );
|
|
size_t myExpectedSize = pixelWidth * pixelHeight * 4;
|
|
size_t myImageSize = image->height() * image->bytesPerLine();
|
|
if ( myExpectedSize != myImageSize ) // should not happen
|
|
{
|
|
QgsMessageLog::logMessage( tr( "unexpected image size" ), tr( "WMS" ) );
|
|
return false;
|
|
}
|
|
|
|
uchar *ptr = image->bits();
|
|
if ( ptr )
|
|
{
|
|
// If image is too large, ptr can be NULL
|
|
memcpy( block, ptr, myExpectedSize );
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
QUrl QgsWmsProvider::createRequestUrlWMS( const QgsRectangle &viewExtent, int pixelWidth, int pixelHeight )
|
|
{
|
|
// Calculate active layers that are also visible.
|
|
|
|
bool changeXY = mCaps.shouldInvertAxisOrientation( mImageCrs );
|
|
|
|
QgsDebugMsg( "Active layer list of " + mSettings.mActiveSubLayers.join( ", " )
|
|
+ " and style list of " + mSettings.mActiveSubStyles.join( ", " ) );
|
|
|
|
QStringList visibleLayers = QStringList();
|
|
QStringList visibleStyles = QStringList();
|
|
|
|
QStringList::const_iterator it2 = mSettings.mActiveSubStyles.constBegin();
|
|
|
|
for ( QStringList::const_iterator it = mSettings.mActiveSubLayers.constBegin();
|
|
it != mSettings.mActiveSubLayers.constEnd();
|
|
++it )
|
|
{
|
|
if ( mActiveSubLayerVisibility.constFind( *it ).value() )
|
|
{
|
|
visibleLayers += *it;
|
|
visibleStyles += *it2;
|
|
}
|
|
|
|
++it2;
|
|
}
|
|
|
|
QString layers = visibleLayers.join( ',' );
|
|
layers = layers.isNull() ? QString() : layers;
|
|
QString styles = visibleStyles.join( ',' );
|
|
styles = styles.isNull() ? QString() : styles;
|
|
|
|
QgsDebugMsg( "Visible layer list of " + layers + " and style list of " + styles );
|
|
|
|
QString bbox = toParamValue( viewExtent, changeXY );
|
|
|
|
QUrl url( mSettings.mIgnoreGetMapUrl ? mSettings.mBaseUrl : getMapUrl() );
|
|
setQueryItem( url, QStringLiteral( "SERVICE" ), QStringLiteral( "WMS" ) );
|
|
setQueryItem( url, QStringLiteral( "VERSION" ), mCaps.mCapabilities.version );
|
|
setQueryItem( url, QStringLiteral( "REQUEST" ), QStringLiteral( "GetMap" ) );
|
|
setQueryItem( url, QStringLiteral( "BBOX" ), bbox );
|
|
setSRSQueryItem( url );
|
|
setQueryItem( url, QStringLiteral( "WIDTH" ), QString::number( pixelWidth ) );
|
|
setQueryItem( url, QStringLiteral( "HEIGHT" ), QString::number( pixelHeight ) );
|
|
setQueryItem( url, QStringLiteral( "LAYERS" ), layers );
|
|
setQueryItem( url, QStringLiteral( "STYLES" ), styles );
|
|
setFormatQueryItem( url );
|
|
|
|
if ( mDpi != -1 )
|
|
{
|
|
if ( mSettings.mDpiMode & DpiQGIS )
|
|
setQueryItem( url, QStringLiteral( "DPI" ), QString::number( mDpi ) );
|
|
if ( mSettings.mDpiMode & DpiUMN )
|
|
setQueryItem( url, QStringLiteral( "MAP_RESOLUTION" ), QString::number( mDpi ) );
|
|
if ( mSettings.mDpiMode & DpiGeoServer )
|
|
setQueryItem( url, QStringLiteral( "FORMAT_OPTIONS" ), QStringLiteral( "dpi:%1" ).arg( mDpi ) );
|
|
}
|
|
|
|
//MH: jpeg does not support transparency and some servers complain if jpg and transparent=true
|
|
if ( mSettings.mImageMimeType == QLatin1String( "image/x-jpegorpng" ) ||
|
|
( !mSettings.mImageMimeType.contains( QLatin1String( "jpeg" ), Qt::CaseInsensitive ) &&
|
|
!mSettings.mImageMimeType.contains( QLatin1String( "jpg" ), Qt::CaseInsensitive ) ) )
|
|
{
|
|
setQueryItem( url, QStringLiteral( "TRANSPARENT" ), QStringLiteral( "TRUE" ) ); // some servers giving error for 'true' (lowercase)
|
|
}
|
|
|
|
QgsDebugMsg( QStringLiteral( "getmap: %1" ).arg( url.toString() ) );
|
|
return url;
|
|
}
|
|
|
|
|
|
void QgsWmsProvider::createTileRequestsWMSC( const QgsWmtsTileMatrix *tm, const QgsWmsProvider::TilePositions &tiles, QgsWmsProvider::TileRequests &requests )
|
|
{
|
|
bool changeXY = mCaps.shouldInvertAxisOrientation( mImageCrs );
|
|
|
|
// add WMS request
|
|
QUrl url( mSettings.mIgnoreGetMapUrl ? mSettings.mBaseUrl : getMapUrl() );
|
|
setQueryItem( url, QStringLiteral( "SERVICE" ), QStringLiteral( "WMS" ) );
|
|
setQueryItem( url, QStringLiteral( "VERSION" ), mCaps.mCapabilities.version );
|
|
setQueryItem( url, QStringLiteral( "REQUEST" ), QStringLiteral( "GetMap" ) );
|
|
setQueryItem( url, QStringLiteral( "LAYERS" ), mSettings.mActiveSubLayers.join( QStringLiteral( "," ) ) );
|
|
setQueryItem( url, QStringLiteral( "STYLES" ), mSettings.mActiveSubStyles.join( QStringLiteral( "," ) ) );
|
|
setQueryItem( url, QStringLiteral( "WIDTH" ), QString::number( tm->tileWidth ) );
|
|
setQueryItem( url, QStringLiteral( "HEIGHT" ), QString::number( tm->tileHeight ) );
|
|
setFormatQueryItem( url );
|
|
|
|
setSRSQueryItem( url );
|
|
|
|
if ( mSettings.mTiled )
|
|
{
|
|
setQueryItem( url, QStringLiteral( "TILED" ), QStringLiteral( "true" ) );
|
|
}
|
|
|
|
if ( mDpi != -1 )
|
|
{
|
|
if ( mSettings.mDpiMode & DpiQGIS )
|
|
setQueryItem( url, QStringLiteral( "DPI" ), QString::number( mDpi ) );
|
|
if ( mSettings.mDpiMode & DpiUMN )
|
|
setQueryItem( url, QStringLiteral( "MAP_RESOLUTION" ), QString::number( mDpi ) );
|
|
if ( mSettings.mDpiMode & DpiGeoServer )
|
|
setQueryItem( url, QStringLiteral( "FORMAT_OPTIONS" ), QStringLiteral( "dpi:%1" ).arg( mDpi ) );
|
|
}
|
|
|
|
if ( mSettings.mImageMimeType == QLatin1String( "image/x-jpegorpng" ) ||
|
|
( !mSettings.mImageMimeType.contains( QLatin1String( "jpeg" ), Qt::CaseInsensitive ) &&
|
|
!mSettings.mImageMimeType.contains( QLatin1String( "jpg" ), Qt::CaseInsensitive ) ) )
|
|
{
|
|
setQueryItem( url, QStringLiteral( "TRANSPARENT" ), QStringLiteral( "TRUE" ) ); // some servers giving error for 'true' (lowercase)
|
|
}
|
|
|
|
int i = 0;
|
|
const auto constTiles = tiles;
|
|
for ( const TilePosition &tile : constTiles )
|
|
{
|
|
QgsRectangle bbox( tm->tileBBox( tile.col, tile.row ) );
|
|
QString turl;
|
|
turl += url.toString();
|
|
turl += QString( changeXY ? "&BBOX=%2,%1,%4,%3" : "&BBOX=%1,%2,%3,%4" )
|
|
.arg( qgsDoubleToString( bbox.xMinimum() ),
|
|
qgsDoubleToString( bbox.yMinimum() ),
|
|
qgsDoubleToString( bbox.xMaximum() ),
|
|
qgsDoubleToString( bbox.yMaximum() ) );
|
|
|
|
QgsDebugMsg( QStringLiteral( "tileRequest %1 %2/%3 (%4,%5): %6" ).arg( mTileReqNo ).arg( i ).arg( tiles.count() ).arg( tile.row ).arg( tile.col ).arg( turl ) );
|
|
requests << TileRequest( turl, tm->tileRect( tile.col, tile.row ), i );
|
|
++i;
|
|
}
|
|
}
|
|
|
|
|
|
void QgsWmsProvider::createTileRequestsWMTS( const QgsWmtsTileMatrix *tm, const QgsWmsProvider::TilePositions &tiles, QgsWmsProvider::TileRequests &requests )
|
|
{
|
|
if ( !getTileUrl().isNull() )
|
|
{
|
|
// KVP
|
|
QUrl url( mSettings.mIgnoreGetMapUrl ? mSettings.mBaseUrl : getTileUrl() );
|
|
|
|
// compose static request arguments.
|
|
setQueryItem( url, QStringLiteral( "SERVICE" ), QStringLiteral( "WMTS" ) );
|
|
setQueryItem( url, QStringLiteral( "REQUEST" ), QStringLiteral( "GetTile" ) );
|
|
setQueryItem( url, QStringLiteral( "VERSION" ), mCaps.mCapabilities.version );
|
|
setQueryItem( url, QStringLiteral( "LAYER" ), mSettings.mActiveSubLayers[0] );
|
|
setQueryItem( url, QStringLiteral( "STYLE" ), mSettings.mActiveSubStyles[0] );
|
|
setQueryItem( url, QStringLiteral( "FORMAT" ), mSettings.mImageMimeType );
|
|
setQueryItem( url, QStringLiteral( "TILEMATRIXSET" ), mTileMatrixSet->identifier );
|
|
setQueryItem( url, QStringLiteral( "TILEMATRIX" ), tm->identifier );
|
|
|
|
for ( QHash<QString, QString>::const_iterator it = mSettings.mTileDimensionValues.constBegin(); it != mSettings.mTileDimensionValues.constEnd(); ++it )
|
|
{
|
|
setQueryItem( url, it.key(), it.value() );
|
|
}
|
|
|
|
url.removeQueryItem( QStringLiteral( "TILEROW" ) );
|
|
url.removeQueryItem( QStringLiteral( "TILECOL" ) );
|
|
|
|
int i = 0;
|
|
const auto constTiles = tiles;
|
|
for ( const TilePosition &tile : constTiles )
|
|
{
|
|
QString turl;
|
|
turl += url.toString();
|
|
turl += QStringLiteral( "&TILEROW=%1&TILECOL=%2" ).arg( tile.row ).arg( tile.col );
|
|
|
|
QgsDebugMsg( QStringLiteral( "tileRequest %1 %2/%3 (%4,%5): %6" ).arg( mTileReqNo ).arg( i ).arg( tiles.count() ).arg( tile.row ).arg( tile.col ).arg( turl ) );
|
|
requests << TileRequest( turl, tm->tileRect( tile.col, tile.row ), i );
|
|
++i;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// REST
|
|
QString url = mTileLayer->getTileURLs[ mSettings.mImageMimeType ];
|
|
|
|
url.replace( QLatin1String( "{layer}" ), mSettings.mActiveSubLayers[0], Qt::CaseInsensitive );
|
|
url.replace( QLatin1String( "{style}" ), mSettings.mActiveSubStyles[0], Qt::CaseInsensitive );
|
|
url.replace( QLatin1String( "{tilematrixset}" ), mTileMatrixSet->identifier, Qt::CaseInsensitive );
|
|
url.replace( QLatin1String( "{tilematrix}" ), tm->identifier, Qt::CaseInsensitive );
|
|
|
|
for ( QHash<QString, QString>::const_iterator it = mSettings.mTileDimensionValues.constBegin(); it != mSettings.mTileDimensionValues.constEnd(); ++it )
|
|
{
|
|
url.replace( "{" + it.key() + "}", it.value(), Qt::CaseInsensitive );
|
|
}
|
|
|
|
int i = 0;
|
|
const auto constTiles = tiles;
|
|
for ( const TilePosition &tile : constTiles )
|
|
{
|
|
QString turl( url );
|
|
turl.replace( QLatin1String( "{tilerow}" ), QString::number( tile.row ), Qt::CaseInsensitive );
|
|
turl.replace( QLatin1String( "{tilecol}" ), QString::number( tile.col ), Qt::CaseInsensitive );
|
|
|
|
QgsDebugMsgLevel( QStringLiteral( "tileRequest %1 %2/%3 (%4,%5): %6" ).arg( mTileReqNo ).arg( i ).arg( tiles.count() ).arg( tile.row ).arg( tile.col ).arg( turl ), 2 );
|
|
requests << TileRequest( turl, tm->tileRect( tile.col, tile.row ), i );
|
|
++i;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// support for Bing Maps tile system
|
|
// https://msdn.microsoft.com/en-us/library/bb259689.aspx
|
|
static QString _tile2quadkey( int tileX, int tileY, int z )
|
|
{
|
|
QString quadKey;
|
|
for ( int i = z; i > 0; i-- )
|
|
{
|
|
char digit = '0';
|
|
int mask = 1 << ( i - 1 );
|
|
if ( tileX & mask )
|
|
digit++;
|
|
if ( tileY & mask )
|
|
digit += 2;
|
|
quadKey.append( QChar( digit ) );
|
|
}
|
|
return quadKey;
|
|
}
|
|
|
|
|
|
void QgsWmsProvider::createTileRequestsXYZ( const QgsWmtsTileMatrix *tm, const QgsWmsProvider::TilePositions &tiles, QgsWmsProvider::TileRequests &requests )
|
|
{
|
|
int z = tm->identifier.toInt();
|
|
QString url = mSettings.mBaseUrl;
|
|
int i = 0;
|
|
const auto constTiles = tiles;
|
|
for ( const TilePosition &tile : constTiles )
|
|
{
|
|
++i;
|
|
QString turl( url );
|
|
|
|
if ( turl.contains( QLatin1String( "{q}" ) ) ) // used in Bing maps
|
|
turl.replace( QLatin1String( "{q}" ), _tile2quadkey( tile.col, tile.row, z ) );
|
|
|
|
turl.replace( QLatin1String( "{x}" ), QString::number( tile.col ), Qt::CaseInsensitive );
|
|
// inverted Y axis
|
|
if ( turl.contains( QLatin1String( "{-y}" ) ) )
|
|
{
|
|
turl.replace( QLatin1String( "{-y}" ), QString::number( tm->matrixHeight - tile.row - 1 ), Qt::CaseInsensitive );
|
|
}
|
|
else
|
|
{
|
|
turl.replace( QLatin1String( "{y}" ), QString::number( tile.row ), Qt::CaseInsensitive );
|
|
}
|
|
turl.replace( QLatin1String( "{z}" ), QString::number( z ), Qt::CaseInsensitive );
|
|
|
|
QgsDebugMsgLevel( QStringLiteral( "tileRequest %1 %2/%3 (%4,%5): %6" ).arg( mTileReqNo ).arg( i ).arg( tiles.count() ).arg( tile.row ).arg( tile.col ).arg( turl ), 2 );
|
|
requests << TileRequest( turl, tm->tileRect( tile.col, tile.row ), i );
|
|
}
|
|
}
|
|
|
|
|
|
bool QgsWmsProvider::retrieveServerCapabilities( bool forceRefresh )
|
|
{
|
|
QgsDebugMsg( QStringLiteral( "entering: forceRefresh=%1" ).arg( forceRefresh ) );
|
|
|
|
if ( !mCaps.isValid() )
|
|
{
|
|
QgsWmsCapabilitiesDownload downloadCaps( mSettings.baseUrl(), mSettings.authorization(), forceRefresh );
|
|
if ( !downloadCaps.downloadCapabilities() )
|
|
{
|
|
mErrorFormat = QStringLiteral( "text/plain" );
|
|
mError = downloadCaps.lastError();
|
|
return false;
|
|
}
|
|
|
|
QgsWmsCapabilities caps( transformContext() );
|
|
if ( !caps.parseResponse( downloadCaps.response(), mSettings.parserSettings() ) )
|
|
{
|
|
mErrorFormat = caps.lastErrorFormat();
|
|
mError = caps.lastError();
|
|
return false;
|
|
}
|
|
|
|
mCaps = caps;
|
|
}
|
|
|
|
Q_ASSERT( mCaps.isValid() );
|
|
|
|
QgsDebugMsg( QStringLiteral( "exiting." ) );
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
void QgsWmsProvider::setupXyzCapabilities( const QString &uri )
|
|
{
|
|
QgsDataSourceUri parsedUri;
|
|
parsedUri.setEncodedUri( uri );
|
|
|
|
QgsCoordinateTransform ct( QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:4326" ) ), QgsCoordinateReferenceSystem( mSettings.mCrsId ),
|
|
transformContext() );
|
|
|
|
// the whole world is projected to a square:
|
|
// X going from 180 W to 180 E
|
|
// Y going from ~85 N to ~85 S (=atan(sinh(pi)) ... to get a square)
|
|
QgsPointXY topLeftLonLat( -180, 180.0 / M_PI * std::atan( std::sinh( M_PI ) ) );
|
|
QgsPointXY bottomRightLonLat( 180, 180.0 / M_PI * std::atan( std::sinh( -M_PI ) ) );
|
|
QgsPointXY topLeft = ct.transform( topLeftLonLat );
|
|
QgsPointXY bottomRight = ct.transform( bottomRightLonLat );
|
|
double xspan = ( bottomRight.x() - topLeft.x() );
|
|
|
|
QgsWmsBoundingBoxProperty bbox;
|
|
bbox.crs = mSettings.mCrsId;
|
|
bbox.box = QgsRectangle( topLeft.x(), bottomRight.y(), bottomRight.x(), topLeft.y() );
|
|
|
|
QgsWmtsTileLayer tl;
|
|
tl.tileMode = XYZ;
|
|
tl.identifier = QStringLiteral( "xyz" ); // as set in parseUri
|
|
tl.boundingBoxes << bbox;
|
|
|
|
double tilePixelRatio = 0.; // unknown
|
|
if ( parsedUri.hasParam( QStringLiteral( "tilePixelRatio" ) ) )
|
|
tilePixelRatio = parsedUri.param( QStringLiteral( "tilePixelRatio" ) ).toDouble();
|
|
|
|
if ( tilePixelRatio != 0 )
|
|
{
|
|
// known tile pixel ratio - will be doing auto-scaling of tiles based on output DPI
|
|
tl.dpi = 96 * tilePixelRatio; // TODO: is 96 correct base DPI ?
|
|
}
|
|
else
|
|
{
|
|
// unknown tile pixel ratio - no scaling of tiles based on output DPI
|
|
tilePixelRatio = 1;
|
|
}
|
|
|
|
mCaps.mTileLayersSupported.append( tl );
|
|
|
|
QgsWmtsTileMatrixSet tms;
|
|
tms.identifier = QStringLiteral( "tms0" ); // as set in parseUri
|
|
tms.crs = mSettings.mCrsId;
|
|
mCaps.mTileMatrixSets[tms.identifier] = tms;
|
|
|
|
int minZoom = 0;
|
|
int maxZoom = 18;
|
|
if ( parsedUri.hasParam( QStringLiteral( "zmin" ) ) )
|
|
minZoom = parsedUri.param( QStringLiteral( "zmin" ) ).toInt();
|
|
if ( parsedUri.hasParam( QStringLiteral( "zmax" ) ) )
|
|
maxZoom = parsedUri.param( QStringLiteral( "zmax" ) ).toInt();
|
|
|
|
// zoom 0 is one tile for the whole world
|
|
for ( int zoom = minZoom; zoom <= maxZoom; ++zoom )
|
|
{
|
|
QgsWmtsTileMatrix tm;
|
|
tm.identifier = QString::number( zoom );
|
|
tm.topLeft = topLeft;
|
|
tm.tileWidth = tm.tileHeight = 256 * tilePixelRatio;
|
|
tm.matrixWidth = tm.matrixHeight = 1 << zoom;
|
|
tm.tres = xspan / ( tm.tileWidth * tm.matrixWidth );
|
|
tm.scaleDenom = 0.0;
|
|
|
|
mCaps.mTileMatrixSets[tms.identifier].tileMatrices[tm.tres] = tm;
|
|
}
|
|
}
|
|
|
|
|
|
Qgis::DataType QgsWmsProvider::dataType( int bandNo ) const
|
|
{
|
|
return sourceDataType( bandNo );
|
|
}
|
|
|
|
Qgis::DataType QgsWmsProvider::sourceDataType( int bandNo ) const
|
|
{
|
|
Q_UNUSED( bandNo )
|
|
return Qgis::ARGB32;
|
|
}
|
|
|
|
int QgsWmsProvider::bandCount() const
|
|
{
|
|
return 1;
|
|
}
|
|
|
|
|
|
static const QgsWmsLayerProperty *_findNestedLayerProperty( const QString &layerName, const QgsWmsLayerProperty *prop )
|
|
{
|
|
if ( prop->name == layerName )
|
|
return prop;
|
|
|
|
for ( const QgsWmsLayerProperty &child : qgis::as_const( prop->layer ) )
|
|
{
|
|
if ( const QgsWmsLayerProperty *res = _findNestedLayerProperty( layerName, &child ) )
|
|
return res;
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
|
|
bool QgsWmsProvider::extentForNonTiledLayer( const QString &layerName, const QString &crs, QgsRectangle &extent ) const
|
|
{
|
|
const QgsWmsLayerProperty *layerProperty = nullptr;
|
|
for ( const QgsWmsLayerProperty &toplevelLayer : qgis::as_const( mCaps.mCapabilities.capability.layers ) )
|
|
{
|
|
layerProperty = _findNestedLayerProperty( layerName, &toplevelLayer );
|
|
if ( layerProperty )
|
|
break;
|
|
}
|
|
if ( !layerProperty )
|
|
return false;
|
|
|
|
// see if we can refine the bounding box with the CRS-specific bounding boxes
|
|
for ( int i = 0; i < layerProperty->boundingBoxes.size(); i++ )
|
|
{
|
|
if ( layerProperty->boundingBoxes[i].crs == crs )
|
|
{
|
|
// exact bounding box is provided for this CRS
|
|
extent = layerProperty->boundingBoxes[i].box;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// exact bounding box for given CRS is not listed - we need to pick a different
|
|
// bounding box definition - either the coarse bounding box (in WGS84)
|
|
// or one of the alternative bounding box definitions for the layer
|
|
|
|
// Use the coarse bounding box
|
|
extent = layerProperty->ex_GeographicBoundingBox;
|
|
|
|
for ( int i = 0; i < layerProperty->boundingBoxes.size(); i++ )
|
|
{
|
|
if ( layerProperty->boundingBoxes[i].crs == DEFAULT_LATLON_CRS )
|
|
{
|
|
if ( layerProperty->boundingBoxes[i].box.contains( extent ) )
|
|
continue; // this bounding box is less specific (probably inherited from parent)
|
|
|
|
// this BBox is probably better than the one in ex_GeographicBoundingBox
|
|
extent = layerProperty->boundingBoxes[i].box;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// transform it to requested CRS
|
|
|
|
QgsCoordinateReferenceSystem dst = QgsCoordinateReferenceSystem::fromOgcWmsCrs( crs );
|
|
QgsCoordinateReferenceSystem wgs = QgsCoordinateReferenceSystem::fromOgcWmsCrs( DEFAULT_LATLON_CRS );
|
|
if ( !wgs.isValid() || !dst.isValid() )
|
|
return false;
|
|
|
|
QgsCoordinateTransform xform( wgs, dst, transformContext() );
|
|
|
|
QgsDebugMsg( QStringLiteral( "transforming layer extent %1" ).arg( extent.toString( true ) ) );
|
|
try
|
|
{
|
|
extent = xform.transformBoundingBox( extent );
|
|
}
|
|
catch ( QgsCsException &cse )
|
|
{
|
|
Q_UNUSED( cse )
|
|
return false;
|
|
}
|
|
QgsDebugMsg( QStringLiteral( "transformed layer extent %1" ).arg( extent.toString( true ) ) );
|
|
|
|
//make sure extent does not contain 'inf' or 'nan'
|
|
if ( !extent.isFinite() )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
bool QgsWmsProvider::parseServiceExceptionReportDom( QByteArray const &xml, QString &errorTitle, QString &errorText )
|
|
{
|
|
|
|
#ifdef QGISDEBUG
|
|
//test the content of the QByteArray
|
|
QString responsestring( xml );
|
|
QgsDebugMsg( "received the following data: " + responsestring );
|
|
#endif
|
|
|
|
// Convert completed document into a Dom
|
|
QDomDocument doc;
|
|
QString errorMsg;
|
|
int errorLine;
|
|
int errorColumn;
|
|
bool contentSuccess = doc.setContent( xml, false, &errorMsg, &errorLine, &errorColumn );
|
|
|
|
if ( !contentSuccess )
|
|
{
|
|
errorTitle = tr( "Dom Exception" );
|
|
errorText = tr( "Could not get WMS Service Exception: %1 at line %2 column %3\n\nResponse was:\n\n%4" )
|
|
.arg( errorMsg )
|
|
.arg( errorLine )
|
|
.arg( errorColumn )
|
|
.arg( QString( xml ) );
|
|
|
|
QgsLogger::debug( "Dom Exception: " + errorText );
|
|
|
|
return false;
|
|
}
|
|
|
|
QDomElement docElem = doc.documentElement();
|
|
|
|
// TODO: Assert the docElem.tagName() is "ServiceExceptionReport"
|
|
|
|
// serviceExceptionProperty.version = docElem.attribute("version");
|
|
|
|
// Start walking through XML.
|
|
QDomNode n = docElem.firstChild();
|
|
|
|
while ( !n.isNull() )
|
|
{
|
|
QDomElement e = n.toElement(); // try to convert the node to an element.
|
|
if ( !e.isNull() )
|
|
{
|
|
QgsDebugMsg( e.tagName() ); // the node really is an element.
|
|
|
|
QString tagName = e.tagName();
|
|
if ( tagName.startsWith( QLatin1String( "wms:" ) ) )
|
|
tagName = tagName.mid( 4 );
|
|
|
|
if ( tagName == QLatin1String( "ServiceException" ) )
|
|
{
|
|
QgsDebugMsg( QStringLiteral( " ServiceException." ) );
|
|
parseServiceException( e, errorTitle, errorText );
|
|
}
|
|
|
|
}
|
|
n = n.nextSibling();
|
|
}
|
|
|
|
QgsDebugMsg( QStringLiteral( "exiting." ) );
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
void QgsWmsProvider::parseServiceException( QDomElement const &e, QString &errorTitle, QString &errorText )
|
|
{
|
|
|
|
QString seCode = e.attribute( QStringLiteral( "code" ) );
|
|
QString seText = e.text();
|
|
|
|
errorTitle = tr( "Service Exception" );
|
|
|
|
// set up friendly descriptions for the service exception
|
|
if ( seCode == QLatin1String( "InvalidFormat" ) )
|
|
{
|
|
errorText = tr( "Request contains a format not offered by the server." );
|
|
}
|
|
else if ( seCode == QLatin1String( "InvalidCRS" ) )
|
|
{
|
|
errorText = tr( "Request contains a CRS not offered by the server for one or more of the Layers in the request." );
|
|
}
|
|
else if ( seCode == QLatin1String( "InvalidSRS" ) ) // legacy WMS < 1.3.0
|
|
{
|
|
errorText = tr( "Request contains a SRS not offered by the server for one or more of the Layers in the request." );
|
|
}
|
|
else if ( seCode == QLatin1String( "LayerNotDefined" ) )
|
|
{
|
|
errorText = tr( "GetMap request is for a Layer not offered by the server, "
|
|
"or GetFeatureInfo request is for a Layer not shown on the map." );
|
|
}
|
|
else if ( seCode == QLatin1String( "StyleNotDefined" ) )
|
|
{
|
|
errorText = tr( "Request is for a Layer in a Style not offered by the server." );
|
|
}
|
|
else if ( seCode == QLatin1String( "LayerNotQueryable" ) )
|
|
{
|
|
errorText = tr( "GetFeatureInfo request is applied to a Layer which is not declared queryable." );
|
|
}
|
|
else if ( seCode == QLatin1String( "InvalidPoint" ) )
|
|
{
|
|
errorText = tr( "GetFeatureInfo request contains invalid X or Y value." );
|
|
}
|
|
else if ( seCode == QLatin1String( "CurrentUpdateSequence" ) )
|
|
{
|
|
errorText = tr( "Value of (optional) UpdateSequence parameter in GetCapabilities request is equal to "
|
|
"current value of service metadata update sequence number." );
|
|
}
|
|
else if ( seCode == QLatin1String( "InvalidUpdateSequence" ) )
|
|
{
|
|
errorText = tr( "Value of (optional) UpdateSequence parameter in GetCapabilities request is greater "
|
|
"than current value of service metadata update sequence number." );
|
|
}
|
|
else if ( seCode == QLatin1String( "MissingDimensionValue" ) )
|
|
{
|
|
errorText = tr( "Request does not include a sample dimension value, and the server did not declare a "
|
|
"default value for that dimension." );
|
|
}
|
|
else if ( seCode == QLatin1String( "InvalidDimensionValue" ) )
|
|
{
|
|
errorText = tr( "Request contains an invalid sample dimension value." );
|
|
}
|
|
else if ( seCode == QLatin1String( "OperationNotSupported" ) )
|
|
{
|
|
errorText = tr( "Request is for an optional operation that is not supported by the server." );
|
|
}
|
|
else if ( seCode.isEmpty() )
|
|
{
|
|
errorText = tr( "(No error code was reported)" );
|
|
}
|
|
else
|
|
{
|
|
errorText = seCode + ' ' + tr( "(Unknown error code)" );
|
|
}
|
|
|
|
errorText += '\n' + tr( "The WMS vendor also reported: " );
|
|
errorText += seText;
|
|
|
|
// TODO = e.attribute("locator");
|
|
|
|
QgsDebugMsg( QStringLiteral( "exiting with composed error message '%1'." ).arg( errorText ) );
|
|
}
|
|
|
|
QgsRectangle QgsWmsProvider::extent() const
|
|
{
|
|
if ( mExtentDirty )
|
|
{
|
|
if ( calculateExtent() )
|
|
{
|
|
mExtentDirty = false;
|
|
}
|
|
}
|
|
|
|
return mLayerExtent;
|
|
}
|
|
|
|
bool QgsWmsProvider::isValid() const
|
|
{
|
|
return mValid;
|
|
}
|
|
|
|
|
|
QString QgsWmsProvider::wmsVersion()
|
|
{
|
|
// TODO
|
|
return QString();
|
|
}
|
|
|
|
|
|
QStringList QgsWmsProvider::subLayers() const
|
|
{
|
|
return mSettings.mActiveSubLayers;
|
|
}
|
|
|
|
|
|
QStringList QgsWmsProvider::subLayerStyles() const
|
|
{
|
|
return mSettings.mActiveSubStyles;
|
|
}
|
|
|
|
bool QgsWmsProvider::calculateExtent() const
|
|
{
|
|
//! \todo Make this handle non-geographic CRSs (e.g. std::floor plans) as per WMS spec
|
|
|
|
|
|
if ( mSettings.mTiled )
|
|
{
|
|
if ( mTileLayer )
|
|
{
|
|
int i;
|
|
for ( i = 0; i < mTileLayer->boundingBoxes.size() && mTileLayer->boundingBoxes[i].crs != mImageCrs; i++ )
|
|
QgsDebugMsg( QStringLiteral( "Skip %1 [%2]" ).arg( mTileLayer->boundingBoxes.at( i ).crs, mImageCrs ) );
|
|
|
|
if ( i < mTileLayer->boundingBoxes.size() )
|
|
{
|
|
mLayerExtent = mTileLayer->boundingBoxes[i].box;
|
|
}
|
|
else
|
|
{
|
|
QgsCoordinateReferenceSystem qgisSrsDest = QgsCoordinateReferenceSystem::fromOgcWmsCrs( mImageCrs );
|
|
|
|
// pick the first that transforms fin(it)e
|
|
for ( i = 0; i < mTileLayer->boundingBoxes.size(); i++ )
|
|
{
|
|
QgsCoordinateReferenceSystem qgisSrsSource = QgsCoordinateReferenceSystem::fromOgcWmsCrs( mTileLayer->boundingBoxes[i].crs );
|
|
|
|
QgsCoordinateTransform ct( qgisSrsSource, qgisSrsDest, transformContext() );
|
|
|
|
QgsDebugMsg( QStringLiteral( "ct: %1 => %2" ).arg( mTileLayer->boundingBoxes.at( i ).crs, mImageCrs ) );
|
|
|
|
try
|
|
{
|
|
QgsRectangle extent = ct.transformBoundingBox( mTileLayer->boundingBoxes.at( i ).box, QgsCoordinateTransform::ForwardTransform );
|
|
|
|
//make sure extent does not contain 'inf' or 'nan'
|
|
if ( extent.isFinite() )
|
|
{
|
|
mLayerExtent = extent;
|
|
break;
|
|
}
|
|
}
|
|
catch ( QgsCsException &cse )
|
|
{
|
|
Q_UNUSED( cse )
|
|
}
|
|
}
|
|
}
|
|
|
|
QgsDebugMsgLevel( "exiting with '" + mLayerExtent.toString() + "'.", 3 );
|
|
|
|
return true;
|
|
}
|
|
|
|
QgsDebugMsg( QStringLiteral( "no extent returned" ) );
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
bool firstLayer = true; //flag to know if a layer is the first to be successfully transformed
|
|
for ( QStringList::const_iterator it = mSettings.mActiveSubLayers.constBegin();
|
|
it != mSettings.mActiveSubLayers.constEnd();
|
|
++it )
|
|
{
|
|
QgsDebugMsg( "Sublayer iterator: " + *it );
|
|
|
|
QgsRectangle extent;
|
|
if ( !extentForNonTiledLayer( *it, mImageCrs, extent ) )
|
|
{
|
|
QgsDebugMsg( "extent for " + *it + " is invalid! (ignoring)" );
|
|
continue;
|
|
}
|
|
|
|
QgsDebugMsg( "extent for " + *it + " is " + extent.toString( 3 ) + '.' );
|
|
|
|
// add to the combined extent of all the active sublayers
|
|
if ( firstLayer )
|
|
{
|
|
mLayerExtent = extent;
|
|
}
|
|
else
|
|
{
|
|
mLayerExtent.combineExtentWith( extent );
|
|
}
|
|
|
|
firstLayer = false;
|
|
|
|
QgsDebugMsg( "combined extent is '" + mLayerExtent.toString()
|
|
+ "' after '" + ( *it ) + "'." );
|
|
|
|
}
|
|
|
|
QgsDebugMsg( "exiting with '" + mLayerExtent.toString() + "'." );
|
|
return true;
|
|
}
|
|
}
|
|
|
|
|
|
int QgsWmsProvider::capabilities() const
|
|
{
|
|
int capability = NoCapabilities;
|
|
bool canIdentify = false;
|
|
|
|
|
|
if ( mSettings.mTiled && mTileLayer )
|
|
{
|
|
QgsDebugMsgLevel( QStringLiteral( "Tiled." ), 2 );
|
|
canIdentify = !mTileLayer->getFeatureInfoURLs.isEmpty() || !getFeatureInfoUrl().isNull();
|
|
}
|
|
else
|
|
{
|
|
QgsDebugMsgLevel( QStringLiteral( "Not tiled." ), 2 );
|
|
// Test for the ability to use the Identify map tool
|
|
for ( QStringList::const_iterator it = mSettings.mActiveSubLayers.begin();
|
|
it != mSettings.mActiveSubLayers.end();
|
|
++it )
|
|
{
|
|
// Is sublayer visible?
|
|
if ( mActiveSubLayerVisibility.find( *it ).value() )
|
|
{
|
|
// Is sublayer queryable?
|
|
if ( mCaps.mQueryableForLayer.find( *it ).value() )
|
|
{
|
|
QgsDebugMsg( '\'' + ( *it ) + "' is queryable." );
|
|
canIdentify = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( canIdentify )
|
|
{
|
|
capability = mCaps.identifyCapabilities();
|
|
if ( capability )
|
|
{
|
|
capability |= Identify;
|
|
}
|
|
}
|
|
|
|
QgsDebugMsgLevel( QStringLiteral( "capability = %1" ).arg( capability ), 2 );
|
|
return capability;
|
|
}
|
|
|
|
QString QgsWmsProvider::layerMetadata( QgsWmsLayerProperty &layer )
|
|
{
|
|
QString metadata =
|
|
// Layer Properties section
|
|
// Use a nested table
|
|
QStringLiteral( "<tr><td>"
|
|
"<table width=\"100%\" class=\"tabular-view\">"
|
|
|
|
// Table header
|
|
"<tr><th class=\"strong\">" ) %
|
|
tr( "Property" ) %
|
|
QStringLiteral( "</th>"
|
|
"<th class=\"strong\">" ) %
|
|
tr( "Value" ) %
|
|
QStringLiteral( "</th></tr>"
|
|
|
|
// Name
|
|
"<tr><td>" ) %
|
|
tr( "Name" ) %
|
|
QStringLiteral( "</td>"
|
|
"<td>" ) %
|
|
layer.name %
|
|
QStringLiteral( "</td></tr>"
|
|
|
|
// Layer Visibility (as managed by this provider)
|
|
"<tr><td>" ) %
|
|
tr( "Visibility" ) %
|
|
QStringLiteral( "</td>"
|
|
"<td>" ) %
|
|
( mActiveSubLayerVisibility.find( layer.name ).value() ? tr( "Visible" ) : tr( "Hidden" ) ) %
|
|
QStringLiteral( "</td></tr>"
|
|
|
|
// Layer Title
|
|
"<tr><td>" ) %
|
|
tr( "Title" ) %
|
|
QStringLiteral( "</td>"
|
|
"<td>" ) %
|
|
layer.title %
|
|
QStringLiteral( "</td></tr>"
|
|
|
|
// Layer Metadata URL
|
|
"<tr><td>" ) %
|
|
tr( "Metadata URL" ) %
|
|
QStringLiteral( "</td>"
|
|
"<td>" ) %
|
|
layer.metadataUrl.onlineResource.xlinkHref;
|
|
|
|
QStringLiteral( "</td></tr>"
|
|
|
|
// Layer Abstract
|
|
"<tr><td>" ) %
|
|
tr( "Abstract" ) %
|
|
QStringLiteral( "</td>"
|
|
"<td>" ) %
|
|
layer.abstract;
|
|
QStringLiteral( "</td></tr>"
|
|
|
|
// Layer Queryability
|
|
"<tr><td>" ) %
|
|
tr( "Can Identify" ) %
|
|
QStringLiteral( "</td>"
|
|
"<td>" ) %
|
|
( layer.queryable ? tr( "Yes" ) : tr( "No" ) ) %
|
|
QStringLiteral( "</td></tr>"
|
|
|
|
// Layer Opacity
|
|
"<tr><td>" ) %
|
|
tr( "Can be Transparent" ) %
|
|
QStringLiteral( "</td>"
|
|
"<td>" ) %
|
|
( layer.opaque ? tr( "No" ) : tr( "Yes" ) ) %
|
|
QStringLiteral( "</td></tr>"
|
|
|
|
// Layer Subsetability
|
|
"<tr><td>" ) %
|
|
tr( "Can Zoom In" ) %
|
|
QStringLiteral( "</td>"
|
|
"<td>" ) %
|
|
( layer.noSubsets ? tr( "No" ) : tr( "Yes" ) ) %
|
|
QStringLiteral( "</td></tr>"
|
|
|
|
// Layer Server Cascade Count
|
|
"<tr><td>" ) %
|
|
tr( "Cascade Count" ) %
|
|
QStringLiteral( "</td>"
|
|
"<td>" ) %
|
|
QString::number( layer.cascaded );
|
|
QStringLiteral( "</td></tr>"
|
|
|
|
// Layer Fixed Width
|
|
"<tr><td>" ) %
|
|
tr( "Fixed Width" ) %
|
|
QStringLiteral( "</td>"
|
|
"<td>" ) %
|
|
QString::number( layer.fixedWidth );
|
|
QStringLiteral( "</td></tr>"
|
|
|
|
// Layer Fixed Height
|
|
"<tr><td>" ) %
|
|
tr( "Fixed Height" ) %
|
|
QStringLiteral( "</td>"
|
|
"<td>" ) %
|
|
QString::number( layer.fixedHeight ) %
|
|
QStringLiteral( "</td></tr>" );
|
|
|
|
// Layer Coordinate Reference Systems
|
|
for ( int j = 0; j < std::min( layer.crs.size(), 10 ); j++ )
|
|
{
|
|
metadata += QStringLiteral( "<tr><td>" ) %
|
|
tr( "Available in CRS" ) %
|
|
QStringLiteral( "</td>"
|
|
"<td>" ) %
|
|
layer.crs[j] %
|
|
QStringLiteral( "</td></tr>" );
|
|
}
|
|
|
|
if ( layer.crs.size() > 10 )
|
|
{
|
|
metadata += QStringLiteral( "<tr><td>" ) %
|
|
tr( "Available in CRS" ) %
|
|
QStringLiteral( "</td>"
|
|
"<td>" ) %
|
|
tr( "(and %n more)", "crs", layer.crs.size() - 10 ) %
|
|
QStringLiteral( "</td></tr>" );
|
|
}
|
|
|
|
// Layer Styles
|
|
for ( int j = 0; j < layer.style.size(); j++ )
|
|
{
|
|
const QgsWmsStyleProperty &style = layer.style.at( j );
|
|
|
|
metadata += QStringLiteral( "<tr><td>" ) %
|
|
tr( "Available in style" ) %
|
|
QStringLiteral( "</td>"
|
|
"<td>" ) %
|
|
|
|
// Nested table.
|
|
QStringLiteral( "<table width=\"100%\" class=\"tabular-view\">"
|
|
|
|
// Layer Style Name
|
|
"<tr><th class=\"strong\">" ) %
|
|
tr( "Name" ) %
|
|
QStringLiteral( "</th>"
|
|
"<td>" ) %
|
|
style.name %
|
|
QStringLiteral( "</td></tr>"
|
|
|
|
// Layer Style Title
|
|
"<tr><th class=\"strong\">" ) %
|
|
tr( "Title" ) %
|
|
QStringLiteral( "</th>"
|
|
"<td>" ) %
|
|
style.title %
|
|
QStringLiteral( "</td></tr>"
|
|
|
|
// Layer Style Abstract
|
|
"<tr><th class=\"strong\">" ) %
|
|
tr( "Abstract" ) %
|
|
QStringLiteral( "</th>"
|
|
"<td>" ) %
|
|
style.abstract %
|
|
QStringLiteral( "</td></tr>" );
|
|
|
|
// LegendURLs
|
|
if ( !style.legendUrl.isEmpty() )
|
|
{
|
|
metadata += QStringLiteral( "<tr><th class=\"strong\">" ) %
|
|
tr( "LegendURLs" ) %
|
|
QStringLiteral( "</th>"
|
|
"<td><table class=\"tabular-view\">"
|
|
"<tr><th>Format</th><th>URL</th></tr>" );
|
|
for ( int k = 0; k < style.legendUrl.size(); k++ )
|
|
{
|
|
const QgsWmsLegendUrlProperty &l = style.legendUrl[k];
|
|
metadata += QStringLiteral( "<tr><td>" ) % l.format % QStringLiteral( "</td><td>" ) % l.onlineResource.xlinkHref % QStringLiteral( "</td></tr>" );
|
|
}
|
|
metadata += QStringLiteral( "</table></td></tr>" );
|
|
}
|
|
|
|
// Close the nested table
|
|
metadata += QStringLiteral( "</table>"
|
|
"</td></tr>" );
|
|
}
|
|
|
|
// Close the nested table
|
|
metadata += QStringLiteral( "</table>"
|
|
"</td></tr>" );
|
|
return metadata;
|
|
}
|
|
|
|
QString QgsWmsProvider::htmlMetadata()
|
|
{
|
|
QString metadata;
|
|
|
|
metadata += QStringLiteral( "<tr><td class=\"highlight\">" ) % tr( "WMS Info" ) % QStringLiteral( "</td><td><div>" );
|
|
|
|
if ( !mSettings.mTiled )
|
|
{
|
|
metadata += QStringLiteral( " <a href=\"\" onclick=\"document.getElementById('selectedlayers').scrollIntoView(); return false;\">" ) %
|
|
tr( "Selected Layers" ) %
|
|
QStringLiteral( "</a> <a href=\"\" onclick=\"document.getElementById('otherlayers').scrollIntoView(); return false;\">" ) %
|
|
tr( "Other Layers" ) %
|
|
QStringLiteral( "</a>" );
|
|
}
|
|
else
|
|
{
|
|
metadata += QStringLiteral( " <a href=\"\" onclick=\"document.getElementById('tilesetproperties').scrollIntoView(); return false;\">" ) %
|
|
tr( "Tile Layer Properties" ) %
|
|
QStringLiteral( "</a> "
|
|
" <a href=\"\" onclick=\"document.getElementById('cachestats'); return false;\">" ) %
|
|
tr( "Cache Stats" ) %
|
|
QStringLiteral( "</a> " );
|
|
}
|
|
|
|
metadata += QStringLiteral( "<br /><table class=\"tabular-view\">" // Nested table 1
|
|
// Server Properties section
|
|
"<tr><th class=\"strong\" id=\"serverproperties\">" ) %
|
|
tr( "Server Properties" ) %
|
|
QStringLiteral( "</th></tr>"
|
|
|
|
// Use a nested table
|
|
"<tr><td>"
|
|
"<table width=\"100%\" class=\"tabular-view\">" ); // Nested table 2
|
|
|
|
// Table header
|
|
metadata += QStringLiteral( "<tr><th class=\"strong\">" ) %
|
|
tr( "Property" ) %
|
|
QStringLiteral( "</th>"
|
|
"<th class=\"strong\">" ) %
|
|
tr( "Value" ) %
|
|
QStringLiteral( "</th></tr>" );
|
|
|
|
// WMS Version
|
|
metadata += QStringLiteral( "<tr><td>" ) %
|
|
tr( "WMS Version" ) %
|
|
QStringLiteral( "</td>"
|
|
"<td>" ) %
|
|
mCaps.mCapabilities.version %
|
|
QStringLiteral( "</td></tr>" );
|
|
|
|
// Service Title
|
|
metadata += QStringLiteral( "<tr><td>" ) %
|
|
tr( "Title" ) %
|
|
QStringLiteral( "</td>"
|
|
"<td>" ) %
|
|
mCaps.mCapabilities.service.title %
|
|
QStringLiteral( "</td></tr>" );
|
|
|
|
// Service Abstract
|
|
metadata += QStringLiteral( "<tr><td>" ) %
|
|
tr( "Abstract" ) %
|
|
QStringLiteral( "</td>"
|
|
"<td>" ) %
|
|
mCaps.mCapabilities.service.abstract %
|
|
QStringLiteral( "</td></tr>" );
|
|
|
|
// Service Keywords
|
|
metadata += QStringLiteral( "<tr><td>" ) %
|
|
tr( "Keywords" ) %
|
|
QStringLiteral( "</td>"
|
|
"<td>" ) %
|
|
mCaps.mCapabilities.service.keywordList.join( QStringLiteral( "<br />" ) ) %
|
|
QStringLiteral( "</td></tr>" );
|
|
|
|
// Service Online Resource
|
|
metadata += QStringLiteral( "<tr><td>" ) %
|
|
tr( "Online Resource" ) %
|
|
QStringLiteral( "</td>"
|
|
"<td>" ) %
|
|
'-' %
|
|
QStringLiteral( "</td></tr>" );
|
|
|
|
// Service Contact Information
|
|
metadata += QStringLiteral( "<tr><td>" ) %
|
|
tr( "Contact Person" ) %
|
|
QStringLiteral( "</td>"
|
|
"<td>" ) %
|
|
mCaps.mCapabilities.service.contactInformation.contactPersonPrimary.contactPerson %
|
|
QStringLiteral( "<br />" ) %
|
|
mCaps.mCapabilities.service.contactInformation.contactPosition %
|
|
QStringLiteral( "<br />" ) %
|
|
mCaps.mCapabilities.service.contactInformation.contactPersonPrimary.contactOrganization %
|
|
QStringLiteral( "</td></tr>" );
|
|
|
|
// Service Fees
|
|
metadata += QStringLiteral( "<tr><td>" ) %
|
|
tr( "Fees" ) %
|
|
QStringLiteral( "</td>"
|
|
"<td>" ) %
|
|
mCaps.mCapabilities.service.fees %
|
|
QStringLiteral( "</td></tr>" );
|
|
|
|
// Service Access Constraints
|
|
metadata += QStringLiteral( "<tr><td>" ) %
|
|
tr( "Access Constraints" ) %
|
|
QStringLiteral( "</td>"
|
|
"<td>" ) %
|
|
mCaps.mCapabilities.service.accessConstraints %
|
|
QStringLiteral( "</td></tr>" );
|
|
|
|
// Base URL
|
|
metadata += QStringLiteral( "<tr><td>" ) %
|
|
tr( "GetCapabilitiesUrl" ) %
|
|
QStringLiteral( "</td>"
|
|
"<td>" ) %
|
|
mSettings.mBaseUrl %
|
|
QStringLiteral( "</td></tr>" );
|
|
|
|
metadata += QStringLiteral( "<tr><td>" ) %
|
|
tr( "GetMapUrl" ) %
|
|
QStringLiteral( "</td>"
|
|
"<td>" ) %
|
|
getMapUrl() % ( mSettings.mIgnoreGetMapUrl ? tr( " <font color=\"red\">(advertised but ignored)</font>" ) : QString() ) %
|
|
QStringLiteral( "</td></tr>" );
|
|
|
|
metadata += QStringLiteral( "<tr><td>" ) %
|
|
tr( "GetFeatureInfoUrl" ) %
|
|
QStringLiteral( "</td>"
|
|
"<td>" ) %
|
|
getFeatureInfoUrl() % ( mSettings.mIgnoreGetFeatureInfoUrl ? tr( " <font color=\"red\">(advertised but ignored)</font>" ) : QString() ) %
|
|
QStringLiteral( "</td></tr>" );
|
|
|
|
metadata += QStringLiteral( "<tr><td>" ) %
|
|
tr( "GetLegendGraphic" ) %
|
|
QStringLiteral( "</td>"
|
|
"<td>" ) %
|
|
getLegendGraphicUrl() % ( mSettings.mIgnoreGetMapUrl ? tr( " <font color=\"red\">(advertised but ignored)</font>" ) : QString() ) %
|
|
QStringLiteral( "</td></tr>" );
|
|
|
|
if ( mSettings.mTiled )
|
|
{
|
|
metadata += QStringLiteral( "<tr><td>" ) %
|
|
tr( "Tile Layer Count" ) %
|
|
QStringLiteral( "</td>"
|
|
"<td>" ) %
|
|
QString::number( mCaps.mTileLayersSupported.size() ) %
|
|
QStringLiteral( "</td></tr>"
|
|
"<tr><td>" ) %
|
|
tr( "GetTileUrl" ) %
|
|
QStringLiteral( "</td>"
|
|
"<td>" ) %
|
|
getTileUrl() %
|
|
QStringLiteral( "</td></tr>" );
|
|
|
|
if ( mTileLayer )
|
|
{
|
|
metadata += QStringLiteral( "<tr><td>" ) %
|
|
tr( "Tile templates" ) %
|
|
QStringLiteral( "</td>"
|
|
"<td>" );
|
|
for ( QHash<QString, QString>::const_iterator it = mTileLayer->getTileURLs.constBegin();
|
|
it != mTileLayer->getTileURLs.constEnd();
|
|
++it )
|
|
{
|
|
metadata += QStringLiteral( "%1:%2<br>" ).arg( it.key(), it.value() );
|
|
}
|
|
metadata += QStringLiteral( "</td></tr>"
|
|
|
|
"<tr><td>" ) %
|
|
tr( "FeatureInfo templates" ) %
|
|
QStringLiteral( "</td>"
|
|
"<td>" );
|
|
for ( QHash<QString, QString>::const_iterator it = mTileLayer->getFeatureInfoURLs.constBegin();
|
|
it != mTileLayer->getFeatureInfoURLs.constEnd();
|
|
++it )
|
|
{
|
|
metadata += QStringLiteral( "%1:%2<br>" ).arg( it.key(), it.value() );
|
|
}
|
|
metadata += QStringLiteral( "</td></tr>" );
|
|
}
|
|
|
|
// GetFeatureInfo Request Formats
|
|
metadata += QStringLiteral( "<tr><td>" ) %
|
|
tr( "Identify Formats" ) %
|
|
QStringLiteral( "</td>"
|
|
"<td>" ) %
|
|
mTileLayer->infoFormats.join( QStringLiteral( "<br />" ) ) %
|
|
QStringLiteral( "</td></tr>" );
|
|
}
|
|
else
|
|
{
|
|
// GetMap Request Formats
|
|
metadata += QStringLiteral( "<tr><td>" ) %
|
|
tr( "Image Formats" ) %
|
|
QStringLiteral( "</td>"
|
|
"<td>" ) %
|
|
mCaps.mCapabilities.capability.request.getMap.format.join( QStringLiteral( "<br />" ) ) %
|
|
QStringLiteral( "</td></tr>"
|
|
|
|
// GetFeatureInfo Request Formats
|
|
"<tr><td>" ) %
|
|
tr( "Identify Formats" ) %
|
|
QStringLiteral( "</td>"
|
|
"<td>" ) %
|
|
mCaps.mCapabilities.capability.request.getFeatureInfo.format.join( QStringLiteral( "<br />" ) ) %
|
|
QStringLiteral( "</td></tr>"
|
|
|
|
// Layer Count (as managed by this provider)
|
|
"<tr><td>" ) %
|
|
tr( "Layer Count" ) %
|
|
QStringLiteral( "</td>"
|
|
"<td>" ) %
|
|
QString::number( mCaps.mLayersSupported.size() ) %
|
|
QStringLiteral( "</td></tr>" );
|
|
}
|
|
|
|
// Close the nested table 2
|
|
metadata += QStringLiteral( "</table>"
|
|
"</td></tr>" );
|
|
|
|
// Layer properties
|
|
if ( !mSettings.mTiled )
|
|
{
|
|
metadata += QStringLiteral( "<tr><th class=\"strong\" id=\"selectedlayers\">" ) %
|
|
tr( "Selected Layers" ) %
|
|
QStringLiteral( "</th></tr>" );
|
|
|
|
int n = 0;
|
|
for ( int i = 0; i < mCaps.mLayersSupported.size(); i++ )
|
|
{
|
|
if ( mSettings.mActiveSubLayers.contains( mCaps.mLayersSupported.at( i ).name ) )
|
|
{
|
|
metadata += layerMetadata( mCaps.mLayersSupported[i] );
|
|
n++;
|
|
}
|
|
} // for each layer
|
|
|
|
// Layer properties
|
|
if ( n < mCaps.mLayersSupported.size() )
|
|
{
|
|
metadata += QStringLiteral( "<tr><th class=\"strong\" id=\"otherlayers\">" ) %
|
|
tr( "Other Layers" ) %
|
|
QStringLiteral( "</th></tr>" );
|
|
|
|
for ( int i = 0; i < mCaps.mLayersSupported.size(); i++ )
|
|
{
|
|
if ( !mSettings.mActiveSubLayers.contains( mCaps.mLayersSupported[i].name ) )
|
|
{
|
|
metadata += layerMetadata( mCaps.mLayersSupported[i] );
|
|
}
|
|
} // for each layer
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Tileset properties
|
|
metadata += QStringLiteral( "<tr><th class=\"strong\" id=\"tilesetproperties\">" ) %
|
|
tr( "Tileset Properties" ) %
|
|
QStringLiteral( "</th></tr>"
|
|
|
|
// Iterate through tilesets
|
|
"<tr><td>"
|
|
|
|
"<table width=\"100%\" class=\"tabular-view\">" ); // Nested table 3
|
|
|
|
for ( const QgsWmtsTileLayer &l : qgis::as_const( mCaps.mTileLayersSupported ) )
|
|
{
|
|
metadata += QStringLiteral( "<tr><th class=\"strong\">" ) %
|
|
tr( "Identifier" ) %
|
|
QStringLiteral( "</th><th class=\"strong\">" ) %
|
|
tr( "Tile mode" ) %
|
|
QStringLiteral( "</th></tr>"
|
|
|
|
"<tr><td>" ) %
|
|
l.identifier %
|
|
QStringLiteral( "</td><td class=\"strong\">" );
|
|
|
|
if ( l.tileMode == WMTS )
|
|
{
|
|
metadata += tr( "WMTS" );
|
|
}
|
|
else if ( l.tileMode == WMSC )
|
|
{
|
|
metadata += tr( "WMS-C" );
|
|
}
|
|
else if ( l.tileMode == XYZ )
|
|
{
|
|
metadata += tr( "XYZ" );
|
|
}
|
|
else
|
|
{
|
|
metadata += tr( "Invalid tile mode" );
|
|
}
|
|
|
|
metadata += QStringLiteral( "</td></tr>"
|
|
|
|
// Table header
|
|
"<tr><th class=\"strong\">" ) %
|
|
tr( "Property" ) %
|
|
QStringLiteral( "</th>"
|
|
"<th class=\"strong\">" ) %
|
|
tr( "Value" ) %
|
|
QStringLiteral( "</th></tr>"
|
|
|
|
"<tr><td class=\"strong\">" ) %
|
|
tr( "Title" ) %
|
|
QStringLiteral( "</td>"
|
|
"<td>" ) %
|
|
l.title %
|
|
QStringLiteral( "</td></tr>"
|
|
|
|
"<tr><td class=\"strong\">" ) %
|
|
tr( "Abstract" ) %
|
|
QStringLiteral( "</td>"
|
|
"<td>" ) %
|
|
l.abstract %
|
|
QStringLiteral( "</td></tr>"
|
|
|
|
"<tr><td class=\"strong\">" ) %
|
|
tr( "Selected" ) %
|
|
QStringLiteral( "</td>"
|
|
"<td class=\"strong\">" ) %
|
|
( l.identifier == mSettings.mActiveSubLayers.join( QStringLiteral( "," ) ) ? tr( "Yes" ) : tr( "No" ) ) %
|
|
QStringLiteral( "</td></tr>" );
|
|
|
|
if ( !l.styles.isEmpty() )
|
|
{
|
|
metadata += QStringLiteral( "<tr><td class=\"strong\">" ) %
|
|
tr( "Available Styles" ) %
|
|
QStringLiteral( "</td>"
|
|
"<td class=\"strong\">" );
|
|
QStringList styles;
|
|
for ( const QgsWmtsStyle &style : qgis::as_const( l.styles ) )
|
|
{
|
|
styles << style.identifier;
|
|
}
|
|
metadata += styles.join( QStringLiteral( ", " ) ) %
|
|
QStringLiteral( "</td></tr>" );
|
|
}
|
|
|
|
metadata += QStringLiteral( "<tr><td class=\"strong\">" ) %
|
|
tr( "CRS" ) %
|
|
QStringLiteral( "</td>"
|
|
"<td>"
|
|
"<table class=\"tabular-view\"><tr>" // Nested table 4
|
|
"<td class=\"strong\">" ) %
|
|
tr( "CRS" ) %
|
|
QStringLiteral( "</td>"
|
|
"<td class=\"strong\">" ) %
|
|
tr( "Bounding Box" ) %
|
|
QStringLiteral( "</td>" );
|
|
for ( int i = 0; i < l.boundingBoxes.size(); i++ )
|
|
{
|
|
metadata += QStringLiteral( "<tr><td>" ) %
|
|
l.boundingBoxes[i].crs %
|
|
QStringLiteral( "</td><td>" ) %
|
|
l.boundingBoxes[i].box.toString() %
|
|
QStringLiteral( "</td></tr>" );
|
|
}
|
|
metadata += QStringLiteral( "</table></td></tr>" // End nested table 4
|
|
"<tr><td class=\"strong\">" ) %
|
|
tr( "Available Tilesets" ) %
|
|
QStringLiteral( "</td><td class=\"strong\">" );
|
|
|
|
for ( const QgsWmtsTileMatrixSetLink &setLink : qgis::as_const( l.setLinks ) )
|
|
{
|
|
metadata += setLink.tileMatrixSet + "<br>";
|
|
}
|
|
|
|
metadata += QStringLiteral( "</td></tr>" );
|
|
}
|
|
|
|
metadata += QStringLiteral( "</table></td></tr>" ); // End nested table 3
|
|
|
|
if ( mTileMatrixSet )
|
|
{
|
|
// Iterate through tilesets
|
|
metadata += QStringLiteral( "<tr><td><table width=\"100%\" class=\"tabular-view\">" // Nested table 3
|
|
|
|
"<tr><th colspan=14 class=\"strong\">%1 %2</th></tr>"
|
|
"<tr>"
|
|
"<th rowspan=2 class=\"strong\">%3</th>"
|
|
"<th colspan=2 class=\"strong\">%4</th>"
|
|
"<th colspan=2 class=\"strong\">%5</th>"
|
|
"<th colspan=2 class=\"strong\">%6</th>"
|
|
"<th colspan=2 class=\"strong\">%7</th>"
|
|
"<th colspan=4 class=\"strong\">%8</th>"
|
|
"</tr><tr>"
|
|
"<th class=\"strong\">%9</th><th class=\"strong\">%10</th>"
|
|
"<th class=\"strong\">%9</th><th class=\"strong\">%10</th>"
|
|
"<th class=\"strong\">%9</th><th class=\"strong\">%10</th>"
|
|
"<th class=\"strong\">%9</th><th class=\"strong\">%10</th>"
|
|
"<th class=\"strong\">%11</th>"
|
|
"<th class=\"strong\">%12</th>"
|
|
"<th class=\"strong\">%13</th>"
|
|
"<th class=\"strong\">%14</th>"
|
|
"</tr>" )
|
|
.arg( tr( "Selected tile matrix set " ),
|
|
mSettings.mTileMatrixSetId,
|
|
tr( "Scale" ),
|
|
tr( "Tile size [px]" ),
|
|
tr( "Tile size [mu]" ),
|
|
tr( "Matrix size" ),
|
|
tr( "Matrix extent [mu]" ) )
|
|
.arg( tr( "Bounds" ),
|
|
tr( "Width" ),
|
|
tr( "Height" ),
|
|
tr( "Top" ),
|
|
tr( "Left" ),
|
|
tr( "Bottom" ),
|
|
tr( "Right" ) );
|
|
|
|
for ( const double key : mNativeResolutions )
|
|
{
|
|
QgsWmtsTileMatrix &tm = mTileMatrixSet->tileMatrices[ key ];
|
|
|
|
double tw = key * tm.tileWidth;
|
|
double th = key * tm.tileHeight;
|
|
|
|
QgsRectangle r( tm.topLeft.x(), tm.topLeft.y() - tw * tm.matrixWidth, tm.topLeft.x() + th * tm.matrixHeight, tm.topLeft.y() );
|
|
|
|
metadata += QStringLiteral( "<tr>"
|
|
"<td>%1</td>"
|
|
"<td>%2</td><td>%3</td>"
|
|
"<td>%4</td><td>%5</td>"
|
|
"<td>%6</td><td>%7</td>"
|
|
"<td>%8</td><td>%9</td>" )
|
|
.arg( tm.scaleDenom )
|
|
.arg( tm.tileWidth ).arg( tm.tileHeight )
|
|
.arg( tw ).arg( th )
|
|
.arg( tm.matrixWidth ).arg( tm.matrixHeight )
|
|
.arg( tw * tm.matrixWidth, 0, 'f' )
|
|
.arg( th * tm.matrixHeight, 0, 'f' );
|
|
|
|
// top
|
|
if ( mLayerExtent.yMaximum() > r.yMaximum() )
|
|
{
|
|
metadata += QStringLiteral( "<td title=\"%1<br>%2\"><font color=\"red\">%3</font></td>" )
|
|
.arg( tr( "%n missing row(s)", nullptr, ( int ) std::ceil( ( mLayerExtent.yMaximum() - r.yMaximum() ) / th ) ),
|
|
tr( "Layer's upper bound: %1" ).arg( mLayerExtent.yMaximum(), 0, 'f' ) )
|
|
.arg( r.yMaximum(), 0, 'f' );
|
|
}
|
|
else
|
|
{
|
|
metadata += QStringLiteral( "<td>%1</td>" ).arg( r.yMaximum(), 0, 'f' );
|
|
}
|
|
|
|
// left
|
|
if ( mLayerExtent.xMinimum() < r.xMinimum() )
|
|
{
|
|
metadata += QStringLiteral( "<td title=\"%1<br>%2\"><font color=\"red\">%3</font></td>" )
|
|
.arg( tr( "%n missing column(s)", nullptr, ( int ) std::ceil( ( r.xMinimum() - mLayerExtent.xMinimum() ) / tw ) ),
|
|
tr( "Layer's left bound: %1" ).arg( mLayerExtent.xMinimum(), 0, 'f' ) )
|
|
.arg( r.xMinimum(), 0, 'f' );
|
|
}
|
|
else
|
|
{
|
|
metadata += QStringLiteral( "<td>%1</td>" ).arg( r.xMinimum(), 0, 'f' );
|
|
}
|
|
|
|
// bottom
|
|
if ( mLayerExtent.yMaximum() > r.yMaximum() )
|
|
{
|
|
metadata += QStringLiteral( "<td title=\"%1<br>%2\"><font color=\"red\">%3</font></td>" )
|
|
.arg( tr( "%n missing row(s)", nullptr, ( int ) std::ceil( ( mLayerExtent.yMaximum() - r.yMaximum() ) / th ) ),
|
|
tr( "Layer's lower bound: %1" ).arg( mLayerExtent.yMaximum(), 0, 'f' ) )
|
|
.arg( r.yMaximum(), 0, 'f' );
|
|
}
|
|
else
|
|
{
|
|
metadata += QStringLiteral( "<td>%1</td>" ).arg( r.yMaximum(), 0, 'f' );
|
|
}
|
|
|
|
// right
|
|
if ( mLayerExtent.xMaximum() > r.xMaximum() )
|
|
{
|
|
metadata += QStringLiteral( "<td title=\"%1<br>%2\"><font color=\"red\">%3</font></td>" )
|
|
.arg( tr( "%n missing column(s)", nullptr, ( int ) std::ceil( ( mLayerExtent.xMaximum() - r.xMaximum() ) / tw ) ),
|
|
tr( "Layer's right bound: %1" ).arg( mLayerExtent.xMaximum(), 0, 'f' ) )
|
|
.arg( r.xMaximum(), 0, 'f' );
|
|
}
|
|
else
|
|
{
|
|
metadata += QStringLiteral( "<td>%1</td>" ).arg( r.xMaximum(), 0, 'f' );
|
|
}
|
|
|
|
metadata += QStringLiteral( "</tr>" );
|
|
}
|
|
|
|
metadata += QStringLiteral( "</table></td></tr>" ); // End nested table 3
|
|
}
|
|
|
|
const QgsWmsStatistics::Stat &stat = QgsWmsStatistics::statForUri( dataSourceUri() );
|
|
|
|
metadata += QStringLiteral( "<tr><th class=\"strong\" id=\"cachestats\">" ) %
|
|
tr( "Cache stats" ) %
|
|
QStringLiteral( "</th></tr>"
|
|
|
|
"<tr><td><table width=\"100%\" class=\"tabular-view\">" // Nested table 3
|
|
|
|
"<tr><th class=\"strong\">" ) %
|
|
tr( "Property" ) %
|
|
QStringLiteral( "</th>"
|
|
"<th class=\"strong\">" ) %
|
|
tr( "Value" ) %
|
|
QStringLiteral( "</th></tr>"
|
|
|
|
"<tr><td>" ) %
|
|
tr( "Hits" ) %
|
|
QStringLiteral( "</td><td>" ) %
|
|
QString::number( stat.cacheHits ) %
|
|
QStringLiteral( "</td></tr>"
|
|
|
|
"<tr><td>" ) %
|
|
tr( "Misses" ) %
|
|
QStringLiteral( "</td><td>" ) %
|
|
QString::number( stat.cacheMisses ) %
|
|
QStringLiteral( "</td></tr>"
|
|
|
|
"<tr><td>" ) %
|
|
tr( "Errors" ) %
|
|
QStringLiteral( "</td><td>" ) %
|
|
QString::number( stat.errors ) %
|
|
QStringLiteral( "</td></tr>"
|
|
|
|
"</table></td></tr>" ); // End nested table 3
|
|
}
|
|
|
|
metadata += QStringLiteral( "</table>" // End nested table 2
|
|
"</table></div></td></tr>\n" ); // End nested table 1
|
|
|
|
return metadata;
|
|
}
|
|
|
|
QgsRasterIdentifyResult QgsWmsProvider::identify( const QgsPointXY &point, QgsRaster::IdentifyFormat format, const QgsRectangle &boundingBox, int width, int height, int /*dpi*/ )
|
|
{
|
|
QgsDebugMsg( QStringLiteral( "format = %1" ).arg( format ) );
|
|
|
|
QString formatStr;
|
|
formatStr = mCaps.mIdentifyFormats.value( format );
|
|
if ( formatStr.isEmpty() )
|
|
{
|
|
return QgsRasterIdentifyResult( QGS_ERROR( tr( "Format not supported" ) ) );
|
|
}
|
|
|
|
QgsDebugMsg( QStringLiteral( "format = %1 format = %2" ).arg( format ).arg( formatStr ) );
|
|
|
|
QMap<int, QVariant> results;
|
|
if ( !extent().contains( point ) )
|
|
{
|
|
results.insert( 1, "" );
|
|
return QgsRasterIdentifyResult( format, results );
|
|
}
|
|
|
|
QgsRectangle myExtent = boundingBox;
|
|
|
|
if ( !myExtent.isEmpty() )
|
|
{
|
|
// we cannot reliably identify WMS if boundingBox is specified but width or theHeight
|
|
// are not, because we don't know original resolution
|
|
if ( width == 0 || height == 0 )
|
|
{
|
|
return QgsRasterIdentifyResult( QGS_ERROR( tr( "Context not fully specified (extent was defined but width and/or height was not)." ) ) );
|
|
}
|
|
}
|
|
else // context (boundingBox, width, height) not defined
|
|
{
|
|
// We don't know original source resolution, so we take some small extent around the point.
|
|
|
|
// Warning: this does not work well with poin/line vector layers where search rectangle
|
|
// is based on pixel size (e.g. UMN Mapserver is using TOLERANCE layer param)
|
|
|
|
double xRes = 0.001; // expecting meters
|
|
|
|
// TODO: add CRS as class member
|
|
QgsCoordinateReferenceSystem crs = QgsCoordinateReferenceSystem::fromOgcWmsCrs( mImageCrs );
|
|
if ( crs.isValid() )
|
|
{
|
|
// set resolution approximately to 1mm
|
|
switch ( crs.mapUnits() )
|
|
{
|
|
case QgsUnitTypes::DistanceMeters:
|
|
xRes = 0.001;
|
|
break;
|
|
case QgsUnitTypes::DistanceFeet:
|
|
xRes = 0.003;
|
|
break;
|
|
case QgsUnitTypes::DistanceDegrees:
|
|
// max length of degree of latitude on pole is 111694 m
|
|
xRes = 1e-8;
|
|
break;
|
|
default:
|
|
xRes = 0.001; // expecting meters
|
|
}
|
|
}
|
|
|
|
// Keep resolution in both axis equal! Otherwise silly server (like QGIS mapserver)
|
|
// fail to calculate coordinate because it is using single resolution average!!!
|
|
double yRes = xRes;
|
|
|
|
// 1x1 should be sufficient but at least we know that GDAL ECW was very unefficient
|
|
// so we use 2x2 (until we find that it is too small for some server)
|
|
width = height = 2;
|
|
|
|
myExtent = QgsRectangle( point.x() - xRes, point.y() - yRes,
|
|
point.x() + xRes, point.y() + yRes );
|
|
}
|
|
|
|
// Point in BBOX/WIDTH/HEIGHT coordinates
|
|
// No need to fiddle with extent origin not covered by layer extent, I believe
|
|
double xRes = myExtent.width() / width;
|
|
double yRes = myExtent.height() / height;
|
|
|
|
|
|
// Mapserver (6.0.3, for example) does not seem to work with 1x1 pixel box
|
|
// (seems to be a different issue, not the slownes of GDAL with ECW mentioned above)
|
|
// so we have to enlarge it a bit
|
|
if ( width == 1 )
|
|
{
|
|
width += 1;
|
|
myExtent.setXMaximum( myExtent.xMaximum() + xRes );
|
|
}
|
|
|
|
if ( height == 1 )
|
|
{
|
|
height += 1;
|
|
myExtent.setYMaximum( myExtent.yMaximum() + yRes );
|
|
}
|
|
|
|
QgsDebugMsg( "myExtent = " + myExtent.toString() );
|
|
QgsDebugMsg( QStringLiteral( "theWidth = %1 height = %2" ).arg( width ).arg( height ) );
|
|
QgsDebugMsg( QStringLiteral( "xRes = %1 yRes = %2" ).arg( xRes ).arg( yRes ) );
|
|
|
|
QgsPointXY finalPoint;
|
|
finalPoint.setX( std::floor( ( point.x() - myExtent.xMinimum() ) / xRes ) );
|
|
finalPoint.setY( std::floor( ( myExtent.yMaximum() - point.y() ) / yRes ) );
|
|
|
|
QgsDebugMsg( QStringLiteral( "point = %1 %2" ).arg( finalPoint.x() ).arg( finalPoint.y() ) );
|
|
|
|
QgsDebugMsg( QStringLiteral( "recalculated orig point (corner) = %1 %2" ).arg( myExtent.xMinimum() + finalPoint.x()*xRes ).arg( myExtent.yMaximum() - finalPoint.y()*yRes ) );
|
|
|
|
// Collect which layers to query on
|
|
//according to the WMS spec for 1.3, the order of x - and y - coordinates is inverted for geographical CRS
|
|
bool changeXY = mCaps.shouldInvertAxisOrientation( mImageCrs );
|
|
|
|
// compose the URL query string for the WMS server.
|
|
QString crsKey = QStringLiteral( "SRS" ); //SRS in 1.1.1 and CRS in 1.3.0
|
|
if ( mCaps.mCapabilities.version == QLatin1String( "1.3.0" ) || mCaps.mCapabilities.version == QLatin1String( "1.3" ) )
|
|
{
|
|
crsKey = QStringLiteral( "CRS" );
|
|
}
|
|
|
|
// Compose request to WMS server
|
|
QString bbox = QString( changeXY ? "%2,%1,%4,%3" : "%1,%2,%3,%4" )
|
|
.arg( qgsDoubleToString( myExtent.xMinimum() ),
|
|
qgsDoubleToString( myExtent.yMinimum() ),
|
|
qgsDoubleToString( myExtent.xMaximum() ),
|
|
qgsDoubleToString( myExtent.yMaximum() ) );
|
|
|
|
//QgsFeatureList featureList;
|
|
|
|
QList<QUrl> urls;
|
|
QStringList layerList;
|
|
|
|
if ( !mSettings.mTiled )
|
|
{
|
|
// Test for which layers are suitable for querying with
|
|
for ( QStringList::const_iterator
|
|
layers = mSettings.mActiveSubLayers.constBegin(),
|
|
styles = mSettings.mActiveSubStyles.constBegin();
|
|
layers != mSettings.mActiveSubLayers.constEnd();
|
|
++layers, ++styles )
|
|
{
|
|
// Is sublayer visible?
|
|
if ( !mActiveSubLayerVisibility.find( *layers ).value() )
|
|
{
|
|
// TODO: something better?
|
|
// we need to keep all sublayers so that we can get their names in identify tool
|
|
results.insert( urls.size(), false );
|
|
continue;
|
|
}
|
|
|
|
|
|
// Is sublayer queryable?
|
|
if ( !mCaps.mQueryableForLayer.find( *layers ).value() )
|
|
{
|
|
results.insert( urls.size(), false );
|
|
continue;
|
|
}
|
|
|
|
QgsDebugMsg( "Layer '" + *layers + "' is queryable." );
|
|
|
|
QUrl requestUrl( mSettings.mIgnoreGetFeatureInfoUrl ? mSettings.mBaseUrl : getFeatureInfoUrl() );
|
|
setQueryItem( requestUrl, QStringLiteral( "SERVICE" ), QStringLiteral( "WMS" ) );
|
|
setQueryItem( requestUrl, QStringLiteral( "VERSION" ), mCaps.mCapabilities.version );
|
|
setQueryItem( requestUrl, QStringLiteral( "REQUEST" ), QStringLiteral( "GetFeatureInfo" ) );
|
|
setQueryItem( requestUrl, QStringLiteral( "BBOX" ), bbox );
|
|
setSRSQueryItem( requestUrl );
|
|
setQueryItem( requestUrl, QStringLiteral( "WIDTH" ), QString::number( width ) );
|
|
setQueryItem( requestUrl, QStringLiteral( "HEIGHT" ), QString::number( height ) );
|
|
setQueryItem( requestUrl, QStringLiteral( "LAYERS" ), *layers );
|
|
setQueryItem( requestUrl, QStringLiteral( "STYLES" ), *styles );
|
|
setFormatQueryItem( requestUrl );
|
|
setQueryItem( requestUrl, QStringLiteral( "QUERY_LAYERS" ), *layers );
|
|
setQueryItem( requestUrl, QStringLiteral( "INFO_FORMAT" ), formatStr );
|
|
|
|
if ( mCaps.mCapabilities.version == QLatin1String( "1.3.0" ) || mCaps.mCapabilities.version == QLatin1String( "1.3" ) )
|
|
{
|
|
setQueryItem( requestUrl, QStringLiteral( "I" ), QString::number( finalPoint.x() ) );
|
|
setQueryItem( requestUrl, QStringLiteral( "J" ), QString::number( finalPoint.y() ) );
|
|
}
|
|
else
|
|
{
|
|
setQueryItem( requestUrl, QStringLiteral( "X" ), QString::number( finalPoint.x() ) );
|
|
setQueryItem( requestUrl, QStringLiteral( "Y" ), QString::number( finalPoint.y() ) );
|
|
}
|
|
|
|
if ( mSettings.mFeatureCount > 0 )
|
|
{
|
|
setQueryItem( requestUrl, QStringLiteral( "FEATURE_COUNT" ), QString::number( mSettings.mFeatureCount ) );
|
|
}
|
|
|
|
layerList << *layers;
|
|
urls << requestUrl;
|
|
}
|
|
}
|
|
else if ( mTileLayer && mTileLayer->tileMode == WMTS )
|
|
{
|
|
// WMTS FeatureInfo
|
|
double vres = boundingBox.width() / width;
|
|
double tres = vres;
|
|
|
|
const QgsWmtsTileMatrix *tm = nullptr;
|
|
|
|
Q_ASSERT( mTileMatrixSet );
|
|
Q_ASSERT( !mTileMatrixSet->tileMatrices.isEmpty() );
|
|
|
|
QMap<double, QgsWmtsTileMatrix> &m = mTileMatrixSet->tileMatrices;
|
|
|
|
// find nearest resolution
|
|
QMap<double, QgsWmtsTileMatrix>::const_iterator prev, it = m.constBegin();
|
|
while ( it != m.constEnd() && it.key() < vres )
|
|
{
|
|
QgsDebugMsg( QStringLiteral( "res:%1 >= %2" ).arg( it.key() ).arg( vres ) );
|
|
prev = it;
|
|
++it;
|
|
}
|
|
|
|
if ( it == m.constEnd() ||
|
|
( it != m.constBegin() && vres - prev.key() < it.key() - vres ) )
|
|
{
|
|
QgsDebugMsg( QStringLiteral( "back to previous res" ) );
|
|
it = prev;
|
|
}
|
|
|
|
tres = it.key();
|
|
tm = &it.value();
|
|
|
|
QgsDebugMsg( QStringLiteral( "layer extent: %1,%2 %3x%4" )
|
|
.arg( qgsDoubleToString( mLayerExtent.xMinimum() ),
|
|
qgsDoubleToString( mLayerExtent.yMinimum() ) )
|
|
.arg( mLayerExtent.width() )
|
|
.arg( mLayerExtent.height() )
|
|
);
|
|
|
|
QgsDebugMsg( QStringLiteral( "view extent: %1,%2 %3x%4 res:%5" )
|
|
.arg( qgsDoubleToString( boundingBox.xMinimum() ),
|
|
qgsDoubleToString( boundingBox.yMinimum() ) )
|
|
.arg( boundingBox.width() )
|
|
.arg( boundingBox.height() )
|
|
.arg( vres, 0, 'f' )
|
|
);
|
|
|
|
QgsDebugMsg( QStringLiteral( "tile matrix %1,%2 res:%3 tilesize:%4x%5 matrixsize:%6x%7 id:%8" )
|
|
.arg( tm->topLeft.x() ).arg( tm->topLeft.y() ).arg( tres )
|
|
.arg( tm->tileWidth ).arg( tm->tileHeight )
|
|
.arg( tm->matrixWidth ).arg( tm->matrixHeight )
|
|
.arg( tm->identifier )
|
|
);
|
|
|
|
// calculate tile coordinates
|
|
double twMap = tm->tileWidth * tres;
|
|
double thMap = tm->tileHeight * tres;
|
|
QgsDebugMsg( QStringLiteral( "tile map size: %1,%2" ).arg( qgsDoubleToString( twMap ), qgsDoubleToString( thMap ) ) );
|
|
|
|
int col = ( int ) std::floor( ( point.x() - tm->topLeft.x() ) / twMap );
|
|
int row = ( int ) std::floor( ( tm->topLeft.y() - point.y() ) / thMap );
|
|
double tx = tm->topLeft.x() + col * twMap;
|
|
double ty = tm->topLeft.y() - row * thMap;
|
|
int i = ( point.x() - tx ) / tres;
|
|
int j = ( ty - point.y() ) / tres;
|
|
|
|
QgsDebugMsg( QStringLiteral( "col=%1 row=%2 i=%3 j=%4 tx=%5 ty=%6" ).arg( col ).arg( row ).arg( i ).arg( j ).arg( tx, 0, 'f', 1 ).arg( ty, 0, 'f', 1 ) );
|
|
|
|
if ( mTileLayer->getFeatureInfoURLs.contains( formatStr ) )
|
|
{
|
|
// REST
|
|
QString url = mTileLayer->getFeatureInfoURLs[ formatStr ];
|
|
|
|
url.replace( QLatin1String( "{layer}" ), mSettings.mActiveSubLayers[0], Qt::CaseInsensitive );
|
|
url.replace( QLatin1String( "{style}" ), mSettings.mActiveSubStyles[0], Qt::CaseInsensitive );
|
|
url.replace( QLatin1String( "{tilematrixset}" ), mTileMatrixSet->identifier, Qt::CaseInsensitive );
|
|
url.replace( QLatin1String( "{tilematrix}" ), tm->identifier, Qt::CaseInsensitive );
|
|
url.replace( QLatin1String( "{tilerow}" ), QString::number( row ), Qt::CaseInsensitive );
|
|
url.replace( QLatin1String( "{tilecol}" ), QString::number( col ), Qt::CaseInsensitive );
|
|
url.replace( QLatin1String( "{i}" ), QString::number( i ), Qt::CaseInsensitive );
|
|
url.replace( QLatin1String( "{j}" ), QString::number( j ), Qt::CaseInsensitive );
|
|
|
|
for ( QHash<QString, QString>::const_iterator it = mSettings.mTileDimensionValues.constBegin(); it != mSettings.mTileDimensionValues.constEnd(); ++it )
|
|
{
|
|
url.replace( "{" + it.key() + "}", it.value(), Qt::CaseInsensitive );
|
|
}
|
|
|
|
urls << QUrl( url );
|
|
layerList << mSettings.mActiveSubLayers[0];
|
|
}
|
|
else if ( !getFeatureInfoUrl().isNull() )
|
|
{
|
|
// KVP
|
|
QUrl url( mSettings.mIgnoreGetFeatureInfoUrl ? mSettings.mBaseUrl : getFeatureInfoUrl() );
|
|
|
|
// compose static request arguments.
|
|
setQueryItem( url, QStringLiteral( "SERVICE" ), QStringLiteral( "WMTS" ) );
|
|
setQueryItem( url, QStringLiteral( "REQUEST" ), QStringLiteral( "GetFeatureInfo" ) );
|
|
setQueryItem( url, QStringLiteral( "VERSION" ), mCaps.mCapabilities.version );
|
|
setQueryItem( url, QStringLiteral( "LAYER" ), mSettings.mActiveSubLayers[0] );
|
|
setQueryItem( url, QStringLiteral( "STYLE" ), mSettings.mActiveSubStyles[0] );
|
|
setQueryItem( url, QStringLiteral( "INFOFORMAT" ), formatStr );
|
|
setQueryItem( url, QStringLiteral( "TILEMATRIXSET" ), mTileMatrixSet->identifier );
|
|
setQueryItem( url, QStringLiteral( "TILEMATRIX" ), tm->identifier );
|
|
|
|
for ( QHash<QString, QString>::const_iterator it = mSettings.mTileDimensionValues.constBegin(); it != mSettings.mTileDimensionValues.constEnd(); ++it )
|
|
{
|
|
setQueryItem( url, it.key(), it.value() );
|
|
}
|
|
|
|
setQueryItem( url, QStringLiteral( "TILEROW" ), QString::number( row ) );
|
|
setQueryItem( url, QStringLiteral( "TILECOL" ), QString::number( col ) );
|
|
setQueryItem( url, QStringLiteral( "I" ), qgsDoubleToString( i ) );
|
|
setQueryItem( url, QStringLiteral( "J" ), qgsDoubleToString( j ) );
|
|
|
|
urls << url;
|
|
layerList << mSettings.mActiveSubLayers[0];
|
|
}
|
|
else
|
|
{
|
|
QgsDebugMsg( QStringLiteral( "No KVP and no feature info url for format %1" ).arg( formatStr ) );
|
|
}
|
|
}
|
|
|
|
for ( int count = 0; count < urls.size(); count++ )
|
|
{
|
|
const QUrl &requestUrl = urls[count];
|
|
|
|
QgsDebugMsg( QStringLiteral( "getfeatureinfo: %1" ).arg( requestUrl.toString() ) );
|
|
QNetworkRequest request( requestUrl );
|
|
QgsSetRequestInitiatorClass( request, QStringLiteral( "QgsWmsProvider" ) );
|
|
QgsSetRequestInitiatorId( request, QStringLiteral( "identify %1,%2" ).arg( point.x() ).arg( point.y() ) );
|
|
mSettings.authorization().setAuthorization( request );
|
|
mIdentifyReply = QgsNetworkAccessManager::instance()->get( request );
|
|
connect( mIdentifyReply, &QNetworkReply::finished, this, &QgsWmsProvider::identifyReplyFinished );
|
|
|
|
QEventLoop loop;
|
|
mIdentifyReply->setProperty( "eventLoop", QVariant::fromValue( qobject_cast<QObject *>( &loop ) ) );
|
|
loop.exec( QEventLoop::ExcludeUserInputEvents );
|
|
|
|
if ( mIdentifyResultBodies.isEmpty() ) // no result
|
|
{
|
|
QgsDebugMsg( QStringLiteral( "mIdentifyResultBodies is empty" ) );
|
|
continue;
|
|
}
|
|
else if ( mIdentifyResultBodies.size() == 1 )
|
|
{
|
|
// Check for service exceptions (exceptions with ogr/gml are in the body)
|
|
bool isXml = false;
|
|
bool isGml = false;
|
|
|
|
const QgsNetworkReplyParser::RawHeaderMap &headers = mIdentifyResultHeaders.value( 0 );
|
|
for ( auto it = headers.constBegin(); it != headers.constEnd(); ++it )
|
|
{
|
|
if ( QString( it.key() ).compare( QLatin1String( "Content-Type" ), Qt::CaseInsensitive ) == 0 )
|
|
{
|
|
isXml = QString( it.value() ).compare( QLatin1String( "text/xml" ), Qt::CaseInsensitive ) == 0;
|
|
isGml = QString( it.value() ).compare( QLatin1String( "ogr/gml" ), Qt::CaseInsensitive ) == 0;
|
|
if ( isXml || isGml )
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ( isGml || isXml )
|
|
{
|
|
QByteArray body = mIdentifyResultBodies.value( 0 );
|
|
|
|
if ( isGml && body.startsWith( "Content-Type: text/xml\r\n\r\n" ) )
|
|
{
|
|
body = body.data() + strlen( "Content-Type: text/xml\r\n\r\n" );
|
|
isXml = true;
|
|
}
|
|
|
|
if ( isXml && parseServiceExceptionReportDom( body, mErrorCaption, mError ) )
|
|
{
|
|
QgsMessageLog::logMessage( tr( "Get feature info request error (Title: %1; Error: %2; URL: %3)" )
|
|
.arg( mErrorCaption, mError,
|
|
requestUrl.toString() ), tr( "WMS" ) );
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( format == QgsRaster::IdentifyFormatHtml || format == QgsRaster::IdentifyFormatText )
|
|
{
|
|
results.insert( results.size(), QString::fromUtf8( mIdentifyResultBodies.value( 0 ) ) );
|
|
}
|
|
else if ( format == QgsRaster::IdentifyFormatFeature ) // GML
|
|
{
|
|
// The response maybe
|
|
// 1) simple GML
|
|
// To get also geometry from UMN Mapserver, it must be enabled for layer, e.g.:
|
|
// LAYER
|
|
// METADATA
|
|
// "ows_geometries" "mygeom"
|
|
// "ows_mygeom_type" "polygon"
|
|
// END
|
|
// END
|
|
|
|
// 2) multipart GML + XSD
|
|
// Multipart is supplied by UMN Mapserver following format is used
|
|
// OUTPUTFORMAT
|
|
// NAME "OGRGML"
|
|
// DRIVER "OGR/GML"
|
|
// FORMATOPTION "FORM=multipart"
|
|
// END
|
|
// WEB
|
|
// METADATA
|
|
// "wms_getfeatureinfo_formatlist" "OGRGML,text/html"
|
|
// END
|
|
// END
|
|
// GetFeatureInfo multipart response does not seem to be defined in
|
|
// OGC specification.
|
|
|
|
int gmlPart = -1;
|
|
int xsdPart = -1;
|
|
int jsonPart = -1;
|
|
for ( int i = 0; i < mIdentifyResultHeaders.size(); i++ )
|
|
{
|
|
if ( xsdPart == -1 && mIdentifyResultHeaders.at( i ).value( "Content-Disposition" ).contains( ".xsd" ) )
|
|
{
|
|
xsdPart = i;
|
|
}
|
|
else if ( gmlPart == -1 && mIdentifyResultHeaders.at( i ).value( "Content-Disposition" ).contains( ".dat" ) )
|
|
{
|
|
gmlPart = i;
|
|
}
|
|
else if ( jsonPart == -1 && mIdentifyResultHeaders.at( i ).value( "Content-Type" ).contains( "json" ) )
|
|
{
|
|
jsonPart = i;
|
|
}
|
|
|
|
if ( gmlPart != -1 && xsdPart != -1 && jsonPart != -1 )
|
|
break;
|
|
}
|
|
|
|
if ( xsdPart == -1 && gmlPart == -1 && jsonPart == -1 )
|
|
{
|
|
if ( mIdentifyResultBodies.size() == 1 ) // GML
|
|
{
|
|
gmlPart = 0;
|
|
}
|
|
if ( mIdentifyResultBodies.size() == 2 ) // GML+XSD
|
|
{
|
|
QgsDebugMsg( QStringLiteral( "Multipart with 2 parts - expected GML + XSD" ) );
|
|
// How to find which part is GML and which XSD? Both have
|
|
// Content-Type: application/binary
|
|
// different are Content-Disposition but it is not reliable.
|
|
// We could analyze beginning of bodies...
|
|
gmlPart = 0;
|
|
xsdPart = 1;
|
|
}
|
|
}
|
|
QgsDebugMsg( QStringLiteral( "jsonPart = %1 gmlPart = %2 xsdPart = %3" ).arg( jsonPart ).arg( gmlPart ).arg( xsdPart ) );
|
|
|
|
if ( gmlPart >= 0 )
|
|
{
|
|
QByteArray gmlByteArray = mIdentifyResultBodies.value( gmlPart );
|
|
QgsDebugMsg( "GML (first 2000 bytes):\n" + gmlByteArray.left( 2000 ) );
|
|
|
|
// QgsGmlSchema.guessSchema() and QgsGml::getFeatures() are using Expat
|
|
// which only accepts UTF-8, UTF-16, ISO-8859-1
|
|
// http://sourceforge.net/p/expat/bugs/498/
|
|
QDomDocument dom;
|
|
dom.setContent( gmlByteArray ); // gets XML encoding
|
|
gmlByteArray.clear();
|
|
QTextStream stream( &gmlByteArray );
|
|
stream.setCodec( QTextCodec::codecForName( "UTF-8" ) );
|
|
dom.save( stream, 4, QDomNode::EncodingFromTextStream );
|
|
|
|
QgsDebugMsg( "GML UTF-8 (first 2000 bytes):\n" + gmlByteArray.left( 2000 ) );
|
|
|
|
QgsWkbTypes::Type wkbType;
|
|
QgsGmlSchema gmlSchema;
|
|
|
|
if ( xsdPart >= 0 ) // XSD available
|
|
{
|
|
QgsDebugMsg( "GML XSD (first 4000 bytes):\n" + QString::fromUtf8( mIdentifyResultBodies.value( xsdPart ).left( 4000 ) ) );
|
|
gmlSchema.parseXSD( mIdentifyResultBodies.value( xsdPart ) );
|
|
}
|
|
else
|
|
{
|
|
// guess from GML
|
|
bool ok = gmlSchema.guessSchema( gmlByteArray );
|
|
if ( !ok )
|
|
{
|
|
QgsError err = gmlSchema.error();
|
|
err.append( tr( "Cannot identify" ) );
|
|
QgsDebugMsg( "guess schema error: " + err.message() );
|
|
return QgsRasterIdentifyResult( err );
|
|
}
|
|
}
|
|
|
|
QStringList featureTypeNames = gmlSchema.typeNames();
|
|
QgsDebugMsg( QStringLiteral( "%1 featureTypeNames found" ).arg( featureTypeNames.size() ) );
|
|
|
|
// Each sublayer may have more features of different types, for example
|
|
// if GROUP of multiple vector layers is used with UMN MapServer
|
|
// Note: GROUP of layers in UMN MapServer is not queryable by default
|
|
// (and I could not find a way to force it), it is possible however
|
|
// to add another RASTER layer with the same name as group which is queryable
|
|
// and has no DATA defined. Then such a layer may be add to QGIS and both
|
|
// GetMap and GetFeatureInfo will return data for the group of the same name.
|
|
// https://github.com/mapserver/mapserver/issues/318#issuecomment-4923208
|
|
QgsFeatureStoreList featureStoreList;
|
|
const auto constFeatureTypeNames = featureTypeNames;
|
|
for ( const QString &featureTypeName : constFeatureTypeNames )
|
|
{
|
|
QgsDebugMsg( QStringLiteral( "featureTypeName = %1" ).arg( featureTypeName ) );
|
|
|
|
QString geometryAttribute = gmlSchema.geometryAttributes( featureTypeName ).value( 0 );
|
|
QList<QgsField> fieldList = gmlSchema.fields( featureTypeName );
|
|
QgsDebugMsg( QStringLiteral( "%1 fields found" ).arg( fieldList.size() ) );
|
|
QgsFields fields;
|
|
for ( int i = 0; i < fieldList.size(); i++ )
|
|
{
|
|
fields.append( fieldList[i] );
|
|
}
|
|
QgsGml gml( featureTypeName, geometryAttribute, fields );
|
|
// TODO: avoid converting to string and back
|
|
int ret = gml.getFeatures( gmlByteArray, &wkbType );
|
|
#ifdef QGISDEBUG
|
|
QgsDebugMsg( QStringLiteral( "parsing result = %1" ).arg( ret ) );
|
|
#else
|
|
Q_UNUSED( ret )
|
|
#endif
|
|
// TODO: all features coming from this layer should probably have the same CRS
|
|
// the same as this layer, because layerExtentToOutputExtent() may be used
|
|
// for results -> verify CRS and reprojects if necessary
|
|
QMap<QgsFeatureId, QgsFeature * > features = gml.featuresMap();
|
|
QgsCoordinateReferenceSystem featuresCrs = gml.crs();
|
|
QgsDebugMsg( QStringLiteral( "%1 features read, crs: %2 %3" ).arg( features.size() ).arg( featuresCrs.authid(), featuresCrs.description() ) );
|
|
QgsCoordinateTransform coordinateTransform;
|
|
if ( featuresCrs.isValid() && featuresCrs != crs() )
|
|
{
|
|
coordinateTransform = QgsCoordinateTransform( featuresCrs, crs(), transformContext() );
|
|
}
|
|
QgsFeatureStore featureStore( fields, crs() );
|
|
QMap<QString, QVariant> params;
|
|
params.insert( QStringLiteral( "sublayer" ), layerList[count] );
|
|
params.insert( QStringLiteral( "featureType" ), featureTypeName );
|
|
params.insert( QStringLiteral( "getFeatureInfoUrl" ), requestUrl.toString() );
|
|
featureStore.setParams( params );
|
|
QMap<QgsFeatureId, QgsFeature * >::const_iterator featIt = features.constBegin();
|
|
for ( ; featIt != features.constEnd(); ++featIt )
|
|
{
|
|
QgsFeature *feature = featIt.value();
|
|
|
|
QgsDebugMsg( QStringLiteral( "feature id = %1 : %2 attributes" ).arg( featIt.key() ).arg( feature->attributes().size() ) );
|
|
|
|
if ( coordinateTransform.isValid() && feature->hasGeometry() )
|
|
{
|
|
QgsGeometry g = feature->geometry();
|
|
g.transform( coordinateTransform );
|
|
feature->setGeometry( g );
|
|
}
|
|
featureStore.addFeature( *feature );
|
|
}
|
|
featureStoreList.append( featureStore );
|
|
}
|
|
// It is suspicious if we guessed feature types from GML but could not get
|
|
// features from it. Either we geuessed wrong schema or parsing features failed.
|
|
// Report it as error so that user can switch to another format in results dialog.
|
|
if ( xsdPart < 0 && !featureTypeNames.isEmpty() && featureStoreList.isEmpty() )
|
|
{
|
|
QgsError err = QGS_ERROR( tr( "Cannot identify" ) );
|
|
err.append( tr( "Result parsing failed. %1 feature types were guessed from gml (%2) but no features were parsed." ).arg( featureTypeNames.size() ).arg( featureTypeNames.join( QStringLiteral( "," ) ) ) );
|
|
QgsDebugMsg( "parsing GML error: " + err.message() );
|
|
return QgsRasterIdentifyResult( err );
|
|
}
|
|
results.insert( results.size(), qVariantFromValue( featureStoreList ) );
|
|
}
|
|
else if ( jsonPart != -1 )
|
|
{
|
|
QString json = QString::fromUtf8( mIdentifyResultBodies.value( jsonPart ) );
|
|
|
|
QgsFeatureStoreList featureStoreList;
|
|
QgsCoordinateTransform coordinateTransform;
|
|
|
|
try
|
|
{
|
|
QJsonDocument doc = QJsonDocument::fromJson( json.toUtf8() );
|
|
if ( doc.isNull() )
|
|
throw QStringLiteral( "Doc expected" );
|
|
if ( !doc.isObject() )
|
|
throw QStringLiteral( "Object expected" );
|
|
|
|
QJsonObject result = doc.object();
|
|
if ( result.value( QLatin1String( "type" ) ).toString() != QLatin1String( "FeatureCollection" ) )
|
|
throw QStringLiteral( "Type FeatureCollection expected: %1" ).arg( result.value( QLatin1String( "type" ) ).toString() );
|
|
|
|
if ( result.value( QLatin1String( "crs" ) ).isObject() )
|
|
{
|
|
QString crsType = result.value( QLatin1String( "crs" ) ).toObject().value( QLatin1String( "type" ) ).toString();
|
|
QString crsText;
|
|
if ( crsType == QLatin1String( "name" ) )
|
|
crsText = result.value( QStringLiteral( "crs" ) ).toObject().value( QLatin1String( "properties" ) ).toObject().value( QLatin1String( "name" ) ).toString();
|
|
else if ( crsType == QLatin1String( "EPSG" ) )
|
|
crsText = QStringLiteral( "%1:%2" ).arg( crsType, result.value( QLatin1String( "crs" ) ).toObject().value( QLatin1String( "properties" ) ).toObject().value( QStringLiteral( "code" ) ).toString() );
|
|
else
|
|
{
|
|
QgsDebugMsg( QStringLiteral( "crs not supported:%1" ).arg( result.value( QLatin1String( "crs" ) ).toString() ) );
|
|
}
|
|
|
|
QgsCoordinateReferenceSystem featuresCrs = QgsCoordinateReferenceSystem::fromOgcWmsCrs( crsText );
|
|
|
|
if ( !featuresCrs.isValid() )
|
|
throw QStringLiteral( "CRS %1 invalid" ).arg( crsText );
|
|
|
|
if ( featuresCrs.isValid() && featuresCrs != crs() )
|
|
{
|
|
coordinateTransform = QgsCoordinateTransform( featuresCrs, crs(), transformContext() );
|
|
}
|
|
}
|
|
|
|
const QJsonValue fc = result.value( QLatin1String( "features" ) );
|
|
if ( !fc.isArray() )
|
|
throw QStringLiteral( "FeatureCollection array expected" );
|
|
|
|
const QJsonArray features = fc.toArray();
|
|
|
|
int i = -1;
|
|
for ( const QJsonValue &fv : features )
|
|
{
|
|
++i;
|
|
const QJsonObject f = fv.toObject();
|
|
const QJsonValue props = f.value( QLatin1String( "properties" ) );
|
|
if ( !props.isObject() )
|
|
{
|
|
QgsDebugMsg( QStringLiteral( "no properties found" ) );
|
|
continue;
|
|
}
|
|
|
|
QgsFields fields;
|
|
|
|
const QJsonObject properties = props.toObject();
|
|
auto fieldIterator = properties.constBegin();
|
|
|
|
for ( ; fieldIterator != properties.constEnd(); ++fieldIterator )
|
|
{
|
|
fields.append( QgsField( fieldIterator.key(), QVariant::String ) );
|
|
}
|
|
|
|
QgsFeature feature( fields );
|
|
|
|
if ( f.value( QLatin1String( "geometry" ) ).isObject() )
|
|
{
|
|
QJsonDocument serializer( f.value( QLatin1String( "geometry" ) ).toObject() );
|
|
QString geom = serializer.toJson( QJsonDocument::JsonFormat::Compact );
|
|
|
|
gdal::ogr_geometry_unique_ptr ogrGeom( OGR_G_CreateGeometryFromJson( geom.toUtf8() ) );
|
|
if ( ogrGeom )
|
|
{
|
|
int wkbSize = OGR_G_WkbSize( ogrGeom.get() );
|
|
unsigned char *wkb = new unsigned char[ wkbSize ];
|
|
OGR_G_ExportToWkb( ogrGeom.get(), ( OGRwkbByteOrder ) QgsApplication::endian(), wkb );
|
|
|
|
QgsGeometry g;
|
|
g.fromWkb( wkb, wkbSize );
|
|
feature.setGeometry( g );
|
|
|
|
if ( coordinateTransform.isValid() && feature.hasGeometry() )
|
|
{
|
|
QgsGeometry transformed = feature.geometry();
|
|
transformed.transform( coordinateTransform );
|
|
feature.setGeometry( transformed );
|
|
}
|
|
}
|
|
}
|
|
|
|
int j = 0;
|
|
fieldIterator = properties.constBegin();
|
|
for ( ; fieldIterator != properties.constEnd(); ++fieldIterator )
|
|
{
|
|
feature.setAttribute( j++, fieldIterator.value().toVariant() );
|
|
}
|
|
|
|
QgsFeatureStore featureStore( fields, crs() );
|
|
|
|
QVariantMap params;
|
|
params.insert( QStringLiteral( "sublayer" ), layerList[count] );
|
|
params.insert( QStringLiteral( "featureType" ), QStringLiteral( "%1_%2" ).arg( count ).arg( i ) );
|
|
params.insert( QStringLiteral( "getFeatureInfoUrl" ), requestUrl.toString() );
|
|
featureStore.setParams( params );
|
|
|
|
feature.setValid( true );
|
|
featureStore.addFeature( feature );
|
|
|
|
featureStoreList.append( featureStore );
|
|
}
|
|
}
|
|
catch ( const QString &err )
|
|
{
|
|
QgsDebugMsg( QStringLiteral( "JSON error: %1\nResult: %2" ).arg( err, QString::fromUtf8( mIdentifyResultBodies.value( jsonPart ) ) ) );
|
|
results.insert( results.size(), err ); // string returned for format type "feature" means error
|
|
}
|
|
|
|
results.insert( results.size(), qVariantFromValue( featureStoreList ) );
|
|
}
|
|
}
|
|
}
|
|
|
|
return QgsRasterIdentifyResult( format, results );
|
|
}
|
|
|
|
void QgsWmsProvider::identifyReplyFinished()
|
|
{
|
|
QgsDebugMsgLevel( QStringLiteral( "Entered." ), 4 );
|
|
mIdentifyResultHeaders.clear();
|
|
mIdentifyResultBodies.clear();
|
|
|
|
QEventLoop *loop = qobject_cast< QEventLoop *>( sender()->property( "eventLoop" ).value< QObject *>() );
|
|
|
|
if ( mIdentifyReply->error() == QNetworkReply::NoError )
|
|
{
|
|
QVariant redirect = mIdentifyReply->attribute( QNetworkRequest::RedirectionTargetAttribute );
|
|
if ( !redirect.isNull() )
|
|
{
|
|
QgsDebugMsg( QStringLiteral( "identify request redirected to %1" ).arg( redirect.toString() ) );
|
|
emit statusChanged( tr( "identify request redirected." ) );
|
|
|
|
mIdentifyReply->deleteLater();
|
|
|
|
QgsDebugMsg( QStringLiteral( "redirected getfeatureinfo: %1" ).arg( redirect.toString() ) );
|
|
mIdentifyReply = QgsNetworkAccessManager::instance()->get( QNetworkRequest( redirect.toUrl() ) );
|
|
mSettings.authorization().setAuthorizationReply( mIdentifyReply );
|
|
mIdentifyReply->setProperty( "eventLoop", QVariant::fromValue( qobject_cast<QObject *>( loop ) ) );
|
|
connect( mIdentifyReply, &QNetworkReply::finished, this, &QgsWmsProvider::identifyReplyFinished );
|
|
return;
|
|
}
|
|
|
|
QVariant status = mIdentifyReply->attribute( QNetworkRequest::HttpStatusCodeAttribute );
|
|
if ( !status.isNull() && status.toInt() >= 400 )
|
|
{
|
|
QVariant phrase = mIdentifyReply->attribute( QNetworkRequest::HttpReasonPhraseAttribute );
|
|
mErrorFormat = QStringLiteral( "text/plain" );
|
|
mError = tr( "Map getfeatureinfo error %1: %2" ).arg( status.toInt() ).arg( phrase.toString() );
|
|
emit statusChanged( mError );
|
|
}
|
|
|
|
QgsNetworkReplyParser parser( mIdentifyReply );
|
|
if ( !parser.isValid() )
|
|
{
|
|
QgsDebugMsg( QStringLiteral( "Cannot parse reply" ) );
|
|
mErrorFormat = QStringLiteral( "text/plain" );
|
|
mError = tr( "Cannot parse getfeatureinfo: %1" ).arg( parser.error() );
|
|
emit statusChanged( mError );
|
|
}
|
|
else
|
|
{
|
|
// TODO: check headers, xsd ...
|
|
QgsDebugMsg( QStringLiteral( "%1 parts" ).arg( parser.parts() ) );
|
|
mIdentifyResultBodies = parser.bodies();
|
|
mIdentifyResultHeaders = parser.headers();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//mIdentifyResult = tr( "ERROR: GetFeatureInfo failed" );
|
|
mErrorFormat = QStringLiteral( "text/plain" );
|
|
mError = tr( "Map getfeatureinfo error: %1 [%2]" ).arg( mIdentifyReply->errorString(), mIdentifyReply->url().toString() );
|
|
emit statusChanged( mError );
|
|
QgsMessageLog::logMessage( mError, tr( "WMS" ) );
|
|
}
|
|
|
|
if ( loop )
|
|
QMetaObject::invokeMethod( loop, "quit", Qt::QueuedConnection );
|
|
|
|
mIdentifyReply->deleteLater();
|
|
mIdentifyReply = nullptr;
|
|
}
|
|
|
|
|
|
QgsCoordinateReferenceSystem QgsWmsProvider::crs() const
|
|
{
|
|
return mCrs;
|
|
}
|
|
|
|
QString QgsWmsProvider::lastErrorTitle()
|
|
{
|
|
return mErrorCaption;
|
|
}
|
|
|
|
|
|
QString QgsWmsProvider::lastError()
|
|
{
|
|
QgsDebugMsg( "returning '" + mError + "'." );
|
|
return mError;
|
|
}
|
|
|
|
QString QgsWmsProvider::lastErrorFormat()
|
|
{
|
|
return mErrorFormat;
|
|
}
|
|
|
|
QString QgsWmsProvider::name() const
|
|
{
|
|
return WMS_KEY;
|
|
}
|
|
|
|
QString QgsWmsProvider::providerKey()
|
|
{
|
|
return WMS_KEY;
|
|
}
|
|
|
|
QString QgsWmsProvider::description() const
|
|
{
|
|
return WMS_DESCRIPTION;
|
|
}
|
|
|
|
void QgsWmsProvider::reloadData()
|
|
{
|
|
}
|
|
|
|
bool QgsWmsProvider::renderInPreview( const QgsDataProvider::PreviewContext &context )
|
|
{
|
|
if ( mSettings.mTiled || mSettings.mXyz )
|
|
return true;
|
|
|
|
return QgsRasterDataProvider::renderInPreview( context );
|
|
}
|
|
|
|
QList<double> QgsWmsProvider::nativeResolutions() const
|
|
{
|
|
return mNativeResolutions;
|
|
}
|
|
|
|
QVector<QgsWmsSupportedFormat> QgsWmsProvider::supportedFormats()
|
|
{
|
|
QVector<QgsWmsSupportedFormat> formats;
|
|
|
|
QList<QByteArray> supportedFormats = QImageReader::supportedImageFormats();
|
|
|
|
if ( supportedFormats.contains( "png" ) )
|
|
{
|
|
QgsWmsSupportedFormat p1 = { "image/png", "PNG" };
|
|
QgsWmsSupportedFormat p2 = { "image/png; mode=24bit", "PNG24" }; // UMN mapserver
|
|
QgsWmsSupportedFormat p3 = { "image/png8", "PNG8" }; // used by geoserver
|
|
QgsWmsSupportedFormat p4 = { "image/png; mode=8bit", "PNG8" }; // used by QGIS server and UMN mapserver
|
|
QgsWmsSupportedFormat p5 = { "png", "PNG" }; // used by french IGN geoportail
|
|
QgsWmsSupportedFormat p6 = { "pngt", "PNGT" }; // used by french IGN geoportail
|
|
|
|
formats << p1 << p2 << p3 << p4 << p5 << p6;
|
|
}
|
|
|
|
if ( supportedFormats.contains( "jpg" ) )
|
|
{
|
|
QgsWmsSupportedFormat j1 = { "image/jpeg", "JPEG" };
|
|
QgsWmsSupportedFormat j2 = { "image/jpg", "JPEG" };
|
|
QgsWmsSupportedFormat j3 = { "jpeg", "JPEG" }; // used by french IGN geoportail
|
|
formats << j1 << j2 << j3;
|
|
}
|
|
|
|
if ( supportedFormats.contains( "png" ) && supportedFormats.contains( "jpg" ) )
|
|
{
|
|
QgsWmsSupportedFormat g1 = { "image/x-jpegorpng", "JPEG/PNG" }; // used by cubewerx
|
|
QgsWmsSupportedFormat g2 = { "image/jpgpng", "JPEG/PNG" }; // used by ESRI
|
|
formats << g1 << g2;
|
|
}
|
|
|
|
if ( supportedFormats.contains( "gif" ) )
|
|
{
|
|
QgsWmsSupportedFormat g1 = { "image/gif", "GIF" };
|
|
formats << g1;
|
|
}
|
|
|
|
if ( supportedFormats.contains( "tiff" ) )
|
|
{
|
|
QgsWmsSupportedFormat t1 = { "image/tiff", "TIFF" };
|
|
formats << t1;
|
|
}
|
|
|
|
if ( supportedFormats.contains( "svg" ) )
|
|
{
|
|
QgsWmsSupportedFormat s1 = { "image/svg", "SVG" };
|
|
QgsWmsSupportedFormat s2 = { "image/svgz", "SVG" };
|
|
QgsWmsSupportedFormat s3 = { "image/svg+xml", "SVG" };
|
|
formats << s1 << s2 << s3;
|
|
}
|
|
|
|
return formats;
|
|
}
|
|
|
|
QString QgsWmsProvider::nodeAttribute( const QDomElement &e, const QString &name, const QString &defValue )
|
|
{
|
|
if ( e.hasAttribute( name ) )
|
|
return e.attribute( name );
|
|
|
|
QDomNamedNodeMap map( e.attributes() );
|
|
for ( int i = 0; i < map.size(); i++ )
|
|
{
|
|
QDomAttr attr( map.item( i ).toElement().toAttr() );
|
|
if ( attr.name().compare( name, Qt::CaseInsensitive ) == 0 )
|
|
return attr.value();
|
|
}
|
|
|
|
return defValue;
|
|
}
|
|
|
|
void QgsWmsProvider::showMessageBox( const QString &title, const QString &text )
|
|
{
|
|
QgsMessageOutput *message = QgsMessageOutput::createMessageOutput();
|
|
message->setTitle( title );
|
|
message->setMessage( text, QgsMessageOutput::MessageText );
|
|
message->showMessage();
|
|
}
|
|
|
|
QUrl QgsWmsProvider::getLegendGraphicFullURL( double scale, const QgsRectangle &visibleExtent )
|
|
{
|
|
bool useContextualWMSLegend = mSettings.mEnableContextualLegend;
|
|
|
|
QString lurl = getLegendGraphicUrl();
|
|
|
|
if ( lurl.isEmpty() )
|
|
{
|
|
QgsDebugMsg( QStringLiteral( "getLegendGraphic url is empty" ) );
|
|
return QUrl();
|
|
}
|
|
|
|
QgsDebugMsg( QStringLiteral( "visibleExtent is %1" ).arg( visibleExtent.toString() ) );
|
|
|
|
QUrl url( lurl );
|
|
|
|
// query names are NOT case-sensitive, so make an uppercase list for proper comparison
|
|
QStringList qnames = QStringList();
|
|
for ( int i = 0; i < url.queryItems().size(); i++ )
|
|
{
|
|
qnames << url.queryItems().at( i ).first.toUpper();
|
|
}
|
|
if ( !qnames.contains( QStringLiteral( "SERVICE" ) ) )
|
|
setQueryItem( url, QStringLiteral( "SERVICE" ), QStringLiteral( "WMS" ) );
|
|
if ( !qnames.contains( QStringLiteral( "VERSION" ) ) )
|
|
setQueryItem( url, QStringLiteral( "VERSION" ), mCaps.mCapabilities.version );
|
|
if ( !qnames.contains( QStringLiteral( "SLD_VERSION" ) ) )
|
|
setQueryItem( url, QStringLiteral( "SLD_VERSION" ), QStringLiteral( "1.1.0" ) ); // can not determine SLD_VERSION
|
|
if ( !qnames.contains( QStringLiteral( "REQUEST" ) ) )
|
|
setQueryItem( url, QStringLiteral( "REQUEST" ), QStringLiteral( "GetLegendGraphic" ) );
|
|
if ( !qnames.contains( QStringLiteral( "FORMAT" ) ) )
|
|
setFormatQueryItem( url );
|
|
if ( !qnames.contains( QStringLiteral( "LAYER" ) ) )
|
|
setQueryItem( url, QStringLiteral( "LAYER" ), mSettings.mActiveSubLayers[0] );
|
|
if ( !qnames.contains( QStringLiteral( "STYLE" ) ) )
|
|
setQueryItem( url, QStringLiteral( "STYLE" ), mSettings.mActiveSubStyles[0] );
|
|
// by setting TRANSPARENT=true, even too big legend images will look good
|
|
if ( !qnames.contains( QStringLiteral( "TRANSPARENT" ) ) )
|
|
setQueryItem( url, QStringLiteral( "TRANSPARENT" ), QStringLiteral( "true" ) );
|
|
|
|
// add config parameter related to resolution
|
|
QgsSettings s;
|
|
int defaultLegendGraphicResolution = s.value( QStringLiteral( "qgis/defaultLegendGraphicResolution" ), 0 ).toInt();
|
|
QgsDebugMsg( QStringLiteral( "defaultLegendGraphicResolution: %1" ).arg( defaultLegendGraphicResolution ) );
|
|
if ( defaultLegendGraphicResolution )
|
|
{
|
|
if ( mSettings.mDpiMode & DpiQGIS )
|
|
setQueryItem( url, QStringLiteral( "DPI" ), QString::number( defaultLegendGraphicResolution ) );
|
|
if ( mSettings.mDpiMode & DpiUMN )
|
|
{
|
|
setQueryItem( url, QStringLiteral( "MAP_RESOLUTION" ), QString::number( defaultLegendGraphicResolution ) );
|
|
setQueryItem( url, QStringLiteral( "SCALE" ), QString::number( scale, 'f' ) );
|
|
}
|
|
if ( mSettings.mDpiMode & DpiGeoServer )
|
|
{
|
|
setQueryItem( url, QStringLiteral( "FORMAT_OPTIONS" ), QStringLiteral( "dpi:%1" ).arg( defaultLegendGraphicResolution ) );
|
|
setQueryItem( url, QStringLiteral( "SCALE" ), QString::number( scale, 'f' ) );
|
|
}
|
|
}
|
|
|
|
if ( useContextualWMSLegend )
|
|
{
|
|
bool changeXY = mCaps.shouldInvertAxisOrientation( mImageCrs );
|
|
setQueryItem( url, QStringLiteral( "BBOX" ), toParamValue( visibleExtent, changeXY ) );
|
|
setSRSQueryItem( url );
|
|
}
|
|
|
|
QgsDebugMsg( QStringLiteral( "getlegendgraphicrequest: %1" ).arg( url.toString() ) );
|
|
return QUrl( url );
|
|
}
|
|
|
|
QImage QgsWmsProvider::getLegendGraphic( double scale, bool forceRefresh, const QgsRectangle *visibleExtent )
|
|
{
|
|
// TODO manage return basing of getCapablity => avoid call if service is not available
|
|
// some services doesn't expose getLegendGraphic in capabilities but adding LegendURL in
|
|
// the layer tags inside capabilities
|
|
|
|
QString lurl = getLegendGraphicUrl();
|
|
|
|
if ( lurl.isEmpty() )
|
|
{
|
|
QgsDebugMsg( QStringLiteral( "getLegendGraphic url is empty" ) );
|
|
return QImage();
|
|
}
|
|
|
|
forceRefresh |= mGetLegendGraphicImage.isNull() || mGetLegendGraphicScale != scale;
|
|
|
|
QgsRectangle mapExtent = visibleExtent ? *visibleExtent : extent();
|
|
forceRefresh |= mGetLegendGraphicExtent != mapExtent;
|
|
|
|
if ( !forceRefresh )
|
|
return mGetLegendGraphicImage;
|
|
|
|
mError.clear();
|
|
|
|
QUrl url( getLegendGraphicFullURL( scale, mGetLegendGraphicExtent ) );
|
|
if ( !url.isValid() )
|
|
return QImage();
|
|
|
|
Q_ASSERT( !mLegendGraphicFetcher ); // or we could just remove it instead, hopefully will cancel download
|
|
mLegendGraphicFetcher.reset( new QgsWmsLegendDownloadHandler( *QgsNetworkAccessManager::instance(), mSettings, url ) );
|
|
if ( !mLegendGraphicFetcher )
|
|
return QImage();
|
|
|
|
connect( mLegendGraphicFetcher.get(), &QgsWmsLegendDownloadHandler::finish, this, &QgsWmsProvider::getLegendGraphicReplyFinished );
|
|
connect( mLegendGraphicFetcher.get(), &QgsWmsLegendDownloadHandler::error, this, &QgsWmsProvider::getLegendGraphicReplyErrored );
|
|
connect( mLegendGraphicFetcher.get(), &QgsWmsLegendDownloadHandler::progress, this, &QgsWmsProvider::getLegendGraphicReplyProgress );
|
|
mLegendGraphicFetcher->start();
|
|
|
|
QEventLoop loop;
|
|
mLegendGraphicFetcher->setProperty( "eventLoop", QVariant::fromValue( qobject_cast<QObject *>( &loop ) ) );
|
|
mLegendGraphicFetcher->setProperty( "legendScale", QVariant::fromValue( scale ) );
|
|
mLegendGraphicFetcher->setProperty( "legendExtent", QVariant::fromValue( mapExtent.toRectF() ) );
|
|
loop.exec( QEventLoop::ExcludeUserInputEvents );
|
|
|
|
QgsDebugMsg( QStringLiteral( "exiting." ) );
|
|
|
|
return mGetLegendGraphicImage;
|
|
}
|
|
|
|
QgsImageFetcher *QgsWmsProvider::getLegendGraphicFetcher( const QgsMapSettings *mapSettings )
|
|
{
|
|
double scale;
|
|
QgsRectangle mapExtent;
|
|
if ( mapSettings && mSettings.mEnableContextualLegend )
|
|
{
|
|
scale = mapSettings->scale();
|
|
mapExtent = mapSettings->visibleExtent();
|
|
try
|
|
{
|
|
QgsCoordinateTransform ct { mapSettings->destinationCrs(), crs(), mapSettings->transformContext() };
|
|
mapExtent = ct.transformBoundingBox( mapExtent );
|
|
}
|
|
catch ( QgsCsException & )
|
|
{
|
|
// Can't reproject
|
|
}
|
|
}
|
|
else
|
|
{
|
|
scale = 0;
|
|
mapExtent = extent();
|
|
}
|
|
|
|
if ( mSettings.mXyz )
|
|
{
|
|
// we are working with XYZ tiles: no legend graphics available
|
|
return nullptr;
|
|
}
|
|
|
|
QUrl url = getLegendGraphicFullURL( scale, mapExtent );
|
|
if ( !url.isValid() )
|
|
return nullptr;
|
|
|
|
if ( mapExtent == mGetLegendGraphicExtent &&
|
|
scale == mGetLegendGraphicScale &&
|
|
!mGetLegendGraphicImage.isNull() )
|
|
{
|
|
QgsDebugMsg( QStringLiteral( "Emitting cached image fetcher" ) );
|
|
// return a cached image, skipping the load
|
|
return new QgsCachedImageFetcher( mGetLegendGraphicImage );
|
|
}
|
|
else
|
|
{
|
|
QgsImageFetcher *fetcher = new QgsWmsLegendDownloadHandler( *QgsNetworkAccessManager::instance(), mSettings, url );
|
|
fetcher->setProperty( "legendScale", QVariant::fromValue( scale ) );
|
|
fetcher->setProperty( "legendExtent", QVariant::fromValue( mapExtent.toRectF() ) );
|
|
connect( fetcher, &QgsImageFetcher::finish, this, &QgsWmsProvider::getLegendGraphicReplyFinished );
|
|
return fetcher;
|
|
}
|
|
}
|
|
|
|
void QgsWmsProvider::getLegendGraphicReplyFinished( const QImage &img )
|
|
{
|
|
|
|
QObject *reply = sender();
|
|
|
|
if ( !img.isNull() )
|
|
{
|
|
mGetLegendGraphicImage = img;
|
|
mGetLegendGraphicExtent = QgsRectangle( reply->property( "legendExtent" ).toRectF() );
|
|
mGetLegendGraphicScale = reply->property( "legendScale" ).value<double>();
|
|
|
|
#ifdef QGISDEBUG
|
|
QString filename = QDir::tempPath() + "/GetLegendGraphic.png";
|
|
mGetLegendGraphicImage.save( filename );
|
|
QgsDebugMsg( "saved GetLegendGraphic result in debug file: " + filename );
|
|
#endif
|
|
}
|
|
|
|
if ( reply == mLegendGraphicFetcher.get() )
|
|
{
|
|
QEventLoop *loop = qobject_cast< QEventLoop *>( reply->property( "eventLoop" ).value< QObject *>() );
|
|
if ( loop )
|
|
QMetaObject::invokeMethod( loop, "quit", Qt::QueuedConnection );
|
|
mLegendGraphicFetcher.reset();
|
|
}
|
|
}
|
|
|
|
void QgsWmsProvider::getLegendGraphicReplyErrored( const QString &message )
|
|
{
|
|
Q_UNUSED( message )
|
|
QgsDebugMsg( QStringLiteral( "get legend failed: %1" ).arg( message ) );
|
|
|
|
QObject *reply = sender();
|
|
|
|
if ( reply == mLegendGraphicFetcher.get() )
|
|
{
|
|
QEventLoop *loop = qobject_cast< QEventLoop *>( reply->property( "eventLoop" ).value< QObject *>() );
|
|
if ( loop )
|
|
QMetaObject::invokeMethod( loop, "quit", Qt::QueuedConnection );
|
|
mLegendGraphicFetcher.reset();
|
|
}
|
|
}
|
|
|
|
void QgsWmsProvider::getLegendGraphicReplyProgress( qint64 bytesReceived, qint64 bytesTotal )
|
|
{
|
|
QString msg = tr( "%1 of %2 bytes of GetLegendGraphic downloaded." ).arg( bytesReceived ).arg( bytesTotal < 0 ? QStringLiteral( "unknown number of" ) : QString::number( bytesTotal ) );
|
|
QgsDebugMsg( msg );
|
|
emit statusChanged( msg );
|
|
}
|
|
|
|
QgsWmsProvider *QgsWmsProviderMetadata::createProvider( const QString &uri, const QgsDataProvider::ProviderOptions &options )
|
|
{
|
|
return new QgsWmsProvider( uri, options );
|
|
}
|
|
|
|
// -----------------
|
|
|
|
QgsWmsImageDownloadHandler::QgsWmsImageDownloadHandler( const QString &providerUri, const QUrl &url, const QgsWmsAuthorization &auth, QImage *image, QgsRasterBlockFeedback *feedback )
|
|
: mProviderUri( providerUri )
|
|
, mCachedImage( image )
|
|
, mEventLoop( new QEventLoop )
|
|
, mFeedback( feedback )
|
|
{
|
|
if ( feedback )
|
|
{
|
|
connect( feedback, &QgsFeedback::canceled, this, &QgsWmsImageDownloadHandler::canceled, Qt::QueuedConnection );
|
|
|
|
// rendering could have been canceled before we started to listen to canceled() signal
|
|
// so let's check before doing the download and maybe quit prematurely
|
|
if ( feedback->isCanceled() )
|
|
return;
|
|
}
|
|
|
|
QNetworkRequest request( url );
|
|
QgsSetRequestInitiatorClass( request, QStringLiteral( "QgsWmsImageDownloadHandler" ) );
|
|
auth.setAuthorization( request );
|
|
request.setAttribute( QNetworkRequest::CacheSaveControlAttribute, true );
|
|
mCacheReply = QgsNetworkAccessManager::instance()->get( request );
|
|
connect( mCacheReply, &QNetworkReply::finished, this, &QgsWmsImageDownloadHandler::cacheReplyFinished );
|
|
connect( mCacheReply, &QNetworkReply::downloadProgress, this, &QgsWmsImageDownloadHandler::cacheReplyProgress );
|
|
|
|
Q_ASSERT( mCacheReply->thread() == QThread::currentThread() );
|
|
}
|
|
|
|
QgsWmsImageDownloadHandler::~QgsWmsImageDownloadHandler()
|
|
{
|
|
delete mEventLoop;
|
|
}
|
|
|
|
void QgsWmsImageDownloadHandler::downloadBlocking()
|
|
{
|
|
if ( mFeedback && mFeedback->isCanceled() )
|
|
return; // nothing to do
|
|
|
|
mEventLoop->exec( QEventLoop::ExcludeUserInputEvents );
|
|
|
|
Q_ASSERT( !mCacheReply );
|
|
}
|
|
|
|
void QgsWmsImageDownloadHandler::cacheReplyFinished()
|
|
{
|
|
if ( mCacheReply->error() == QNetworkReply::NoError )
|
|
{
|
|
QVariant redirect = mCacheReply->attribute( QNetworkRequest::RedirectionTargetAttribute );
|
|
if ( !redirect.isNull() )
|
|
{
|
|
mCacheReply->deleteLater();
|
|
|
|
QgsDebugMsg( QStringLiteral( "redirected getmap: %1" ).arg( redirect.toString() ) );
|
|
mCacheReply = QgsNetworkAccessManager::instance()->get( QNetworkRequest( redirect.toUrl() ) );
|
|
connect( mCacheReply, &QNetworkReply::finished, this, &QgsWmsImageDownloadHandler::cacheReplyFinished );
|
|
return;
|
|
}
|
|
|
|
QVariant status = mCacheReply->attribute( QNetworkRequest::HttpStatusCodeAttribute );
|
|
if ( !status.isNull() && status.toInt() >= 400 )
|
|
{
|
|
QVariant phrase = mCacheReply->attribute( QNetworkRequest::HttpReasonPhraseAttribute );
|
|
|
|
QgsMessageLog::logMessage( tr( "Map request error (Status: %1; Reason phrase: %2; URL: %3)" )
|
|
.arg( status.toInt() )
|
|
.arg( phrase.toString(),
|
|
mCacheReply->url().toString() ), tr( "WMS" ) );
|
|
|
|
mCacheReply->deleteLater();
|
|
mCacheReply = nullptr;
|
|
|
|
finish();
|
|
return;
|
|
}
|
|
|
|
QString contentType = mCacheReply->header( QNetworkRequest::ContentTypeHeader ).toString();
|
|
QgsDebugMsg( "contentType: " + contentType );
|
|
QByteArray text = mCacheReply->readAll();
|
|
QImage myLocalImage = QImage::fromData( text );
|
|
if ( !myLocalImage.isNull() )
|
|
{
|
|
QPainter p( mCachedImage );
|
|
p.drawImage( 0, 0, myLocalImage );
|
|
}
|
|
else if ( contentType.startsWith( QLatin1String( "image/" ), Qt::CaseInsensitive ) ||
|
|
contentType.compare( QLatin1String( "application/octet-stream" ), Qt::CaseInsensitive ) == 0 )
|
|
{
|
|
QgsMessageLog::logMessage( tr( "Returned image is flawed [Content-Type: %1; URL: %2]" )
|
|
.arg( contentType, mCacheReply->url().toString() ), tr( "WMS" ) );
|
|
}
|
|
else
|
|
{
|
|
QString errorTitle, errorText;
|
|
if ( contentType.compare( QLatin1String( "text/xml" ), Qt::CaseInsensitive ) == 0 && QgsWmsProvider::parseServiceExceptionReportDom( text, errorTitle, errorText ) )
|
|
{
|
|
QgsMessageLog::logMessage( tr( "Map request error (Title: %1; Error: %2; URL: %3)" )
|
|
.arg( errorTitle, errorText,
|
|
mCacheReply->url().toString() ), tr( "WMS" ) );
|
|
}
|
|
else
|
|
{
|
|
QgsMessageLog::logMessage( tr( "Map request error (Status: %1; Response: %2; Content-Type: %3; URL: %4)" )
|
|
.arg( status.toInt() )
|
|
.arg( QString::fromUtf8( text ),
|
|
contentType,
|
|
mCacheReply->url().toString() ), tr( "WMS" ) );
|
|
}
|
|
|
|
mCacheReply->deleteLater();
|
|
mCacheReply = nullptr;
|
|
|
|
finish();
|
|
return;
|
|
}
|
|
|
|
mCacheReply->deleteLater();
|
|
mCacheReply = nullptr;
|
|
|
|
finish();
|
|
}
|
|
else
|
|
{
|
|
// report any errors except for the one we have caused by canceling the request
|
|
if ( mCacheReply->error() != QNetworkReply::OperationCanceledError )
|
|
{
|
|
QgsWmsStatistics::Stat &stat = QgsWmsStatistics::statForUri( mProviderUri );
|
|
|
|
stat.errors++;
|
|
if ( stat.errors < 100 )
|
|
{
|
|
QgsMessageLog::logMessage( tr( "Map request failed [error: %1 url: %2]" ).arg( mCacheReply->errorString(), mCacheReply->url().toString() ), tr( "WMS" ) );
|
|
}
|
|
else if ( stat.errors == 100 )
|
|
{
|
|
QgsMessageLog::logMessage( tr( "Not logging more than 100 request errors." ), tr( "WMS" ) );
|
|
}
|
|
}
|
|
|
|
mCacheReply->deleteLater();
|
|
mCacheReply = nullptr;
|
|
|
|
finish();
|
|
}
|
|
}
|
|
|
|
void QgsWmsImageDownloadHandler::cacheReplyProgress( qint64 bytesReceived, qint64 bytesTotal )
|
|
{
|
|
Q_UNUSED( bytesReceived )
|
|
Q_UNUSED( bytesTotal )
|
|
QgsDebugMsg( QStringLiteral( "%1 of %2 bytes of map downloaded." ).arg( bytesReceived ).arg( bytesTotal < 0 ? QString( "unknown number of" ) : QString::number( bytesTotal ) ) );
|
|
}
|
|
|
|
void QgsWmsImageDownloadHandler::canceled()
|
|
{
|
|
QgsDebugMsg( QStringLiteral( "Caught canceled() signal" ) );
|
|
if ( mCacheReply )
|
|
{
|
|
// abort the reply if it is still active
|
|
QgsDebugMsg( QStringLiteral( "Aborting WMS network request" ) );
|
|
mCacheReply->abort();
|
|
}
|
|
}
|
|
|
|
|
|
// ----------
|
|
|
|
|
|
QgsWmsTiledImageDownloadHandler::QgsWmsTiledImageDownloadHandler( const QString &providerUri, const QgsWmsAuthorization &auth, int tileReqNo, const QgsWmsProvider::TileRequests &requests, QImage *image, const QgsRectangle &viewExtent, bool smoothPixmapTransform, QgsRasterBlockFeedback *feedback )
|
|
: mProviderUri( providerUri )
|
|
, mAuth( auth )
|
|
, mImage( image )
|
|
, mViewExtent( viewExtent )
|
|
, mEventLoop( new QEventLoop )
|
|
, mTileReqNo( tileReqNo )
|
|
, mSmoothPixmapTransform( smoothPixmapTransform )
|
|
, mFeedback( feedback )
|
|
{
|
|
if ( feedback )
|
|
{
|
|
connect( feedback, &QgsFeedback::canceled, this, &QgsWmsTiledImageDownloadHandler::canceled, Qt::QueuedConnection );
|
|
|
|
// rendering could have been canceled before we started to listen to canceled() signal
|
|
// so let's check before doing the download and maybe quit prematurely
|
|
if ( feedback->isCanceled() )
|
|
return;
|
|
}
|
|
|
|
const auto constRequests = requests;
|
|
for ( const QgsWmsProvider::TileRequest &r : constRequests )
|
|
{
|
|
QNetworkRequest request( r.url );
|
|
QgsSetRequestInitiatorClass( request, QStringLiteral( "QgsWmsTiledImageDownloadHandler" ) );
|
|
auth.setAuthorization( request );
|
|
request.setRawHeader( "Accept", "*/*" );
|
|
request.setAttribute( QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache );
|
|
request.setAttribute( QNetworkRequest::CacheSaveControlAttribute, true );
|
|
request.setAttribute( static_cast<QNetworkRequest::Attribute>( TileReqNo ), mTileReqNo );
|
|
request.setAttribute( static_cast<QNetworkRequest::Attribute>( TileIndex ), r.index );
|
|
request.setAttribute( static_cast<QNetworkRequest::Attribute>( TileRect ), r.rect );
|
|
request.setAttribute( static_cast<QNetworkRequest::Attribute>( TileRetry ), 0 );
|
|
|
|
QNetworkReply *reply = QgsNetworkAccessManager::instance()->get( request );
|
|
connect( reply, &QNetworkReply::finished, this, &QgsWmsTiledImageDownloadHandler::tileReplyFinished );
|
|
|
|
mReplies << reply;
|
|
}
|
|
}
|
|
|
|
QgsWmsTiledImageDownloadHandler::~QgsWmsTiledImageDownloadHandler()
|
|
{
|
|
delete mEventLoop;
|
|
}
|
|
|
|
void QgsWmsTiledImageDownloadHandler::downloadBlocking()
|
|
{
|
|
if ( mFeedback && mFeedback->isCanceled() )
|
|
return; // nothing to do
|
|
|
|
mEventLoop->exec( QEventLoop::ExcludeUserInputEvents );
|
|
|
|
Q_ASSERT( mReplies.isEmpty() );
|
|
}
|
|
|
|
|
|
void QgsWmsTiledImageDownloadHandler::tileReplyFinished()
|
|
{
|
|
QNetworkReply *reply = qobject_cast<QNetworkReply *>( sender() );
|
|
|
|
#if defined(QGISDEBUG)
|
|
bool fromCache = reply->attribute( QNetworkRequest::SourceIsFromCacheAttribute ).toBool();
|
|
QgsWmsStatistics::Stat &stat = QgsWmsStatistics::statForUri( mProviderUri );
|
|
if ( fromCache )
|
|
stat.cacheHits++;
|
|
else
|
|
stat.cacheMisses++;
|
|
#endif
|
|
#if defined(QGISDEBUG)
|
|
QgsDebugMsgLevel( QStringLiteral( "raw headers:" ), 3 );
|
|
const auto constRawHeaderPairs = reply->rawHeaderPairs();
|
|
for ( const QNetworkReply::RawHeaderPair &pair : constRawHeaderPairs )
|
|
{
|
|
QgsDebugMsgLevel( QStringLiteral( " %1:%2" )
|
|
.arg( QString::fromUtf8( pair.first ),
|
|
QString::fromUtf8( pair.second ) ), 3 );
|
|
}
|
|
#endif
|
|
|
|
if ( QgsNetworkAccessManager::instance()->cache() )
|
|
{
|
|
QNetworkCacheMetaData cmd = QgsNetworkAccessManager::instance()->cache()->metaData( reply->request().url() );
|
|
|
|
QNetworkCacheMetaData::RawHeaderList hl;
|
|
const auto constRawHeaders = cmd.rawHeaders();
|
|
for ( const QNetworkCacheMetaData::RawHeader &h : constRawHeaders )
|
|
{
|
|
if ( h.first != "Cache-Control" )
|
|
hl.append( h );
|
|
}
|
|
cmd.setRawHeaders( hl );
|
|
|
|
QgsDebugMsgLevel( QStringLiteral( "expirationDate:%1" ).arg( cmd.expirationDate().toString() ), 4 );
|
|
if ( cmd.expirationDate().isNull() )
|
|
{
|
|
QgsSettings s;
|
|
cmd.setExpirationDate( QDateTime::currentDateTime().addSecs( s.value( QStringLiteral( "qgis/defaultTileExpiry" ), "24" ).toInt() * 60 * 60 ) );
|
|
}
|
|
|
|
QgsNetworkAccessManager::instance()->cache()->updateMetaData( cmd );
|
|
}
|
|
|
|
int tileReqNo = reply->request().attribute( static_cast<QNetworkRequest::Attribute>( TileReqNo ) ).toInt();
|
|
int tileNo = reply->request().attribute( static_cast<QNetworkRequest::Attribute>( TileIndex ) ).toInt();
|
|
QRectF r = reply->request().attribute( static_cast<QNetworkRequest::Attribute>( TileRect ) ).toRectF();
|
|
#ifdef QGISDEBUG
|
|
int retry = reply->request().attribute( static_cast<QNetworkRequest::Attribute>( TileRetry ) ).toInt();
|
|
#endif
|
|
|
|
QgsDebugMsgLevel( QStringLiteral( "tile reply %1 (%2) tile:%3(retry %4) rect:%5,%6 %7,%8) fromcache:%9 %10 url:%11" )
|
|
.arg( tileReqNo ).arg( mTileReqNo ).arg( tileNo ).arg( retry )
|
|
.arg( r.left(), 0, 'f' ).arg( r.bottom(), 0, 'f' ).arg( r.right(), 0, 'f' ).arg( r.top(), 0, 'f' )
|
|
.arg( fromCache )
|
|
.arg( reply->error() == QNetworkReply::NoError ? QString() : QStringLiteral( "error: " ) + reply->errorString(),
|
|
reply->url().toString() ), 4
|
|
);
|
|
|
|
if ( reply->error() == QNetworkReply::NoError )
|
|
{
|
|
QVariant redirect = reply->attribute( QNetworkRequest::RedirectionTargetAttribute );
|
|
if ( !redirect.isNull() )
|
|
{
|
|
QNetworkRequest request( redirect.toUrl() );
|
|
QgsSetRequestInitiatorClass( request, QStringLiteral( "QgsWmsTiledImageDownloadHandler" ) );
|
|
mAuth.setAuthorization( request );
|
|
request.setRawHeader( "Accept", "*/*" );
|
|
request.setAttribute( QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache );
|
|
request.setAttribute( QNetworkRequest::CacheSaveControlAttribute, true );
|
|
request.setAttribute( static_cast<QNetworkRequest::Attribute>( TileReqNo ), tileReqNo );
|
|
request.setAttribute( static_cast<QNetworkRequest::Attribute>( TileIndex ), tileNo );
|
|
request.setAttribute( static_cast<QNetworkRequest::Attribute>( TileRect ), r );
|
|
request.setAttribute( static_cast<QNetworkRequest::Attribute>( TileRetry ), 0 );
|
|
|
|
mReplies.removeOne( reply );
|
|
reply->deleteLater();
|
|
|
|
QgsDebugMsg( QStringLiteral( "redirected gettile: %1" ).arg( redirect.toString() ) );
|
|
reply = QgsNetworkAccessManager::instance()->get( request );
|
|
mReplies << reply;
|
|
|
|
connect( reply, &QNetworkReply::finished, this, &QgsWmsTiledImageDownloadHandler::tileReplyFinished );
|
|
|
|
return;
|
|
}
|
|
|
|
QVariant status = reply->attribute( QNetworkRequest::HttpStatusCodeAttribute );
|
|
if ( !status.isNull() && status.toInt() >= 400 )
|
|
{
|
|
QVariant phrase = reply->attribute( QNetworkRequest::HttpReasonPhraseAttribute );
|
|
|
|
QgsWmsProvider::showMessageBox( tr( "Tile request error" ), tr( "Status: %1\nReason phrase: %2" ).arg( status.toInt() ).arg( phrase.toString() ) );
|
|
|
|
mReplies.removeOne( reply );
|
|
reply->deleteLater();
|
|
|
|
if ( mReplies.isEmpty() )
|
|
finish();
|
|
|
|
return;
|
|
}
|
|
|
|
QString contentType = reply->header( QNetworkRequest::ContentTypeHeader ).toString();
|
|
QgsDebugMsgLevel( "contentType: " + contentType, 3 );
|
|
if ( !contentType.isEmpty() && !contentType.startsWith( QLatin1String( "image/" ), Qt::CaseInsensitive ) &&
|
|
contentType.compare( QLatin1String( "application/octet-stream" ), Qt::CaseInsensitive ) != 0 )
|
|
{
|
|
QByteArray text = reply->readAll();
|
|
QString errorTitle, errorText;
|
|
if ( contentType.compare( QLatin1String( "text/xml" ), Qt::CaseInsensitive ) == 0 && QgsWmsProvider::parseServiceExceptionReportDom( text, errorTitle, errorText ) )
|
|
{
|
|
QgsMessageLog::logMessage( tr( "Tile request error (Title: %1; Error: %2; URL: %3)" )
|
|
.arg( errorTitle, errorText,
|
|
reply->url().toString() ), tr( "WMS" ) );
|
|
}
|
|
else
|
|
{
|
|
QgsMessageLog::logMessage( tr( "Tile request error (Status: %1; Content-Type: %2; Length: %3; URL: %4)" )
|
|
.arg( status.toString(),
|
|
contentType )
|
|
.arg( text.size() )
|
|
.arg( reply->url().toString() ), tr( "WMS" ) );
|
|
#ifdef QGISDEBUG
|
|
QFile file( QDir::tempPath() + "/broken-image.png" );
|
|
if ( file.open( QIODevice::WriteOnly | QIODevice::Truncate ) )
|
|
{
|
|
file.write( text );
|
|
file.close();
|
|
}
|
|
#endif
|
|
}
|
|
|
|
mReplies.removeOne( reply );
|
|
reply->deleteLater();
|
|
|
|
if ( mReplies.isEmpty() )
|
|
finish();
|
|
|
|
return;
|
|
}
|
|
|
|
// only take results from current request number
|
|
if ( mTileReqNo == tileReqNo )
|
|
{
|
|
double cr = mViewExtent.width() / mImage->width();
|
|
|
|
QRectF dst( ( r.left() - mViewExtent.xMinimum() ) / cr,
|
|
( mViewExtent.yMaximum() - r.bottom() ) / cr,
|
|
r.width() / cr,
|
|
r.height() / cr );
|
|
|
|
QgsDebugMsgLevel( QStringLiteral( "tile reply: length %1" ).arg( reply->bytesAvailable() ), 2 );
|
|
|
|
QImage myLocalImage = QImage::fromData( reply->readAll() );
|
|
|
|
if ( !myLocalImage.isNull() )
|
|
{
|
|
QPainter p( mImage );
|
|
// if image size is "close enough" to destination size, don't smooth it out. Instead try for pixel-perfect placement!
|
|
const bool disableSmoothing = ( qgsDoubleNear( dst.width(), myLocalImage.width(), 2 ) && qgsDoubleNear( dst.height(), myLocalImage.height(), 2 ) );
|
|
if ( !disableSmoothing && mSmoothPixmapTransform )
|
|
p.setRenderHint( QPainter::SmoothPixmapTransform, true );
|
|
p.drawImage( dst, myLocalImage );
|
|
p.end();
|
|
#if 0
|
|
myLocalImage.save( QString( "%1/%2-tile-%3.png" ).arg( QDir::tempPath() ).arg( mTileReqNo ).arg( tileNo ) );
|
|
p.drawRect( dst ); // show tile bounds
|
|
p.drawText( dst, Qt::AlignCenter, QString( "(%1)\n%2,%3\n%4,%5\n%6x%7" )
|
|
.arg( tileNo )
|
|
.arg( r.left() ).arg( r.bottom() )
|
|
.arg( r.right() ).arg( r.top() )
|
|
.arg( r.width() ).arg( r.height() ) );
|
|
#endif
|
|
|
|
QgsTileCache::insertTile( reply->url(), myLocalImage );
|
|
|
|
if ( mFeedback )
|
|
mFeedback->onNewData();
|
|
}
|
|
else
|
|
{
|
|
QgsMessageLog::logMessage( tr( "Returned image is flawed [Content-Type: %1; URL: %2]" )
|
|
.arg( contentType, reply->url().toString() ), tr( "WMS" ) );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
QgsDebugMsg( QStringLiteral( "Reply too late [%1]" ).arg( reply->url().toString() ) );
|
|
}
|
|
|
|
mReplies.removeOne( reply );
|
|
reply->deleteLater();
|
|
|
|
if ( mReplies.isEmpty() )
|
|
finish();
|
|
|
|
}
|
|
else
|
|
{
|
|
if ( !( mFeedback && mFeedback->isPreviewOnly() ) )
|
|
{
|
|
// report any errors except for the one we have caused by canceling the request
|
|
if ( reply->error() != QNetworkReply::OperationCanceledError )
|
|
{
|
|
QgsWmsStatistics::Stat &stat = QgsWmsStatistics::statForUri( mProviderUri );
|
|
stat.errors++;
|
|
|
|
// if we reached timeout, let's try again (e.g. in case of slow connection or slow server)
|
|
if ( reply->error() == QNetworkReply::TimeoutError )
|
|
repeatTileRequest( reply->request() );
|
|
}
|
|
}
|
|
|
|
mReplies.removeOne( reply );
|
|
reply->deleteLater();
|
|
|
|
if ( mReplies.isEmpty() )
|
|
finish();
|
|
}
|
|
|
|
#if 0
|
|
const QgsWmsStatistics::Stat &stat = QgsWmsStatistics::statForUri( mProviderUri );
|
|
emit statusChanged( tr( "%n tile requests in background", "tile request count", mReplies.count() )
|
|
+ tr( ", %n cache hits", "tile cache hits", stat.cacheHits )
|
|
+ tr( ", %n cache misses.", "tile cache missed", stat.cacheMisses )
|
|
+ tr( ", %n errors.", "errors", stat.errors )
|
|
);
|
|
#endif
|
|
}
|
|
|
|
void QgsWmsTiledImageDownloadHandler::canceled()
|
|
{
|
|
QgsDebugMsgLevel( QStringLiteral( "Caught canceled() signal" ), 3 );
|
|
const auto constMReplies = mReplies;
|
|
for ( QNetworkReply *reply : constMReplies )
|
|
{
|
|
QgsDebugMsgLevel( QStringLiteral( "Aborting tiled network request" ), 3 );
|
|
reply->abort();
|
|
}
|
|
}
|
|
|
|
|
|
void QgsWmsTiledImageDownloadHandler::repeatTileRequest( QNetworkRequest const &oldRequest )
|
|
{
|
|
QgsWmsStatistics::Stat &stat = QgsWmsStatistics::statForUri( mProviderUri );
|
|
|
|
if ( stat.errors == 100 )
|
|
{
|
|
QgsMessageLog::logMessage( tr( "Not logging more than 100 request errors." ), tr( "WMS" ) );
|
|
}
|
|
|
|
QNetworkRequest request( oldRequest );
|
|
QgsSetRequestInitiatorClass( request, QStringLiteral( "QgsWmsTiledImageDownloadHandler" ) );
|
|
|
|
QString url = request.url().toString();
|
|
int tileReqNo = request.attribute( static_cast<QNetworkRequest::Attribute>( TileReqNo ) ).toInt();
|
|
int tileNo = request.attribute( static_cast<QNetworkRequest::Attribute>( TileIndex ) ).toInt();
|
|
int retry = request.attribute( static_cast<QNetworkRequest::Attribute>( TileRetry ) ).toInt();
|
|
retry++;
|
|
|
|
QgsSettings s;
|
|
int maxRetry = s.value( QStringLiteral( "qgis/defaultTileMaxRetry" ), "3" ).toInt();
|
|
if ( retry > maxRetry )
|
|
{
|
|
if ( stat.errors < 100 )
|
|
{
|
|
QgsMessageLog::logMessage( tr( "Tile request max retry error. Failed %1 requests for tile %2 of tileRequest %3 (url: %4)" )
|
|
.arg( maxRetry ).arg( tileNo ).arg( tileReqNo ).arg( url ), tr( "WMS" ) );
|
|
}
|
|
return;
|
|
}
|
|
|
|
mAuth.setAuthorization( request );
|
|
if ( stat.errors < 100 )
|
|
{
|
|
QgsMessageLog::logMessage( tr( "repeat tileRequest %1 tile %2(retry %3)" )
|
|
.arg( tileReqNo ).arg( tileNo ).arg( retry ), tr( "WMS" ), Qgis::Info );
|
|
}
|
|
QgsDebugMsg( QStringLiteral( "repeat tileRequest %1 %2(retry %3) for url: %4" ).arg( tileReqNo ).arg( tileNo ).arg( retry ).arg( url ) );
|
|
request.setAttribute( static_cast<QNetworkRequest::Attribute>( TileRetry ), retry );
|
|
|
|
QNetworkReply *reply = QgsNetworkAccessManager::instance()->get( request );
|
|
mReplies << reply;
|
|
connect( reply, &QNetworkReply::finished, this, &QgsWmsTiledImageDownloadHandler::tileReplyFinished );
|
|
}
|
|
|
|
// Some servers like http://glogow.geoportal2.pl/map/wms/wms.php? do not BBOX
|
|
// to be formatted with excessive precision. As a double is exactly represented
|
|
// with 19 decimal figures, do not attempt to output more
|
|
static QString formatDouble( double x )
|
|
{
|
|
if ( x == 0.0 )
|
|
return QStringLiteral( "0" );
|
|
const int numberOfDecimals = std::max( 0, 19 - static_cast<int>( std::ceil( std::log10( std::fabs( x ) ) ) ) );
|
|
return qgsDoubleToString( x, numberOfDecimals );
|
|
}
|
|
|
|
QString QgsWmsProvider::toParamValue( const QgsRectangle &rect, bool changeXY )
|
|
{
|
|
return QString( changeXY ? "%2,%1,%4,%3" : "%1,%2,%3,%4" )
|
|
.arg( formatDouble( rect.xMinimum() ),
|
|
formatDouble( rect.yMinimum() ),
|
|
formatDouble( rect.xMaximum() ),
|
|
formatDouble( rect.yMaximum() ) );
|
|
}
|
|
|
|
void QgsWmsProvider::setSRSQueryItem( QUrl &url )
|
|
{
|
|
QString crsKey = QStringLiteral( "SRS" ); //SRS in 1.1.1 and CRS in 1.3.0
|
|
if ( mCaps.mCapabilities.version == QLatin1String( "1.3.0" ) || mCaps.mCapabilities.version == QLatin1String( "1.3" ) )
|
|
{
|
|
crsKey = QStringLiteral( "CRS" );
|
|
}
|
|
setQueryItem( url, crsKey, mImageCrs );
|
|
}
|
|
|
|
bool QgsWmsProvider::ignoreExtents() const
|
|
{
|
|
return mSettings.mIgnoreReportedLayerExtents;
|
|
}
|
|
|
|
// ----------
|
|
|
|
QgsWmsLegendDownloadHandler::QgsWmsLegendDownloadHandler( QgsNetworkAccessManager &networkAccessManager, const QgsWmsSettings &settings, const QUrl &url )
|
|
: mNetworkAccessManager( networkAccessManager )
|
|
, mSettings( settings )
|
|
, mInitialUrl( url )
|
|
{
|
|
}
|
|
|
|
QgsWmsLegendDownloadHandler::~QgsWmsLegendDownloadHandler()
|
|
{
|
|
if ( mReply )
|
|
{
|
|
// Send finished if not done yet ?
|
|
QgsDebugMsg( QStringLiteral( "WMSLegendDownloader destroyed while still processing reply" ) );
|
|
mReply->deleteLater();
|
|
}
|
|
mReply = nullptr;
|
|
}
|
|
|
|
/* public */
|
|
void
|
|
QgsWmsLegendDownloadHandler::start()
|
|
{
|
|
Q_ASSERT( mVisitedUrls.empty() );
|
|
startUrl( mInitialUrl );
|
|
}
|
|
|
|
/* private */
|
|
void
|
|
QgsWmsLegendDownloadHandler::startUrl( const QUrl &url )
|
|
{
|
|
Q_ASSERT( !mReply ); // don't call me twice from outside !
|
|
Q_ASSERT( url.isValid() );
|
|
|
|
if ( mVisitedUrls.contains( url ) )
|
|
{
|
|
QString err( tr( "Redirect loop detected: %1" ).arg( url.toString() ) );
|
|
QgsMessageLog::logMessage( err, tr( "WMS" ) );
|
|
sendError( err );
|
|
return;
|
|
}
|
|
mVisitedUrls.insert( url );
|
|
|
|
QgsDebugMsg( QStringLiteral( "legend url: %1" ).arg( url.toString() ) );
|
|
|
|
QNetworkRequest request( url );
|
|
QgsSetRequestInitiatorClass( request, QStringLiteral( "QgsWmsLegendDownloadHandler" ) );
|
|
mSettings.authorization().setAuthorization( request );
|
|
request.setAttribute( QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache );
|
|
request.setAttribute( QNetworkRequest::CacheSaveControlAttribute, true );
|
|
|
|
mReply = mNetworkAccessManager.get( request );
|
|
mSettings.authorization().setAuthorizationReply( mReply );
|
|
connect( mReply, static_cast < void ( QNetworkReply::* )( QNetworkReply::NetworkError ) >( &QNetworkReply::error ), this, &QgsWmsLegendDownloadHandler::errored );
|
|
connect( mReply, &QNetworkReply::finished, this, &QgsWmsLegendDownloadHandler::finished );
|
|
connect( mReply, &QNetworkReply::downloadProgress, this, &QgsWmsLegendDownloadHandler::progressed );
|
|
}
|
|
|
|
void
|
|
QgsWmsLegendDownloadHandler::sendError( const QString &msg )
|
|
{
|
|
QgsDebugMsg( QStringLiteral( "emitting error: %1" ).arg( msg ) );
|
|
Q_ASSERT( mReply );
|
|
mReply->deleteLater();
|
|
mReply = nullptr;
|
|
emit error( msg );
|
|
}
|
|
|
|
void
|
|
QgsWmsLegendDownloadHandler::sendSuccess( const QImage &img )
|
|
{
|
|
QgsDebugMsg( QStringLiteral( "emitting finish: %1x%2 image" ).arg( img.width() ).arg( img.height() ) );
|
|
Q_ASSERT( mReply );
|
|
mReply->deleteLater();
|
|
mReply = nullptr;
|
|
emit finish( img );
|
|
}
|
|
|
|
void
|
|
QgsWmsLegendDownloadHandler::errored( QNetworkReply::NetworkError /* code */ )
|
|
{
|
|
if ( !mReply )
|
|
return;
|
|
|
|
sendError( mReply->errorString() );
|
|
}
|
|
|
|
void
|
|
QgsWmsLegendDownloadHandler::finished()
|
|
{
|
|
if ( !mReply )
|
|
return;
|
|
|
|
// or ::errored() should have been called before ::finished
|
|
Q_ASSERT( mReply->error() == QNetworkReply::NoError );
|
|
|
|
QgsDebugMsg( QStringLiteral( "reply OK" ) );
|
|
QVariant redirect = mReply->attribute( QNetworkRequest::RedirectionTargetAttribute );
|
|
if ( !redirect.isNull() )
|
|
{
|
|
mReply->deleteLater();
|
|
mReply = nullptr;
|
|
startUrl( redirect.toUrl() );
|
|
return;
|
|
}
|
|
|
|
QVariant status = mReply->attribute( QNetworkRequest::HttpStatusCodeAttribute );
|
|
if ( !status.isNull() && status.toInt() >= 400 )
|
|
{
|
|
QVariant phrase = mReply->attribute( QNetworkRequest::HttpReasonPhraseAttribute );
|
|
QString msg( tr( "GetLegendGraphic request error" ) );
|
|
msg += QStringLiteral( " - " );
|
|
msg += QString( tr( "Status: %1\nReason phrase: %2" ) ).arg( status.toInt() ).arg( phrase.toString() );
|
|
sendError( msg );
|
|
return;
|
|
}
|
|
|
|
QImage myLocalImage = QImage::fromData( mReply->readAll() );
|
|
if ( myLocalImage.isNull() )
|
|
{
|
|
QString msg( tr( "Returned legend image is flawed [URL: %1]" )
|
|
.arg( mReply->url().toString() ) );
|
|
sendError( msg );
|
|
return;
|
|
}
|
|
|
|
sendSuccess( myLocalImage );
|
|
}
|
|
|
|
void
|
|
QgsWmsLegendDownloadHandler::progressed( qint64 recv, qint64 tot )
|
|
{
|
|
emit progress( recv, tot );
|
|
}
|
|
|
|
//------
|
|
|
|
QgsCachedImageFetcher::QgsCachedImageFetcher( const QImage &img )
|
|
: _img( img )
|
|
{
|
|
}
|
|
|
|
void QgsCachedImageFetcher::start()
|
|
{
|
|
QTimer::singleShot( 1, this, SLOT( send() ) );
|
|
}
|
|
|
|
|
|
// -----------------------
|
|
|
|
|
|
QgsWmsProviderMetadata::QgsWmsProviderMetadata()
|
|
: QgsProviderMetadata( QgsWmsProvider::WMS_KEY, QgsWmsProvider::WMS_DESCRIPTION )
|
|
{
|
|
}
|
|
|
|
QList<QgsDataItemProvider *> QgsWmsProviderMetadata::dataItemProviders() const
|
|
{
|
|
QList<QgsDataItemProvider *> providers;
|
|
|
|
providers
|
|
<< new QgsWmsDataItemProvider
|
|
<< new QgsXyzTileDataItemProvider;
|
|
|
|
return providers;
|
|
}
|
|
|
|
#ifndef HAVE_STATIC_PROVIDERS
|
|
QGISEXTERN QgsProviderMetadata *providerMetadataFactory()
|
|
{
|
|
return new QgsWmsProviderMetadata();
|
|
}
|
|
#endif
|