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 .. versionadded:: 3.8
%End %End
bool readXml( const QDomElement &element, const QgsReadWriteContext &context, QStringList &missingTransforms /Out/ ); bool readXml( const QDomElement &element, const QgsReadWriteContext &context, QStringList &missingTransforms /Out/ );
%Docstring %Docstring
Reads the context's state from a DOM ``element``. Reads the context's state from a DOM ``element``.

View File

@ -18,6 +18,7 @@
#include "qgsmessagebar.h" #include "qgsmessagebar.h"
#include "qgsmessagebaritem.h" #include "qgsmessagebaritem.h"
#include "qgsmessageoutput.h" #include "qgsmessageoutput.h"
#include "qgsproject.h"
// //
// QgsAppMissingRequiredGridHandler // QgsAppMissingRequiredGridHandler
@ -47,9 +48,19 @@ QgsAppMissingGridHandler::QgsAppMissingGridHandler( QObject *parent )
emit coordinateOperationCreationError( sourceCrs, destinationCrs, error ); 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::missingRequiredGrid, this, &QgsAppMissingGridHandler::onMissingRequiredGrid, Qt::QueuedConnection );
connect( this, &QgsAppMissingGridHandler::missingPreferredGrid, this, &QgsAppMissingGridHandler::onMissingPreferredGrid, Qt::QueuedConnection ); connect( this, &QgsAppMissingGridHandler::missingPreferredGrid, this, &QgsAppMissingGridHandler::onMissingPreferredGrid, Qt::QueuedConnection );
connect( this, &QgsAppMissingGridHandler::coordinateOperationCreationError, this, &QgsAppMissingGridHandler::onCoordinateOperationCreationError, 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 ); 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 ) bool QgsAppMissingGridHandler::shouldWarnAboutPair( const QgsCoordinateReferenceSystem &source, const QgsCoordinateReferenceSystem &dest )
{ {
if ( mAlreadyWarnedPairs.contains( qMakePair( source, dest ) ) || mAlreadyWarnedPairs.contains( qMakePair( dest, source ) ) ) 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 ) ); mAlreadyWarnedPairs.append( qMakePair( source, dest ) );
return true; 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 QgsCoordinateReferenceSystem &destinationCrs,
const QString &error ); const QString &error );
void missingGridUsedByContextHandler( const QgsCoordinateReferenceSystem &sourceCrs,
const QgsCoordinateReferenceSystem &destinationCrs,
const QgsDatumTransform::TransformDetails &desired );
private slots: private slots:
void onMissingRequiredGrid( const QgsCoordinateReferenceSystem &sourceCrs, void onMissingRequiredGrid( const QgsCoordinateReferenceSystem &sourceCrs,
@ -59,11 +63,17 @@ class QgsAppMissingGridHandler : public QObject
void onCoordinateOperationCreationError( const QgsCoordinateReferenceSystem &sourceCrs, void onCoordinateOperationCreationError( const QgsCoordinateReferenceSystem &sourceCrs,
const QgsCoordinateReferenceSystem &destinationCrs, const QgsCoordinateReferenceSystem &destinationCrs,
const QString &error ); const QString &error );
void onMissingGridUsedByContextHandler( const QgsCoordinateReferenceSystem &sourceCrs,
const QgsCoordinateReferenceSystem &destinationCrs,
const QgsDatumTransform::TransformDetails &desired );
private: private:
bool shouldWarnAboutPair( const QgsCoordinateReferenceSystem &source, const QgsCoordinateReferenceSystem &dest ); 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 > > mAlreadyWarnedPairs;
QList< QPair< QgsCoordinateReferenceSystem, QgsCoordinateReferenceSystem > > mAlreadyWarnedPairsForProject;
}; };

View File

