Add friendly, descriptive errors when opening a project which has

preset non-default coordinate operations set between a crs pair,
which use grids that are not available on the local system
This commit is contained in:
Nyall Dawson 2019-06-03 13:07:08 +10:00
parent fe4ea3318a
commit 789dbb570b
10 changed files with 161 additions and 3 deletions

View File

@ -212,6 +212,7 @@ be used.
.. versionadded:: 3.8
%End
bool readXml( const QDomElement &element, const QgsReadWriteContext &context, QStringList &missingTransforms /Out/ );
%Docstring
Reads the context's state from a DOM ``element``.

View File

@ -18,6 +18,7 @@
#include "qgsmessagebar.h"
#include "qgsmessagebaritem.h"
#include "qgsmessageoutput.h"
#include "qgsproject.h"
//
// QgsAppMissingRequiredGridHandler
@ -47,9 +48,19 @@ QgsAppMissingGridHandler::QgsAppMissingGridHandler( QObject *parent )
emit coordinateOperationCreationError( sourceCrs, destinationCrs, error );
} );
QgsCoordinateTransform::setCustomMissingGridUsedByContextHandler( [ = ]( const QgsCoordinateReferenceSystem & sourceCrs,
const QgsCoordinateReferenceSystem & destinationCrs,
const QgsDatumTransform::TransformDetails & desired )
{
emit missingGridUsedByContextHandler( sourceCrs, destinationCrs, desired );
} );
connect( this, &QgsAppMissingGridHandler::missingRequiredGrid, this, &QgsAppMissingGridHandler::onMissingRequiredGrid, Qt::QueuedConnection );
connect( this, &QgsAppMissingGridHandler::missingPreferredGrid, this, &QgsAppMissingGridHandler::onMissingPreferredGrid, Qt::QueuedConnection );
connect( this, &QgsAppMissingGridHandler::coordinateOperationCreationError, this, &QgsAppMissingGridHandler::onCoordinateOperationCreationError, Qt::QueuedConnection );
connect( this, &QgsAppMissingGridHandler::missingGridUsedByContextHandler, this, &QgsAppMissingGridHandler::onMissingGridUsedByContextHandler, Qt::QueuedConnection );
connect( QgsProject::instance(), &QgsProject::cleared, this, [ = ] { mAlreadyWarnedPairsForProject.clear(); } );
}
@ -197,6 +208,61 @@ void QgsAppMissingGridHandler::onCoordinateOperationCreationError( const QgsCoor
bar->pushWidget( widget, Qgis::Critical, 0 );
}
void QgsAppMissingGridHandler::onMissingGridUsedByContextHandler( const QgsCoordinateReferenceSystem &sourceCrs, const QgsCoordinateReferenceSystem &destinationCrs, const QgsDatumTransform::TransformDetails &desired )
{
if ( !shouldWarnAboutPairForCurrentProject( sourceCrs, destinationCrs ) )
return;
const QString shortMessage = tr( "Cannot use project transform between %1 and %2" ).arg( displayIdentifierForCrs( sourceCrs, true ),
displayIdentifierForCrs( destinationCrs, true ) );
QString gridMessage;
for ( const QgsDatumTransform::GridDetails &grid : desired.grids )
{
if ( !grid.isAvailable )
{
QString m = tr( "This transformation requires the grid file “%1”, which is not available for use on the system." ).arg( grid.shortName );
if ( !grid.url.isEmpty() )
{
if ( !grid.packageName.isEmpty() )
{
m += ' ' + tr( "This grid is part of the <i>%1</i> package, available for download from <a href=\"%2\">%2</a>." ).arg( grid.packageName, grid.url );
}
else
{
m += ' ' + tr( "This grid is available for download from <a href=\"%1\">%1</a>." ).arg( grid.url );
}
}
gridMessage += QStringLiteral( "<li>%1</li>" ).arg( m );
}
}
if ( !gridMessage.isEmpty() )
{
gridMessage = "<ul>" + gridMessage + "</ul>";
}
const QString longMessage = tr( "<p>This project specifies a preset transform between <i>%1</i> and <i>%2</i>, which is not available for use on the system.</p>" ).arg( displayIdentifierForCrs( sourceCrs ),
displayIdentifierForCrs( destinationCrs ) )
+ gridMessage
+ tr( "<p>The operation specified for use in the project is:</p><p><code>%1</code></p>" ).arg( desired.proj ) ;
QgsMessageBar *bar = QgisApp::instance()->messageBar();
QgsMessageBarItem *widget = bar->createMessage( QString(), shortMessage );
QPushButton *detailsButton = new QPushButton( tr( "Details" ) );
connect( detailsButton, &QPushButton::clicked, this, [longMessage]
{
// dlg has deleted on close
QgsMessageOutput * dlg( QgsMessageOutput::createMessageOutput() );
dlg->setTitle( tr( "Project Transformation Not Available" ) );
dlg->setMessage( longMessage, QgsMessageOutput::MessageHtml );
dlg->showMessage();
} );
widget->layout()->addWidget( detailsButton );
bar->pushWidget( widget, Qgis::Critical, 0 );
}
bool QgsAppMissingGridHandler::shouldWarnAboutPair( const QgsCoordinateReferenceSystem &source, const QgsCoordinateReferenceSystem &dest )
{
if ( mAlreadyWarnedPairs.contains( qMakePair( source, dest ) ) || mAlreadyWarnedPairs.contains( qMakePair( dest, source ) ) )
@ -207,3 +273,14 @@ bool QgsAppMissingGridHandler::shouldWarnAboutPair( const QgsCoordinateReference
mAlreadyWarnedPairs.append( qMakePair( source, dest ) );
return true;
}
bool QgsAppMissingGridHandler::shouldWarnAboutPairForCurrentProject( const QgsCoordinateReferenceSystem &source, const QgsCoordinateReferenceSystem &dest )
{
if ( mAlreadyWarnedPairsForProject.contains( qMakePair( source, dest ) ) || mAlreadyWarnedPairsForProject.contains( qMakePair( dest, source ) ) )
{
return false;
}
mAlreadyWarnedPairsForProject.append( qMakePair( source, dest ) );
return true;
}

View File

@ -45,6 +45,10 @@ class QgsAppMissingGridHandler : public QObject
const QgsCoordinateReferenceSystem &destinationCrs,
const QString &error );
void missingGridUsedByContextHandler( const QgsCoordinateReferenceSystem &sourceCrs,
const QgsCoordinateReferenceSystem &destinationCrs,
const QgsDatumTransform::TransformDetails &desired );
private slots:
void onMissingRequiredGrid( const QgsCoordinateReferenceSystem &sourceCrs,
@ -59,11 +63,17 @@ class QgsAppMissingGridHandler : public QObject
void onCoordinateOperationCreationError( const QgsCoordinateReferenceSystem &sourceCrs,
const QgsCoordinateReferenceSystem &destinationCrs,
const QString &error );
void onMissingGridUsedByContextHandler( const QgsCoordinateReferenceSystem &sourceCrs,
const QgsCoordinateReferenceSystem &destinationCrs,
const QgsDatumTransform::TransformDetails &desired );
private:
bool shouldWarnAboutPair( const QgsCoordinateReferenceSystem &source, const QgsCoordinateReferenceSystem &dest );
bool shouldWarnAboutPairForCurrentProject( const QgsCoordinateReferenceSystem &source, const QgsCoordinateReferenceSystem &dest );
QList< QPair< QgsCoordinateReferenceSystem, QgsCoordinateReferenceSystem > > mAlreadyWarnedPairs;
QList< QPair< QgsCoordinateReferenceSystem, QgsCoordinateReferenceSystem > > mAlreadyWarnedPairsForProject;
};

