diff --git a/python/core/auto_generated/qgscoordinatetransformcontext.sip.in b/python/core/auto_generated/qgscoordinatetransformcontext.sip.in index 0984e16476f..918602ec898 100644 --- a/python/core/auto_generated/qgscoordinatetransformcontext.sip.in +++ b/python/core/auto_generated/qgscoordinatetransformcontext.sip.in @@ -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``. diff --git a/src/app/qgsappcoordinateoperationhandlers.cpp b/src/app/qgsappcoordinateoperationhandlers.cpp index 773024e3d1f..8efb24621f9 100644 --- a/src/app/qgsappcoordinateoperationhandlers.cpp +++ b/src/app/qgsappcoordinateoperationhandlers.cpp @@ -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 %1 package, available for download from %2." ).arg( grid.packageName, grid.url ); + } + else + { + m += ' ' + tr( "This grid is available for download from %1." ).arg( grid.url ); + } + } + gridMessage += QStringLiteral( "
  • %1
  • " ).arg( m ); + } + } + if ( !gridMessage.isEmpty() ) + { + gridMessage = ""; + } + + const QString longMessage = tr( "

    This project specifies a preset transform between %1 and %2, which is not available for use on the system.

    " ).arg( displayIdentifierForCrs( sourceCrs ), + displayIdentifierForCrs( destinationCrs ) ) + + gridMessage + + tr( "

    The operation specified for use in the project is:

    %1

    " ).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; +} diff --git a/src/app/qgsappcoordinateoperationhandlers.h b/src/app/qgsappcoordinateoperationhandlers.h index e33184615e2..083e7e35344 100644 --- a/src/app/qgsappcoordinateoperationhandlers.h +++ b/src/app/qgsappcoordinateoperationhandlers.h @@ -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; }; diff --git a/src/core/qgscoordinatetransform.cpp b/src/core/qgscoordinatetransform.cpp index 3886fe47376..65e701caa0a 100644 --- a/src/core/qgscoordinatetransform.cpp +++ b/src/core/qgscoordinatetransform.cpp @@ -969,3 +969,8 @@ void QgsCoordinateTransform::setCustomCoordinateOperationCreationErrorHandler( c { QgsCoordinateTransformPrivate::setCustomCoordinateOperationCreationErrorHandler( handler ); } + +void QgsCoordinateTransform::setCustomMissingGridUsedByContextHandler( const std::function &handler ) +{ + QgsCoordinateTransformPrivate::setCustomMissingGridUsedByContextHandler( handler ); +} diff --git a/src/core/qgscoordinatetransform.h b/src/core/qgscoordinatetransform.h index 5768a7f39ae..2009aeec4a4 100644 --- a/src/core/qgscoordinatetransform.h +++ b/src/core/qgscoordinatetransform.h @@ -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: diff --git a/src/core/qgscoordinatetransform_p.cpp b/src/core/qgscoordinatetransform_p.cpp index b30eda061b7..23e54edf6e4 100644 --- a/src/core/qgscoordinatetransform_p.cpp +++ b/src/core/qgscoordinatetransform_p.cpp @@ -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 &handler ) +{ + sMissingGridUsedByContextHandler = handler; +} + #if PROJ_VERSION_MAJOR<6 QString QgsCoordinateTransformPrivate::stripDatumTransform( const QString &proj4 ) const { diff --git a/src/core/qgscoordinatetransform_p.h b/src/core/qgscoordinatetransform_p.h index 38a4c4e9f9a..4f79ab53a76 100644 --- a/src/core/qgscoordinatetransform_p.h +++ b/src/core/qgscoordinatetransform_p.h @@ -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 diff --git a/src/core/qgscoordinatetransformcontext.cpp b/src/core/qgscoordinatetransformcontext.cpp index ba35e2a8f85..e6796b0b495 100644 --- a/src/core/qgscoordinatetransformcontext.cpp +++ b/src/core/qgscoordinatetransformcontext.cpp @@ -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; } diff --git a/src/core/qgscoordinatetransformcontext.h b/src/core/qgscoordinatetransformcontext.h index 5644e1a47dc..b7d644298e7 100644 --- a/src/core/qgscoordinatetransformcontext.h +++ b/src/core/qgscoordinatetransformcontext.h @@ -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. * diff --git a/src/core/qgsproject.cpp b/src/core/qgsproject.cpp index ea5382c1f64..94a40484fd8 100644 --- a/src/core/qgsproject.cpp +++ b/src/core/qgsproject.cpp @@ -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 ); }