1 /***************************************************************************
2     qgsappcoordinateoperationhandlers.cpp
3     -------------------------
4     begin                : May 2019
5     copyright            : (C) 2019 by Nyall Dawson
6     email                : nyall dot dawson at gmail dot com
7  ***************************************************************************
8  *                                                                         *
9  *   This program is free software; you can redistribute it and/or modify  *
10  *   it under the terms of the GNU General Public License as published by  *
11  *   the Free Software Foundation; either version 2 of the License, or     *
12  *   (at your option) any later version.                                   *
13  *                                                                         *
14  ***************************************************************************/
15 #include "qgsappcoordinateoperationhandlers.h"
16 #include "qgscoordinatetransform.h"
17 #include "qgisapp.h"
18 #include "qgsmessagebar.h"
19 #include "qgsmessagebaritem.h"
20 #include "qgsmessageoutput.h"
21 #include "qgsproject.h"
22 #include "qgsinstallgridshiftdialog.h"
23 
24 //
25 // QgsAppMissingRequiredGridHandler
26 //
QgsAppMissingGridHandler(QObject * parent)27 QgsAppMissingGridHandler::QgsAppMissingGridHandler( QObject *parent )
28   : QObject( parent )
29 {
30   QgsCoordinateTransform::setCustomMissingRequiredGridHandler( [ = ]( const QgsCoordinateReferenceSystem & sourceCrs,
31       const QgsCoordinateReferenceSystem & destinationCrs,
32       const QgsDatumTransform::GridDetails & grid )
33   {
34     emit missingRequiredGrid( sourceCrs, destinationCrs, grid );
35   } );
36 
37   QgsCoordinateTransform::setCustomMissingPreferredGridHandler( [ = ]( const QgsCoordinateReferenceSystem & sourceCrs,
38       const QgsCoordinateReferenceSystem & destinationCrs,
39       const QgsDatumTransform::TransformDetails & preferredOperation,
40       const QgsDatumTransform::TransformDetails & availableOperation )
41   {
42     emit missingPreferredGrid( sourceCrs, destinationCrs, preferredOperation, availableOperation );
43   } );
44 
45   QgsCoordinateTransform::setCustomCoordinateOperationCreationErrorHandler( [ = ]( const QgsCoordinateReferenceSystem & sourceCrs,
46       const QgsCoordinateReferenceSystem & destinationCrs,
47       const QString & error )
48   {
49     emit coordinateOperationCreationError( sourceCrs, destinationCrs, error );
50   } );
51 
52   QgsCoordinateTransform::setCustomMissingGridUsedByContextHandler( [ = ]( const QgsCoordinateReferenceSystem & sourceCrs,
53       const QgsCoordinateReferenceSystem & destinationCrs,
54       const QgsDatumTransform::TransformDetails & desired )
55   {
56     emit missingGridUsedByContextHandler( sourceCrs, destinationCrs, desired );
57   } );
58 
59   QgsCoordinateTransform::setFallbackOperationOccurredHandler( [ = ]( const QgsCoordinateReferenceSystem & sourceCrs,
60       const QgsCoordinateReferenceSystem & destinationCrs,
61       const QString & desired )
62   {
63     emit fallbackOperationOccurred( sourceCrs, destinationCrs, desired );
64   } );
65 
66   connect( this, &QgsAppMissingGridHandler::missingRequiredGrid, this, &QgsAppMissingGridHandler::onMissingRequiredGrid, Qt::QueuedConnection );
67   connect( this, &QgsAppMissingGridHandler::missingPreferredGrid, this, &QgsAppMissingGridHandler::onMissingPreferredGrid, Qt::QueuedConnection );
68   connect( this, &QgsAppMissingGridHandler::coordinateOperationCreationError, this, &QgsAppMissingGridHandler::onCoordinateOperationCreationError, Qt::QueuedConnection );
69   connect( this, &QgsAppMissingGridHandler::missingGridUsedByContextHandler, this, &QgsAppMissingGridHandler::onMissingGridUsedByContextHandler, Qt::QueuedConnection );
70   connect( this, &QgsAppMissingGridHandler::fallbackOperationOccurred, this, &QgsAppMissingGridHandler::onFallbackOperationOccurred, Qt::QueuedConnection );
71 
72   connect( QgsProject::instance(), &QgsProject::cleared, this, [ = ]
73   {
74     mAlreadyWarnedPairsForProject.clear();
75     mAlreadyWarnedBallparkPairsForProject.clear();
76   } );
77 
78 }
79 
onMissingRequiredGrid(const QgsCoordinateReferenceSystem & sourceCrs,const QgsCoordinateReferenceSystem & destinationCrs,const QgsDatumTransform::GridDetails & grid)80 void QgsAppMissingGridHandler::onMissingRequiredGrid( const QgsCoordinateReferenceSystem &sourceCrs, const QgsCoordinateReferenceSystem &destinationCrs, const QgsDatumTransform::GridDetails &grid )
81 {
82   if ( !shouldWarnAboutPair( sourceCrs, destinationCrs ) )
83     return;
84 
85   const QString shortMessage = tr( "No transform available between %1 and %2" ).arg( sourceCrs.userFriendlyIdentifier( QgsCoordinateReferenceSystem::ShortString ),
86                                destinationCrs.userFriendlyIdentifier( QgsCoordinateReferenceSystem::ShortString ) );
87 
88   QString downloadMessage;
89   const QString gridName = grid.shortName;
90   if ( !grid.url.isEmpty() )
91   {
92     if ( !grid.packageName.isEmpty() )
93     {
94       downloadMessage = 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 );
95     }
96     else
97     {
98       downloadMessage = tr( "This grid is available for download from <a href=\"%1\">%1</a>." ).arg( grid.url );
99     }
100   }
101 
102   const QString longMessage = tr( "<p>No transform is available between <i>%1</i> and <i>%2</i>.</p>"
103                                   "<p>This transformation requires the grid file “%3”, which is not available for use on the system.</p>" ).arg( sourceCrs.userFriendlyIdentifier(),
104                                       destinationCrs.userFriendlyIdentifier(),
105                                       grid.shortName );
106 
107   QgsMessageBar *bar = QgisApp::instance()->messageBar();
108   QgsMessageBarItem *widget = bar->createMessage( QString(), shortMessage );
109   QPushButton *detailsButton = new QPushButton( tr( "Details" ) );
110   connect( detailsButton, &QPushButton::clicked, this, [longMessage, downloadMessage, bar, widget, gridName]
111   {
112     QgsInstallGridShiftFileDialog *dlg = new QgsInstallGridShiftFileDialog( gridName, QgisApp::instance() );
113     dlg->setAttribute( Qt::WA_DeleteOnClose );
114     dlg->setWindowTitle( tr( "No Transformations Available" ) );
115     dlg->setDescription( longMessage );
116     dlg->setDownloadMessage( downloadMessage );
117     if ( dlg->exec() )
118     {
119       bar->popWidget( widget );
120     }
121   } );
122 
123   widget->layout()->addWidget( detailsButton );
124   bar->pushWidget( widget, Qgis::Critical, 0 );
125 }
126 
onMissingPreferredGrid(const QgsCoordinateReferenceSystem & sourceCrs,const QgsCoordinateReferenceSystem & destinationCrs,const QgsDatumTransform::TransformDetails & preferredOperation,const QgsDatumTransform::TransformDetails & availableOperation)127 void QgsAppMissingGridHandler::onMissingPreferredGrid( const QgsCoordinateReferenceSystem &sourceCrs, const QgsCoordinateReferenceSystem &destinationCrs, const QgsDatumTransform::TransformDetails &preferredOperation, const QgsDatumTransform::TransformDetails &availableOperation )
128 {
129   if ( !shouldWarnAboutPair( sourceCrs, destinationCrs ) )
130     return;
131 
132   const QString shortMessage = tr( "Cannot use preferred transform between %1 and %2" ).arg( sourceCrs.userFriendlyIdentifier( QgsCoordinateReferenceSystem::ShortString ),
133                                destinationCrs.userFriendlyIdentifier( QgsCoordinateReferenceSystem::ShortString ) );
134 
135   QString gridMessage;
136   QString downloadMessage;
137   QString gridName;
138   for ( const QgsDatumTransform::GridDetails &grid : preferredOperation.grids )
139   {
140     if ( !grid.isAvailable )
141     {
142       QString m = tr( "This transformation requires the grid file “%1”, which is not available for use on the system." ).arg( grid.shortName );
143       gridName = grid.shortName;
144       if ( !grid.url.isEmpty() )
145       {
146         if ( !grid.packageName.isEmpty() )
147         {
148           downloadMessage = 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 );
149         }
150         else
151         {
152           downloadMessage = tr( "This grid is available for download from <a href=\"%1\">%1</a>." ).arg( grid.url );
153         }
154       }
155       gridMessage += QStringLiteral( "<li>%1</li>" ).arg( m );
156     }
157   }
158   if ( !gridMessage.isEmpty() )
159   {
160     gridMessage = "<ul>" + gridMessage + "</ul>";
161   }
162 
163   QString accuracyMessage;
164   if ( availableOperation.accuracy >= 0 && preferredOperation.accuracy >= 0 )
165     accuracyMessage = tr( "<p>Current transform “<i>%1</i>” has an accuracy of %2 meters, while the preferred transformation “<i>%3</i>” has accuracy %4 meters.</p>" ).arg( availableOperation.name )
166                       .arg( availableOperation.accuracy ).arg( preferredOperation.name ).arg( preferredOperation.accuracy );
167   else if ( preferredOperation.accuracy >= 0 )
168     accuracyMessage = tr( "<p>Current transform “<i>%1</i>” has an unknown accuracy, while the preferred transformation “<i>%2</i>” has accuracy %3 meters.</p>" ).arg( availableOperation.name )
169                       .arg( preferredOperation.name ).arg( preferredOperation.accuracy );
170 
171   const QString longMessage = tr( "<p>The preferred transform between <i>%1</i> and <i>%2</i> is not available for use on the system.</p>" ).arg( sourceCrs.userFriendlyIdentifier(),
172                               destinationCrs.userFriendlyIdentifier() )
173                               + gridMessage + accuracyMessage;
174 
175   QgsMessageBar *bar = QgisApp::instance()->messageBar();
176   QgsMessageBarItem *widget = bar->createMessage( QString(), shortMessage );
177   QPushButton *detailsButton = new QPushButton( tr( "Details" ) );
178   connect( detailsButton, &QPushButton::clicked, this, [longMessage, downloadMessage, gridName, widget, bar]
179   {
180     QgsInstallGridShiftFileDialog *dlg = new QgsInstallGridShiftFileDialog( gridName, QgisApp::instance() );
181     dlg->setAttribute( Qt::WA_DeleteOnClose );
182     dlg->setWindowTitle( tr( "Preferred Transformation Not Available" ) );
183     dlg->setDescription( longMessage );
184     dlg->setDownloadMessage( downloadMessage );
185     if ( dlg->exec() )
186     {
187       bar->popWidget( widget );
188     }
189   } );
190 
191   widget->layout()->addWidget( detailsButton );
192   bar->pushWidget( widget, Qgis::Warning, 0 );
193 }
194 
onCoordinateOperationCreationError(const QgsCoordinateReferenceSystem & sourceCrs,const QgsCoordinateReferenceSystem & destinationCrs,const QString & error)195 void QgsAppMissingGridHandler::onCoordinateOperationCreationError( const QgsCoordinateReferenceSystem &sourceCrs, const QgsCoordinateReferenceSystem &destinationCrs, const QString &error )
196 {
197   if ( !shouldWarnAboutPairForCurrentProject( sourceCrs, destinationCrs ) )
198     return;
199 
200   const QString shortMessage = tr( "No transform available between %1 and %2" ).arg( sourceCrs.userFriendlyIdentifier( QgsCoordinateReferenceSystem::ShortString ), destinationCrs.userFriendlyIdentifier( QgsCoordinateReferenceSystem::ShortString ) );
201   const QString longMessage = tr( "<p>No transform is available between <i>%1</i> and <i>%2</i>.</p><p style=\"color: red\">%3</p>" ).arg( sourceCrs.userFriendlyIdentifier(), destinationCrs.userFriendlyIdentifier(), error );
202 
203   QgsMessageBar *bar = QgisApp::instance()->messageBar();
204   QgsMessageBarItem *widget = bar->createMessage( QString(), shortMessage );
205   QPushButton *detailsButton = new QPushButton( tr( "Details" ) );
206   connect( detailsButton, &QPushButton::clicked, this, [longMessage]
207   {
208     // dlg has deleted on close
209     QgsMessageOutput * dlg( QgsMessageOutput::createMessageOutput() );
210     dlg->setTitle( tr( "No Transformations Available" ) );
211     dlg->setMessage( longMessage, QgsMessageOutput::MessageHtml );
212     dlg->showMessage();
213   } );
214 
215   widget->layout()->addWidget( detailsButton );
216   bar->pushWidget( widget, Qgis::Critical, 0 );
217 }
218 
onMissingGridUsedByContextHandler(const QgsCoordinateReferenceSystem & sourceCrs,const QgsCoordinateReferenceSystem & destinationCrs,const QgsDatumTransform::TransformDetails & desired)219 void QgsAppMissingGridHandler::onMissingGridUsedByContextHandler( const QgsCoordinateReferenceSystem &sourceCrs, const QgsCoordinateReferenceSystem &destinationCrs, const QgsDatumTransform::TransformDetails &desired )
220 {
221   if ( !shouldWarnAboutPairForCurrentProject( sourceCrs, destinationCrs ) )
222     return;
223 
224   const QString shortMessage = tr( "Cannot use project transform between %1 and %2" ).arg( sourceCrs.userFriendlyIdentifier( QgsCoordinateReferenceSystem::ShortString ),
225                                destinationCrs.userFriendlyIdentifier( QgsCoordinateReferenceSystem::ShortString ) );
226 
227   QString gridMessage;
228   QString downloadMessage;
229   QString gridName;
230   for ( const QgsDatumTransform::GridDetails &grid : desired.grids )
231   {
232     if ( !grid.isAvailable )
233     {
234       gridName = grid.shortName;
235       QString m = tr( "This transformation requires the grid file “%1”, which is not available for use on the system." ).arg( grid.shortName );
236       if ( !grid.url.isEmpty() )
237       {
238         if ( !grid.packageName.isEmpty() )
239         {
240           downloadMessage = 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 );
241         }
242         else
243         {
244           downloadMessage = tr( "This grid is available for download from <a href=\"%1\">%1</a>." ).arg( grid.url );
245         }
246       }
247       gridMessage += QStringLiteral( "<li>%1</li>" ).arg( m );
248     }
249   }
250   if ( !gridMessage.isEmpty() )
251   {
252     gridMessage = "<ul>" + gridMessage + "</ul>";
253   }
254 
255   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( sourceCrs.userFriendlyIdentifier(),
256                               destinationCrs.userFriendlyIdentifier() )
257                               + gridMessage
258                               + tr( "<p>The operation specified for use in the project is:</p><p><code>%1</code></p>" ).arg( desired.proj ) ;
259 
260   QgsMessageBar *bar = QgisApp::instance()->messageBar();
261   QgsMessageBarItem *widget = bar->createMessage( QString(), shortMessage );
262   QPushButton *detailsButton = new QPushButton( tr( "Details" ) );
263   connect( detailsButton, &QPushButton::clicked, this, [longMessage, gridName, downloadMessage, bar, widget]
264   {
265     QgsInstallGridShiftFileDialog *dlg = new QgsInstallGridShiftFileDialog( gridName, QgisApp::instance() );
266     dlg->setAttribute( Qt::WA_DeleteOnClose );
267     dlg->setWindowTitle( tr( "Project Transformation Not Available" ) );
268     dlg->setDescription( longMessage );
269     dlg->setDownloadMessage( downloadMessage );
270     if ( dlg->exec() )
271     {
272       bar->popWidget( widget );
273     }
274   } );
275 
276   widget->layout()->addWidget( detailsButton );
277   bar->pushWidget( widget, Qgis::Critical, 0 );
278 }
279 
onFallbackOperationOccurred(const QgsCoordinateReferenceSystem & sourceCrs,const QgsCoordinateReferenceSystem & destinationCrs,const QString & desired)280 void QgsAppMissingGridHandler::onFallbackOperationOccurred( const QgsCoordinateReferenceSystem &sourceCrs, const QgsCoordinateReferenceSystem &destinationCrs, const QString &desired )
281 {
282   if ( !shouldWarnAboutBallparkPairForCurrentProject( sourceCrs, destinationCrs ) )
283     return;
284 
285   const QString shortMessage = tr( "Used a ballpark transform from %1 to %2" ).arg( sourceCrs.userFriendlyIdentifier( QgsCoordinateReferenceSystem::ShortString ), destinationCrs.userFriendlyIdentifier( QgsCoordinateReferenceSystem::ShortString ) );
286   const QString longMessage = tr( "<p>An alternative, ballpark-only transform was used when transforming coordinates between <i>%1</i> and <i>%2</i>. The results may not match those obtained by using the preferred operation:</p><code>%3</code><p style=\"font-weight: bold\">Possibly an incorrect choice of operation was made for transformations between these reference systems. Check the Project Properties and ensure that the selected transform operations are applicable over the whole extent of the current project." ).arg( sourceCrs.userFriendlyIdentifier(), destinationCrs.userFriendlyIdentifier(), desired );
287 
288   QgsMessageBar *bar = QgisApp::instance()->messageBar();
289   QgsMessageBarItem *widget = bar->createMessage( QString(), shortMessage );
290   QPushButton *detailsButton = new QPushButton( tr( "Details" ) );
291   connect( detailsButton, &QPushButton::clicked, this, [longMessage]
292   {
293     // dlg has deleted on close
294     QgsMessageOutput * dlg( QgsMessageOutput::createMessageOutput() );
295     dlg->setTitle( tr( "Ballpark Transform Occurred" ) );
296     dlg->setMessage( longMessage, QgsMessageOutput::MessageHtml );
297     dlg->showMessage();
298   } );
299 
300   widget->layout()->addWidget( detailsButton );
301   bar->pushWidget( widget, Qgis::Warning, 0 );
302 }
303 
shouldWarnAboutPair(const QgsCoordinateReferenceSystem & source,const QgsCoordinateReferenceSystem & dest)304 bool QgsAppMissingGridHandler::shouldWarnAboutPair( const QgsCoordinateReferenceSystem &source, const QgsCoordinateReferenceSystem &dest )
305 {
306   if ( mAlreadyWarnedPairs.contains( qMakePair( source, dest ) ) || mAlreadyWarnedPairs.contains( qMakePair( dest, source ) ) )
307   {
308     return false;
309   }
310 
311   mAlreadyWarnedPairs.append( qMakePair( source, dest ) );
312   return true;
313 }
314 
shouldWarnAboutPairForCurrentProject(const QgsCoordinateReferenceSystem & source,const QgsCoordinateReferenceSystem & dest)315 bool QgsAppMissingGridHandler::shouldWarnAboutPairForCurrentProject( const QgsCoordinateReferenceSystem &source, const QgsCoordinateReferenceSystem &dest )
316 {
317   if ( mAlreadyWarnedPairsForProject.contains( qMakePair( source, dest ) ) || mAlreadyWarnedPairsForProject.contains( qMakePair( dest, source ) ) )
318   {
319     return false;
320   }
321 
322   mAlreadyWarnedPairsForProject.append( qMakePair( source, dest ) );
323   return true;
324 }
325 
shouldWarnAboutBallparkPairForCurrentProject(const QgsCoordinateReferenceSystem & source,const QgsCoordinateReferenceSystem & dest)326 bool QgsAppMissingGridHandler::shouldWarnAboutBallparkPairForCurrentProject( const QgsCoordinateReferenceSystem &source, const QgsCoordinateReferenceSystem &dest )
327 {
328   if ( mAlreadyWarnedBallparkPairsForProject.contains( qMakePair( source, dest ) ) || mAlreadyWarnedBallparkPairsForProject.contains( qMakePair( dest, source ) ) )
329   {
330     return false;
331   }
332 
333   mAlreadyWarnedBallparkPairsForProject.append( qMakePair( source, dest ) );
334   return true;
335 }
336