View File

@ -969,3 +969,8 @@ void QgsCoordinateTransform::setCustomCoordinateOperationCreationErrorHandler( c
{
QgsCoordinateTransformPrivate::setCustomCoordinateOperationCreationErrorHandler( handler );
}
void QgsCoordinateTransform::setCustomMissingGridUsedByContextHandler( const std::function<void ( const QgsCoordinateReferenceSystem &, const QgsCoordinateReferenceSystem &, const QgsDatumTransform::TransformDetails & )> &handler )
{
QgsCoordinateTransformPrivate::setCustomMissingGridUsedByContextHandler( handler );
}

View File

@ -447,6 +447,7 @@ class CORE_EXPORT QgsCoordinateTransform
*
* \see setCustomMissingPreferredGridHandler()
* \see setCustomCoordinateOperationCreationErrorHandler()
* \see setCustomMissingGridUsedByContextHandler()
*
* \note Not available in Python bindings
* \since QGIS 3.8
@ -466,6 +467,7 @@ class CORE_EXPORT QgsCoordinateTransform
*
* \see setCustomMissingRequiredGridHandler()
* \see setCustomCoordinateOperationCreationErrorHandler()
* \see setCustomMissingGridUsedByContextHandler()
*
* \note Not available in Python bindings
* \since QGIS 3.8
@ -482,6 +484,7 @@ class CORE_EXPORT QgsCoordinateTransform
*
* \see setCustomMissingRequiredGridHandler()
* \see setCustomMissingPreferredGridHandler()
* \see setCustomMissingGridUsedByContextHandler()
*
* \note Not available in Python bindings
* \since QGIS 3.8
@ -490,6 +493,21 @@ class CORE_EXPORT QgsCoordinateTransform
const QgsCoordinateReferenceSystem &destinationCrs,
const QString &error )> &handler );
/**
* Sets a custom handler to use when a coordinate operation was specified for use between \a sourceCrs and
* \a destinationCrs by the transform context, yet the coordinate operation could not be created. The \a desiredOperation argument
* specifies the desired transform details as specified by the context.
*
* \see setCustomMissingRequiredGridHandler()
* \see setCustomMissingPreferredGridHandler()
* \see setCustomCoordinateOperationCreationErrorHandler()
*
* \note Not available in Python bindings
* \since QGIS 3.8
*/
static void setCustomMissingGridUsedByContextHandler( const std::function< void( const QgsCoordinateReferenceSystem &sourceCrs,
const QgsCoordinateReferenceSystem &destinationCrs,
const QgsDatumTransform::TransformDetails &desiredOperation )> &handler );
#endif
private:

