271 changes: 192 additions & 79 deletions src/providers/wcs/qgswcsprovider.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
#include <QImageReader>
#include <QPainter>
#include <QPixmap>
#include <QRegExp>
#include <QSet>
#include <QSettings>
#include <QEventLoop>
Expand Down Expand Up @@ -132,6 +133,8 @@ QgsWcsProvider::QgsWcsProvider( QString const &uri )
mHeight = mCoverageSummary->height;
mHasSize = mCoverageSummary->hasSize;

QgsDebugMsg( QString( "mWidth = %1 mHeight = %2" ).arg( mWidth ).arg( mHeight ) ) ;

if ( !calculateExtent() )
{
QgsDebugMsg( "Cannot calculate extent" );
Expand Down Expand Up @@ -417,13 +420,13 @@ void QgsWcsProvider::getCache( int bandNo, QgsRectangle const & viewExtent, int
clearCache();

// --------------- BOUNDING BOX --------------------------------
//according to the WCS spec for 1.3, some CRS have inverted axis
bool changeXY = false;
//according to the WCS spec for 1.1, some CRS have inverted axis
// box:
// 1.0.0: minx,miny,maxx,maxy
// 1.1.0, 1.1.2: OGC 07-067r5 (WCS 1.1.2) referes to OGC 06-121r3 which says
// "The number of axes included, and the order of these axes, shall be as specified
// by the referenced CRS." That means inverted for geographic.
bool changeXY = false;
if ( !mIgnoreAxisOrientation && ( mCapabilities.version().startsWith( "1.1" ) ) )
{
//create CRS from string
Expand All @@ -434,55 +437,72 @@ void QgsWcsProvider::getCache( int bandNo, QgsRectangle const & viewExtent, int
}
}

if ( mInvertAxisOrientation )
changeXY = !changeXY;
if ( mInvertAxisOrientation ) changeXY = !changeXY;

// Bounding box in WCS format (Warning: does not work with scientific notation)
QString bbox = QString( changeXY ? "%2,%1,%4,%3" : "%1,%2,%3,%4" )
.arg( viewExtent.xMinimum(), 0, 'f', 16 )
.arg( viewExtent.yMinimum(), 0, 'f', 16 )
.arg( viewExtent.xMaximum(), 0, 'f', 16 )
.arg( viewExtent.yMaximum(), 0, 'f', 16 );

// ---------------- CRS -----------------------------------------
// 1.0.0:
// CRS - CRS of request
// RESPONSE_CRS - requested CRS of response
// 1.1.0, 1.1.2
// GridBaseCRS=urn:ogc:def:crs:SG:6.6:32618
// GridType=urn:ogc:def:method:CS:1.1:2dGridIn2dCrs
// GridCS=urn:ogc:def:cs:OGC:0Grid2dSquareCS
// GridOrigin=0,0
// GridOffsets=0.0707,-0.0707,0.1414,0.1414&

QString crsKey = "RESPONSE_CRS";
// TODO
/*
if ( mCapabilities.version == "1.3.0" || mCapabilities.version == "1.3" )
double xRes = viewExtent.width() / pixelWidth;
double yRes = viewExtent.height() / pixelHeight;
QgsRectangle extent = viewExtent;
if ( mCapabilities.version().startsWith( "1.1" ) )
{
crsKey = "CRS";
// WCS 1.1 grid is using cell centers -> shrink the extent to border cells centers by half cell size
extent = QgsRectangle( viewExtent.xMinimum() + xRes / 2., viewExtent.yMinimum() + yRes / 2., viewExtent.xMaximum() - xRes / 2., viewExtent.yMaximum() - yRes / 2. );
}
*/

QString identifierKey = "COVERAGE";
if ( mCapabilities.version().startsWith( "1.1" ) ) identifierKey = "IDENTIFIER";
// Bounding box in WCS format (Warning: does not work with scientific notation)
QString bbox = QString( changeXY ? "%2,%1,%4,%3" : "%1,%2,%3,%4" )
.arg( extent.xMinimum(), 0, 'f', 16 )
.arg( extent.yMinimum(), 0, 'f', 16 )
.arg( extent.xMaximum(), 0, 'f', 16 )
.arg( extent.yMaximum(), 0, 'f', 16 );

QUrl url( mIgnoreGetMapUrl ? mBaseUrl : mCapabilities.getCoverageUrl() );

// The same for 1.0.0, 1.1.0, 1.1.2
// Version 1.0.0, 1.1.0, 1.1.2
setQueryItem( url, "SERVICE", "WCS" );
setQueryItem( url, "VERSION", mCapabilities.version() );
setQueryItem( url, "REQUEST", "GetCoverage" );

// Different for each version
setQueryItem( url, "BBOX", bbox );
setQueryItem( url, "CRS", mCoverageCrs ); // 1.0.0 - BOX is in coverage CRS
setQueryItem( url, crsKey, mCoverageCrs );
setQueryItem( url, "WIDTH", QString::number( pixelWidth ) );
setQueryItem( url, "HEIGHT", QString::number( pixelHeight ) );
setQueryItem( url, identifierKey, mIdentifier );
setQueryItem( url, "FORMAT", mFormat );

// Version 1.0.0
if ( mCapabilities.version().startsWith( "1.0" ) )
{
setQueryItem( url, "COVERAGE", mIdentifier );
setQueryItem( url, "BBOX", bbox );
setQueryItem( url, "CRS", mCoverageCrs ); // request BBOX CRS
setQueryItem( url, "RESPONSE_CRS", mCoverageCrs ); // response CRS
setQueryItem( url, "WIDTH", QString::number( pixelWidth ) );
setQueryItem( url, "HEIGHT", QString::number( pixelHeight ) );
}

// Version 1.1.0, 1.1.2
if ( mCapabilities.version().startsWith( "1.1" ) )
{
setQueryItem( url, "IDENTIFIER", mIdentifier );
QString crsUrn = QString( "urn:ogc:def:crs:%1::%2" ).arg( mCoverageCrs.split( ':' ).value( 0 ) ).arg( mCoverageCrs.split( ':' ).value( 1 ) );
bbox += "," + crsUrn;
setQueryItem( url, "BOUNDINGBOX", bbox );

// GridBaseCRS=urn:ogc:def:crs:SG:6.6:32618
// GridType=urn:ogc:def:method:CS:1.1:2dGridIn2dCrs
// GridCS=urn:ogc:def:cs:OGC:0Grid2dSquareCS
// GridOrigin=0,0
// GridOffsets=0.0707,-0.0707,0.1414,0.1414&

setQueryItem( url, "GRIDBASECRS", crsUrn ); // response CRS
// GridOrigin is BBOX minx, maxy
// TODO: invert axis, pixels centers
QString gridOrigin = QString( changeXY ? "%2,%1" : "%1,%2" )
.arg( extent.xMinimum(), 0, 'f', 16 )
.arg( extent.yMaximum(), 0, 'f', 16 );
setQueryItem( url, "GRIDORIGIN", gridOrigin );

QString gridOffsets = QString( changeXY ? "%2,%1" : "%1,%2" )
.arg( xRes, 0, 'f', 16 )
.arg( yRes, 0, 'f', 16 );
setQueryItem( url, "GRIDOFFSETS", gridOffsets );
}


QgsDebugMsg( QString( "GetCoverage: %1" ).arg( url.toString() ) );

// cache some details for if the user wants to do an identifyAsHtml() later
Expand Down Expand Up @@ -571,69 +591,158 @@ void QgsWcsProvider::cacheReplyFinished()
return;
}

// Read response
clearCache(); // should not be necessary

QString contentType = mCacheReply->header( QNetworkRequest::ContentTypeHeader ).toString();
QgsDebugMsg( "contentType: " + contentType );
//if ( contentType.startsWith( "image/", Qt::CaseInsensitive ) )
// TODO: How to recognize easily coverage data from exception?
if ( !contentType.startsWith( "text/", Qt::CaseInsensitive ) &&
contentType.toLower() != "application/vnd.ogc.se_xml" )

// Exception
if ( contentType.startsWith( "text/", Qt::CaseInsensitive ) ||
contentType.toLower() == "application/vnd.ogc.se_xml" )
{
// Create memory file
clearCache();
mCachedData = mCacheReply->readAll();
if ( mCachedData.size() == 0 )
// TODO: test also if it is really exception (from content)
QByteArray text = mCacheReply->readAll();
if ( contentType.toLower() == "text/xml" && parseServiceExceptionReportDom( text ) )
{
QgsMessageLog::logMessage( tr( "No data recieved" ), tr( "WCS" ) );
clearCache();
return;
QgsMessageLog::logMessage( tr( "Map request error (Title:%1; Error:%2; URL: %3)" )
.arg( mErrorCaption ).arg( mError )
.arg( mCacheReply->url().toString() ), tr( "WCS" ) );
}
else
{
QgsMessageLog::logMessage( tr( "Map request error (Status: %1; Response: %2; URL:%3)" )
.arg( status.toInt() )
.arg( QString::fromUtf8( text ) )
.arg( mCacheReply->url().toString() ), tr( "WCS" ) );
}
QgsDebugMsg( QString( "%1 bytes recieved" ).arg( mCachedData.size() ) );

mCachedMemFile = VSIFileFromMemBuffer( TO8F( mCachedMemFilename ),
( GByte* )mCachedData.data(),
( vsi_l_offset )mCachedData.size(),
FALSE );
mCacheReply->deleteLater();
mCacheReply = 0;

return;
}

// WCS 1.1 gives response as multipart, e.g.
// multipart/mixed; boundary=wcs
// multipart/mixed; boundary="wcs"\n
if ( contentType.startsWith( "multipart/", Qt::CaseInsensitive ) )
{
// It seams that Qt does not have currently support for multipart reply
// and it is not even possible to create QNetworkReply from raw data
// so we have to parse response
QRegExp re( ".*boundary=\"?([^\"]+)\"?\\s?", Qt::CaseInsensitive );

if ( !mCachedMemFile )
if ( !re.indexIn( contentType ) == 0 )
{
QgsMessageLog::logMessage( tr( "Cannot create memory file" ), tr( "WCS" ) );
QgsMessageLog::logMessage( tr( "Cannot find boundary in multipart content type" ), tr( "WCS" ) );
clearCache();
mCacheReply->deleteLater();
mCacheReply = 0;
return;
}
QgsDebugMsg( "Memory file created" );

CPLErrorReset();
mCachedGdalDataset = GDALOpen( TO8F( mCachedMemFilename ), GA_ReadOnly );
if ( !mCachedGdalDataset )
QString boundary = re.cap( 1 );
QgsDebugMsg( "boundary = " + boundary );
boundary = "--" + boundary;

// Lines should be terminated by CRLF ("\r\n") but any new line combination may appear
QByteArray data = mCacheReply->readAll();
int from, to;
from = data.indexOf( boundary.toAscii(), 0 ) + boundary.length() + 1 ;
QVector<QByteArray> partHeaders;
QVector<QByteArray> partBodies;
while ( true )
{
QgsMessageLog::logMessage( QString::fromUtf8( CPLGetLastErrorMsg() ), tr( "WCS" ) );
to = data.indexOf( boundary.toAscii(), from );
if ( to < 0 ) break;
QgsDebugMsg( QString( "part %1 - %2" ).arg( from ).arg( to ) );
QByteArray part = data.mid( from, to - from );
// Remove possible new line from beginning
while ( part.size() > 0 && ( part.at( 0 ) == '\r' || part.at( 0 ) == '\n' ) )
{
part.remove( 0, 1 );
}
// Split header and data (find empty new line)
// New lines should be CRLF, but we support also CRLFCRLF, LFLF to find empty line
int pos = 0; // body start
while ( pos < part.size() - 1 )
{
if ( part.at( pos ) == '\n' && ( part.at( pos + 1 ) == '\n' || part.at( pos + 1 ) == '\r' ) )
{
if ( part.at( pos + 1 ) == '\r' ) pos++;
pos += 2;
break;
}
pos++;
}
partHeaders.append( part.left( pos ) );
partBodies.append( part.mid( pos ) );
QgsDebugMsg( "Part header:\n" + partHeaders.last() );

from = to + boundary.length() + 1;
}
if ( partBodies.size() < 2 )
{
QgsMessageLog::logMessage( tr( "Expected 2 parts, %1 recieved" ).arg( partBodies.size() ), tr( "WCS" ) );
clearCache();
mCacheReply->deleteLater();
mCacheReply = 0;
return;
}
QgsDebugMsg( "Dataset opened" );
else if ( partBodies.size() > 2 )
{
// We will try the second one
QgsMessageLog::logMessage( tr( "More than 2 parts (%1) recieved" ).arg( partBodies.size() ), tr( "WCS" ) );
}
mCachedData = partBodies.value( 1 );
}
else
{
QByteArray text = mCacheReply->readAll();
if ( contentType.toLower() == "text/xml" && parseServiceExceptionReportDom( text ) )
{
QgsMessageLog::logMessage( tr( "Map request error (Title:%1; Error:%2; URL: %3)" )
.arg( mErrorCaption ).arg( mError )
.arg( mCacheReply->url().toString() ), tr( "WCS" ) );
}
else
{
QgsMessageLog::logMessage( tr( "Map request error (Status: %1; Response: %2; URL:%3)" )
.arg( status.toInt() )
.arg( QString::fromUtf8( text ) )
.arg( mCacheReply->url().toString() ), tr( "WCS" ) );
}
// Mime types for WCS 1.0 response should be usually image/<format>
// (image/tiff, image/png, image/jpeg, image/png; mode=8bit, etc.)
// but other mime types (like application/*) may probably also appear

// Create memory file
mCachedData = mCacheReply->readAll();
}

if ( mCachedData.size() == 0 )
{
QgsMessageLog::logMessage( tr( "No data recieved" ), tr( "WCS" ) );
clearCache();
mCacheReply->deleteLater();
mCacheReply = 0;
return;
}
QgsDebugMsg( QString( "%1 bytes recieved" ).arg( mCachedData.size() ) );

mCachedMemFile = VSIFileFromMemBuffer( TO8F( mCachedMemFilename ),
( GByte* )mCachedData.data(),
( vsi_l_offset )mCachedData.size(),
FALSE );

if ( !mCachedMemFile )
{
QgsMessageLog::logMessage( tr( "Cannot create memory file" ), tr( "WCS" ) );
clearCache();
mCacheReply->deleteLater();
mCacheReply = 0;
return;
}
QgsDebugMsg( "Memory file created" );

CPLErrorReset();
mCachedGdalDataset = GDALOpen( TO8F( mCachedMemFilename ), GA_ReadOnly );
if ( !mCachedGdalDataset )
{
QgsMessageLog::logMessage( QString::fromUtf8( CPLGetLastErrorMsg() ), tr( "WCS" ) );
clearCache();
mCacheReply->deleteLater();
mCacheReply = 0;
return;
}
QgsDebugMsg( "Dataset opened" );

mCacheReply->deleteLater();
mCacheReply = 0;
Expand Down Expand Up @@ -762,15 +871,21 @@ void QgsWcsProvider::clearCache()
QgsDebugMsg( "Entered" );
if ( mCachedGdalDataset )
{
QgsDebugMsg( "Close mCachedGdalDataset" );
GDALClose( mCachedGdalDataset );
mCachedGdalDataset = 0;
QgsDebugMsg( "Closed" );
}
if ( mCachedMemFile )
{
QgsDebugMsg( "Close mCachedMemFile" );
VSIFCloseL( mCachedMemFile );
mCachedMemFile = 0;
QgsDebugMsg( "Closed" );
}
QgsDebugMsg( "Clear mCachedData" );
mCachedData.clear();
QgsDebugMsg( "Cleared" );
}

