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