View File

@ -47,6 +47,10 @@ std::function< void( const QgsCoordinateReferenceSystem &sourceCrs,
const QgsCoordinateReferenceSystem &destinationCrs,
const QString &error )> QgsCoordinateTransformPrivate::sCoordinateOperationCreationErrorHandler = nullptr;
std::function< void( const QgsCoordinateReferenceSystem &sourceCrs,
const QgsCoordinateReferenceSystem &destinationCrs,
const QgsDatumTransform::TransformDetails &desiredOperation )> QgsCoordinateTransformPrivate::sMissingGridUsedByContextHandler = nullptr;
#if PROJ_VERSION_MAJOR<6
#ifdef USE_THREAD_LOCAL
thread_local QgsProjContextStore QgsCoordinateTransformPrivate::mProjContext;
@ -298,7 +302,29 @@ ProjData QgsCoordinateTransformPrivate::threadLocalProjData()
#if PROJ_VERSION_MAJOR>=6
QgsProjUtils::proj_pj_unique_ptr transform;
if ( !mProjCoordinateOperation.isEmpty() )
{
transform.reset( proj_create( context, mProjCoordinateOperation.toUtf8().constData() ) );
if ( !transform || !proj_coordoperation_is_instantiable( context, transform.get() ) )
{
if ( sMissingGridUsedByContextHandler )
{
QgsDatumTransform::TransformDetails desired;
desired.proj = mProjCoordinateOperation;
desired.accuracy = -1; //unknown, can't retrieve from proj as we can't instantiate the op
desired.grids = QgsProjUtils::gridsUsed( mProjCoordinateOperation );
sMissingGridUsedByContextHandler( mSourceCRS, mDestCRS, desired );
}
else
{
const QString err = QObject::tr( "Could not use operation specified in project between %1 and %2. (Wanted to use: %3)." ).arg( mSourceCRS.authid() )
.arg( mDestCRS.authid() )
.arg( mProjCoordinateOperation );
QgsMessageLog::logMessage( err, QString(), Qgis::Critical );
}
transform.reset();
}
}
QString nonAvailableError;
if ( !transform ) // fallback on default proj pathway
@ -508,6 +534,11 @@ void QgsCoordinateTransformPrivate::setCustomCoordinateOperationCreationErrorHan
sCoordinateOperationCreationErrorHandler = handler;
}
void QgsCoordinateTransformPrivate::setCustomMissingGridUsedByContextHandler( const std::function<void ( const QgsCoordinateReferenceSystem &, const QgsCoordinateReferenceSystem &, const QgsDatumTransform::TransformDetails & )> &handler )
{
sMissingGridUsedByContextHandler = handler;
}
#if PROJ_VERSION_MAJOR<6
QString QgsCoordinateTransformPrivate::stripDatumTransform( const QString &proj4 ) const
{

View File

@ -182,6 +182,17 @@ class QgsCoordinateTransformPrivate : public QSharedData
const QgsCoordinateReferenceSystem &destinationCrs,
const QString &error )> &handler );
/**
* Sets a custom handler to use when a coordinate operation was specified for use between \a sourceCrs and
* \a destinationCrs by the transform context, yet the coordinate operation could not be created. The \a desiredOperation argument
* specifies the desired transform details as specified by the context.
*
* \since QGIS 3.8
*/
static void setCustomMissingGridUsedByContextHandler( const std::function< void( const QgsCoordinateReferenceSystem &sourceCrs,
const QgsCoordinateReferenceSystem &destinationCrs,
const QgsDatumTransform::TransformDetails &desiredOperation )> &handler );
private:
#if PROJ_VERSION_MAJOR<6
@ -206,6 +217,10 @@ class QgsCoordinateTransformPrivate : public QSharedData
static std::function< void( const QgsCoordinateReferenceSystem &sourceCrs,
const QgsCoordinateReferenceSystem &destinationCrs,
const QString &error )> sCoordinateOperationCreationErrorHandler;
static std::function< void( const QgsCoordinateReferenceSystem &sourceCrs,
const QgsCoordinateReferenceSystem &destinationCrs,
const QgsDatumTransform::TransformDetails &desiredOperation )> sMissingGridUsedByContextHandler;
};
/// @endcond