void QgsWcsProvider::cacheReplyProgress( qint64 bytesReceived, qint64 bytesTotal )
Expand Down Expand Up @@ -1414,8 +1529,6 @@ QString QgsWcsProvider::description() const

void QgsWcsProvider::reloadData()
{
//delete mCachedImage;
//mCachedImage = 0;
clearCache();
}

Expand Down
3 changes: 2 additions & 1 deletion src/providers/wcs/qgswcsprovider.h
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,8 @@ class QgsWcsProvider : public QgsRasterDataProvider
*/
QMap<QString, bool> mQueryableForLayer;

/** WCS CRS type of the coverage CRS requested from the WCS server */
/** Coverage CRS used for requests in Auth */
// TODO: use QgsCoordinateReferenceSystem ?
QString mCoverageCrs;

/** The reply to the on going request to fill the cache */
Expand Down
6 changes: 5 additions & 1 deletion src/providers/wcs/qgswcssourceselect.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,11 @@ void QgsWCSSourceSelect::on_mLayersTreeWidget_itemSelectionChanged()
QString identifier = selectedIdentifier();
if ( identifier.isEmpty() ) { return; }

mCapabilities.describeCoverage( identifier ); // 1.0 get additional info
if ( mCapabilities.version().startsWith( "1.0" ) )
{
// 1.0 get additional info
mCapabilities.describeCoverage( identifier );
}

populateFormats();

Expand Down