@ -969,3 +969,8 @@ void QgsCoordinateTransform::setCustomCoordinateOperationCreationErrorHandler( c
{ {
QgsCoordinateTransformPrivate::setCustomCoordinateOperationCreationErrorHandler( handler ); 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 setCustomMissingPreferredGridHandler()
* \see setCustomCoordinateOperationCreationErrorHandler() * \see setCustomCoordinateOperationCreationErrorHandler()
* \see setCustomMissingGridUsedByContextHandler()
* *
* \note Not available in Python bindings * \note Not available in Python bindings
* \since QGIS 3.8 * \since QGIS 3.8
@ -466,6 +467,7 @@ class CORE_EXPORT QgsCoordinateTransform
* *
* \see setCustomMissingRequiredGridHandler() * \see setCustomMissingRequiredGridHandler()
* \see setCustomCoordinateOperationCreationErrorHandler() * \see setCustomCoordinateOperationCreationErrorHandler()
* \see setCustomMissingGridUsedByContextHandler()
* *
* \note Not available in Python bindings * \note Not available in Python bindings
* \since QGIS 3.8 * \since QGIS 3.8
@ -482,6 +484,7 @@ class CORE_EXPORT QgsCoordinateTransform
* *
* \see setCustomMissingRequiredGridHandler() * \see setCustomMissingRequiredGridHandler()
* \see setCustomMissingPreferredGridHandler() * \see setCustomMissingPreferredGridHandler()
* \see setCustomMissingGridUsedByContextHandler()
* *
* \note Not available in Python bindings * \note Not available in Python bindings
* \since QGIS 3.8 * \since QGIS 3.8
@ -490,6 +493,21 @@ class CORE_EXPORT QgsCoordinateTransform
const QgsCoordinateReferenceSystem &destinationCrs, const QgsCoordinateReferenceSystem &destinationCrs,
const QString &error )> &handler ); 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 #endif
private: private:

View File

@ -47,6 +47,10 @@ std::function< void( const QgsCoordinateReferenceSystem &sourceCrs,
const QgsCoordinateReferenceSystem &destinationCrs, const QgsCoordinateReferenceSystem &destinationCrs,
const QString &error )> QgsCoordinateTransformPrivate::sCoordinateOperationCreationErrorHandler = nullptr; 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 #if PROJ_VERSION_MAJOR<6
#ifdef USE_THREAD_LOCAL #ifdef USE_THREAD_LOCAL
thread_local QgsProjContextStore QgsCoordinateTransformPrivate::mProjContext; thread_local QgsProjContextStore QgsCoordinateTransformPrivate::mProjContext;
@ -298,7 +302,29 @@ ProjData QgsCoordinateTransformPrivate::threadLocalProjData()
#if PROJ_VERSION_MAJOR>=6 #if PROJ_VERSION_MAJOR>=6
QgsProjUtils::proj_pj_unique_ptr transform; QgsProjUtils::proj_pj_unique_ptr transform;
if ( !mProjCoordinateOperation.isEmpty() ) if ( !mProjCoordinateOperation.isEmpty() )
{
transform.reset( proj_create( context, mProjCoordinateOperation.toUtf8().constData() ) ); 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; QString nonAvailableError;
if ( !transform ) // fallback on default proj pathway if ( !transform ) // fallback on default proj pathway
@ -508,6 +534,11 @@ void QgsCoordinateTransformPrivate::setCustomCoordinateOperationCreationErrorHan
sCoordinateOperationCreationErrorHandler = handler; sCoordinateOperationCreationErrorHandler = handler;
} }
void QgsCoordinateTransformPrivate::setCustomMissingGridUsedByContextHandler( const std::function<void ( const QgsCoordinateReferenceSystem &, const QgsCoordinateReferenceSystem &, const QgsDatumTransform::TransformDetails & )> &handler )
{
sMissingGridUsedByContextHandler = handler;
}
#if PROJ_VERSION_MAJOR<6 #if PROJ_VERSION_MAJOR<6
QString QgsCoordinateTransformPrivate::stripDatumTransform( const QString &proj4 ) const QString QgsCoordinateTransformPrivate::stripDatumTransform( const QString &proj4 ) const
{ {

View File

@ -182,6 +182,17 @@ class QgsCoordinateTransformPrivate : public QSharedData
const QgsCoordinateReferenceSystem &destinationCrs, const QgsCoordinateReferenceSystem &destinationCrs,
const QString &error )> &handler ); 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: private:
#if PROJ_VERSION_MAJOR<6 #if PROJ_VERSION_MAJOR<6
@ -206,6 +217,10 @@ class QgsCoordinateTransformPrivate : public QSharedData
static std::function< void( const QgsCoordinateReferenceSystem &sourceCrs, static std::function< void( const QgsCoordinateReferenceSystem &sourceCrs,
const QgsCoordinateReferenceSystem &destinationCrs, const QgsCoordinateReferenceSystem &destinationCrs,
const QString &error )> sCoordinateOperationCreationErrorHandler; const QString &error )> sCoordinateOperationCreationErrorHandler;
static std::function< void( const QgsCoordinateReferenceSystem &sourceCrs,
const QgsCoordinateReferenceSystem &destinationCrs,
const QgsDatumTransform::TransformDetails &desiredOperation )> sMissingGridUsedByContextHandler;
}; };
/// @endcond /// @endcond

View File

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

View File

@ -205,6 +205,8 @@ class CORE_EXPORT QgsCoordinateTransformContext
*/ */
QString calculateCoordinateOperation( const QgsCoordinateReferenceSystem &source, const QgsCoordinateReferenceSystem &destination ) const; 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. * Reads the context's state from a DOM \a element.
* *

View File

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