View File

@ -223,8 +223,7 @@ bool QgsCoordinateTransformContext::readXml( const QDomElement &element, const Q
if ( !QgsProjUtils::coordinateOperationIsAvailable( coordinateOp ) )
{
// not possible in current Proj 6 api!
// missingTransforms.append( QgsProjUtils::nonAvailableGrids( coordinateOp ) );
missingTransforms.append( coordinateOp ); // yuck, we don't want to expose this string to users!
// QgsCoordinateTransform will alert users to this, we don't need to use missingTransforms here
result = false;
}

View File

@ -205,6 +205,8 @@ class CORE_EXPORT QgsCoordinateTransformContext
*/
QString calculateCoordinateOperation( const QgsCoordinateReferenceSystem &source, const QgsCoordinateReferenceSystem &destination ) const;
// TODO QGIS 4.0 - remove missingTransforms, not used for Proj >= 6.0 builds
/**
* Reads the context's state from a DOM \a element.
*

View File

@ -1232,7 +1232,7 @@ bool QgsProject::readProjectFile( const QString &filename )
mCrs = projectCrs;
QStringList datumErrors;
if ( !mTransformContext.readXml( doc->documentElement(), context, datumErrors ) )
if ( !mTransformContext.readXml( doc->documentElement(), context, datumErrors ) && !datumErrors.empty() )
{
emit missingDatumTransforms( datumErrors );
}