1 /***************************************************************************
2                          qgscoordinateoperationwidget.cpp
3                          ---------------------------
4     begin                : December 2019
5     copyright            : (C) 2019 by Nyall Dawson
6     email                : nyall dot dawson at gmail dot com
7  ***************************************************************************/
8 
9 /***************************************************************************
10  *                                                                         *
11  *   This program is free software; you can redistribute it and/or modify  *
12  *   it under the terms of the GNU General Public License as published by  *
13  *   the Free Software Foundation; either version 2 of the License, or     *
14  *   (at your option) any later version.                                   *
15  *                                                                         *
16  ***************************************************************************/
17 
18 #include "qgscoordinateoperationwidget.h"
19 #include "qgscoordinatetransform.h"
20 #include "qgsprojectionselectiondialog.h"
21 #include "qgslogger.h"
22 #include "qgssettings.h"
23 #include "qgsproject.h"
24 #include "qgsguiutils.h"
25 #include "qgsgui.h"
26 #include "qgshelp.h"
27 #include "qgsinstallgridshiftdialog.h"
28 
29 #include <QDir>
30 #include <QPushButton>
31 
32 #if PROJ_VERSION_MAJOR>=6
33 #include "qgsprojutils.h"
34 #include <proj.h>
35 #endif
36 
QgsCoordinateOperationWidget(QWidget * parent)37 QgsCoordinateOperationWidget::QgsCoordinateOperationWidget( QWidget *parent )
38   : QWidget( parent )
39 {
40   setupUi( this );
41 
42   mLabelSrcDescription->setTextInteractionFlags( Qt::TextBrowserInteraction );
43   mLabelSrcDescription->setOpenExternalLinks( true );
44   mInstallGridButton->hide();
45 
46 #if PROJ_VERSION_MAJOR>=6
47   connect( mInstallGridButton, &QPushButton::clicked, this, &QgsCoordinateOperationWidget::installGrid );
48   connect( mAllowFallbackCheckBox, &QCheckBox::toggled, this, [ = ]
49   {
50     if ( !mBlockSignals )
51       emit operationChanged();
52   } );
53   mCoordinateOperationTableWidget->setColumnCount( 3 );
54 #else
55   mCoordinateOperationTableWidget->setColumnCount( 2 );
56 #endif
57 
58   QStringList headers;
59 #if PROJ_VERSION_MAJOR>=6
60   headers << tr( "Transformation" ) << tr( "Accuracy (meters)" ) << tr( "Area of Use" );
61 #else
62   headers << tr( "Source Transform" ) << tr( "Destination Transform" ) ;
63 #endif
64   mCoordinateOperationTableWidget->setHorizontalHeaderLabels( headers );
65 
66 #if PROJ_VERSION_MAJOR<6
67   mAreaCanvas->hide();
68 #endif
69 
70 #if PROJ_VERSION_MAJOR>=6
71   // proj 6 doesn't provide deprecated operations
72   mHideDeprecatedCheckBox->setVisible( false );
73   mShowSupersededCheckBox->setVisible( true );
74   mLabelDstDescription->hide();
75 #else
76   mShowSupersededCheckBox->setVisible( false );
77   mAllowFallbackCheckBox->setVisible( false );
78   QgsSettings settings;
79   mHideDeprecatedCheckBox->setChecked( settings.value( QStringLiteral( "Windows/DatumTransformDialog/hideDeprecated" ), true ).toBool() );
80 #endif
81 
82   connect( mHideDeprecatedCheckBox, &QCheckBox::stateChanged, this, [ = ] { loadAvailableOperations(); } );
83   connect( mShowSupersededCheckBox, &QCheckBox::toggled, this, &QgsCoordinateOperationWidget::showSupersededToggled );
84   connect( mCoordinateOperationTableWidget, &QTableWidget::currentItemChanged, this, &QgsCoordinateOperationWidget::tableCurrentItemChanged );
85   connect( mCoordinateOperationTableWidget, &QTableWidget::itemDoubleClicked, this, &QgsCoordinateOperationWidget::operationDoubleClicked );
86 
87   mLabelSrcDescription->clear();
88   mLabelDstDescription->clear();
89 }
90 
setMapCanvas(QgsMapCanvas * canvas)91 void QgsCoordinateOperationWidget::setMapCanvas( QgsMapCanvas *canvas )
92 {
93 #if PROJ_VERSION_MAJOR<6
94   ( void )canvas;
95 #else
96   if ( canvas )
97   {
98     // show canvas extent in preview widget
99     QPolygonF mainCanvasPoly = canvas->mapSettings().visiblePolygon();
100     QgsGeometry g = QgsGeometry::fromQPolygonF( mainCanvasPoly );
101     // close polygon
102     mainCanvasPoly << mainCanvasPoly.at( 0 );
103     if ( QgsProject::instance()->crs() !=
104          QgsCoordinateReferenceSystem::fromEpsgId( 4326 ) )
105     {
106       // reproject extent
107       QgsCoordinateTransform ct( QgsProject::instance()->crs(),
108                                  QgsCoordinateReferenceSystem::fromEpsgId( 4326 ), QgsProject::instance() );
109       ct.setBallparkTransformsAreAppropriate( true );
110       g = g.densifyByCount( 5 );
111       try
112       {
113         g.transform( ct );
114       }
115       catch ( QgsCsException & )
116       {
117       }
118     }
119     mAreaCanvas->setCanvasRect( g.boundingBox() );
120   }
121 #endif
122 }
123 
setShowMakeDefault(bool show)124 void QgsCoordinateOperationWidget::setShowMakeDefault( bool show )
125 {
126   mMakeDefaultCheckBox->setVisible( show );
127 }
128 
makeDefaultSelected() const129 bool QgsCoordinateOperationWidget::makeDefaultSelected() const
130 {
131   return mMakeDefaultCheckBox->isChecked();
132 }
133 
hasSelection() const134 bool QgsCoordinateOperationWidget::hasSelection() const
135 {
136   return !mCoordinateOperationTableWidget->selectedItems().isEmpty();
137 }
138 
availableOperations() const139 QList<QgsCoordinateOperationWidget::OperationDetails> QgsCoordinateOperationWidget::availableOperations() const
140 {
141   QList<QgsCoordinateOperationWidget::OperationDetails> res;
142   res.reserve( mDatumTransforms.size() );
143 #if PROJ_VERSION_MAJOR>=6
144   for ( const QgsDatumTransform::TransformDetails &details : mDatumTransforms )
145   {
146     OperationDetails op;
147     op.proj = details.proj;
148     op.sourceTransformId = -1;
149     op.destinationTransformId = -1;
150     op.isAvailable = details.isAvailable;
151     res << op;
152   }
153 #else
154   for ( const QgsDatumTransform::TransformPair &details : mDatumTransforms )
155   {
156     OperationDetails op;
157     op.sourceTransformId = details.sourceTransformId;
158     op.destinationTransformId = details.destinationTransformId;
159     res << op;
160   }
161 #endif
162   return res;
163 }
164 
loadAvailableOperations()165 void QgsCoordinateOperationWidget::loadAvailableOperations()
166 {
167   mCoordinateOperationTableWidget->setRowCount( 0 );
168 
169   int row = 0;
170   int preferredInitialRow = -1;
171 #if PROJ_VERSION_MAJOR>=6
172   for ( const QgsDatumTransform::TransformDetails &transform : qgis::as_const( mDatumTransforms ) )
173   {
174     std::unique_ptr< QTableWidgetItem > item = qgis::make_unique< QTableWidgetItem >();
175     item->setData( ProjRole, transform.proj );
176     item->setData( AvailableRole, transform.isAvailable );
177     item->setFlags( item->flags() & ~Qt::ItemIsEditable );
178 
179     QString name = transform.name;
180     if ( !transform.authority.isEmpty() && !transform.code.isEmpty() )
181       name += QStringLiteral( " %1 %2:%3" ).arg( QString( QChar( 0x2013 ) ), transform.authority, transform.code );
182     item->setText( name );
183 
184     if ( row == 0 ) // highlight first (preferred) operation
185     {
186       QFont f = item->font();
187       f.setBold( true );
188       item->setFont( f );
189       item->setForeground( QBrush( QColor( 0, 120, 0 ) ) );
190     }
191 
192     if ( !transform.isAvailable )
193     {
194       item->setForeground( QBrush( palette().color( QPalette::Disabled, QPalette::Text ) ) );
195     }
196 
197     if ( preferredInitialRow < 0 && transform.isAvailable )
198     {
199       // try to select a "preferred" entry by default
200       preferredInitialRow = row;
201     }
202 
203     QString missingMessage;
204     if ( !transform.isAvailable )
205     {
206       QStringList gridMessages;
207       QStringList missingGrids;
208       QStringList missingGridPackages;
209       QStringList missingGridUrls;
210 
211       for ( const QgsDatumTransform::GridDetails &grid : transform.grids )
212       {
213         if ( !grid.isAvailable )
214         {
215           missingGrids << grid.shortName;
216           missingGridPackages << grid.packageName;
217           missingGridUrls << grid.url;
218           QString m = tr( "This transformation requires the grid file “%1”, which is not available for use on the system." ).arg( grid.shortName );
219           if ( !grid.url.isEmpty() )
220           {
221             if ( !grid.packageName.isEmpty() )
222             {
223               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 );
224             }
225             else
226             {
227               m += ' ' + tr( "This grid is available for download from <a href=\"%1\">%1</a>." ).arg( grid.url );
228             }
229           }
230           gridMessages << m;
231         }
232       }
233 
234       item->setData( MissingGridsRole, missingGrids );
235       item->setData( MissingGridPackageNamesRole, missingGridPackages );
236       item->setData( MissingGridUrlsRole, missingGridUrls );
237 
238       if ( gridMessages.count() > 1 )
239       {
240         for ( int k = 0; k < gridMessages.count(); ++k )
241           gridMessages[k] = QStringLiteral( "<li>%1</li>" ).arg( gridMessages.at( k ) );
242 
243         missingMessage = QStringLiteral( "<ul>%1</ul" ).arg( gridMessages.join( QString() ) );
244       }
245       else if ( !gridMessages.empty() )
246       {
247         missingMessage = gridMessages.constFirst();
248       }
249     }
250 
251     QStringList areasOfUse;
252     QStringList authorityCodes;
253 
254     QStringList opText;
255     QString lastSingleOpScope;
256     QString lastSingleOpRemarks;
257     for ( const QgsDatumTransform::SingleOperationDetails &singleOpDetails : transform.operationDetails )
258     {
259       QString text;
260       if ( !singleOpDetails.scope.isEmpty() )
261       {
262         text += QStringLiteral( "<b>%1</b>: %2" ).arg( tr( "Scope" ), formatScope( singleOpDetails.scope ) );
263         lastSingleOpScope = singleOpDetails.scope;
264       }
265       if ( !singleOpDetails.remarks.isEmpty() )
266       {
267         if ( !text.isEmpty() )
268           text += QLatin1String( "<br>" );
269         text += QStringLiteral( "<b>%1</b>: %2" ).arg( tr( "Remarks" ), singleOpDetails.remarks );
270         lastSingleOpRemarks = singleOpDetails.remarks;
271       }
272       if ( !singleOpDetails.areaOfUse.isEmpty() )
273       {
274         if ( !areasOfUse.contains( singleOpDetails.areaOfUse ) )
275           areasOfUse << singleOpDetails.areaOfUse;
276       }
277       if ( !singleOpDetails.authority.isEmpty() && !singleOpDetails.code.isEmpty() )
278       {
279         const QString identifier = QStringLiteral( "%1:%2" ).arg( singleOpDetails.authority, singleOpDetails.code );
280         if ( !authorityCodes.contains( identifier ) )
281           authorityCodes << identifier;
282       }
283 
284       if ( !text.isEmpty() )
285       {
286         opText.append( text );
287       }
288     }
289 
290     QString text;
291     if ( !transform.scope.isEmpty() && transform.scope != lastSingleOpScope )
292     {
293       text += QStringLiteral( "<b>%1</b>: %2" ).arg( tr( "Scope" ), transform.scope );
294     }
295     if ( !transform.remarks.isEmpty() && transform.remarks != lastSingleOpRemarks )
296     {
297       if ( !text.isEmpty() )
298         text += QLatin1String( "<br>" );
299       text += QStringLiteral( "<b>%1</b>: %2" ).arg( tr( "Remarks" ), transform.remarks );
300     }
301     if ( !text.isEmpty() )
302     {
303       opText.append( text );
304     }
305 
306     if ( opText.count() > 1 )
307     {
308       for ( int k = 0; k < opText.count(); ++k )
309         opText[k] = QStringLiteral( "<li>%1</li>" ).arg( opText.at( k ) );
310     }
311 
312     if ( !transform.areaOfUse.isEmpty() && !areasOfUse.contains( transform.areaOfUse ) )
313       areasOfUse << transform.areaOfUse;
314     item->setData( BoundsRole, transform.bounds );
315 
316     const QString id = !transform.authority.isEmpty() && !transform.code.isEmpty() ? QStringLiteral( "%1:%2" ).arg( transform.authority, transform.code ) : QString();
317     if ( !id.isEmpty() && !authorityCodes.contains( id ) )
318       authorityCodes << id;
319 
320     const QColor disabled = palette().color( QPalette::Disabled, QPalette::Text );
321     const QColor active = palette().color( QPalette::Active, QPalette::Text );
322 
323     const QColor codeColor( static_cast< int >( active.red() * 0.6 + disabled.red() * 0.4 ),
324                             static_cast< int >( active.green() * 0.6 + disabled.green() * 0.4 ),
325                             static_cast< int >( active.blue() * 0.6 + disabled.blue() * 0.4 ) );
326     const QString toolTipString = QStringLiteral( "<b>%1</b>" ).arg( transform.name )
327                                   + ( !opText.empty() ? ( opText.count() == 1 ? QStringLiteral( "<p>%1</p>" ).arg( opText.at( 0 ) ) : QStringLiteral( "<ul>%1</ul>" ).arg( opText.join( QString() ) ) ) : QString() )
328                                   + ( !areasOfUse.empty() ? QStringLiteral( "<p><b>%1</b>: %2</p>" ).arg( tr( "Area of use" ), areasOfUse.join( QLatin1String( ", " ) ) ) : QString() )
329                                   + ( !authorityCodes.empty() ? QStringLiteral( "<p><b>%1</b>: %2</p>" ).arg( tr( "Identifiers" ), authorityCodes.join( QLatin1String( ", " ) ) ) : QString() )
330                                   + ( !missingMessage.isEmpty() ? QStringLiteral( "<p><b style=\"color: red\">%1</b></p>" ).arg( missingMessage ) : QString() )
331                                   + QStringLiteral( "<p><code style=\"color: %1\">%2</code></p>" ).arg( codeColor.name(), transform.proj );
332 
333     item->setToolTip( toolTipString );
334     mCoordinateOperationTableWidget->setRowCount( row + 1 );
335     mCoordinateOperationTableWidget->setItem( row, 0, item.release() );
336 
337     item = qgis::make_unique< QTableWidgetItem >();
338     item->setFlags( item->flags() & ~Qt::ItemIsEditable );
339     item->setText( transform.accuracy >= 0 ? QString::number( transform.accuracy ) : tr( "Unknown" ) );
340     item->setToolTip( toolTipString );
341     if ( !transform.isAvailable )
342     {
343       item->setForeground( QBrush( palette().color( QPalette::Disabled, QPalette::Text ) ) );
344     }
345     mCoordinateOperationTableWidget->setItem( row, 1, item.release() );
346 
347 #if PROJ_VERSION_MAJOR>=6
348     // area of use column
349     item = qgis::make_unique< QTableWidgetItem >();
350     item->setFlags( item->flags() & ~Qt::ItemIsEditable );
351     item->setText( areasOfUse.join( QLatin1String( ", " ) ) );
352     item->setToolTip( toolTipString );
353     if ( !transform.isAvailable )
354     {
355       item->setForeground( QBrush( palette().color( QPalette::Disabled, QPalette::Text ) ) );
356     }
357     mCoordinateOperationTableWidget->setItem( row, 2, item.release() );
358 #endif
359 
360     row++;
361   }
362 #else
363   Q_NOWARN_DEPRECATED_PUSH
364   for ( const QgsDatumTransform::TransformPair &transform : qgis::as_const( mDatumTransforms ) )
365   {
366     bool itemDisabled = false;
367     bool itemHidden = false;
368 
369     if ( transform.sourceTransformId == -1 && transform.destinationTransformId == -1 )
370       continue;
371 
372     QgsDatumTransform::TransformInfo srcInfo = QgsDatumTransform::datumTransformInfo( transform.sourceTransformId );
373     QgsDatumTransform::TransformInfo destInfo = QgsDatumTransform::datumTransformInfo( transform.destinationTransformId );
374     for ( int i = 0; i < 2; ++i )
375     {
376       std::unique_ptr< QTableWidgetItem > item = qgis::make_unique< QTableWidgetItem >();
377       int nr = i == 0 ? transform.sourceTransformId : transform.destinationTransformId;
378       item->setData( TransformIdRole, nr );
379       item->setFlags( item->flags() & ~Qt::ItemIsEditable );
380 
381       item->setText( QgsDatumTransform::datumTransformToProj( nr ) );
382 
383       //Describe datums in a tooltip
384       QgsDatumTransform::TransformInfo info = i == 0 ? srcInfo : destInfo;
385       if ( info.datumTransformId == -1 )
386         continue;
387 
388       if ( info.deprecated )
389       {
390         itemHidden = mHideDeprecatedCheckBox->isChecked();
391         item->setForeground( QBrush( QColor( 255, 0, 0 ) ) );
392       }
393 
394       if ( ( srcInfo.preferred && !srcInfo.deprecated ) || ( destInfo.preferred && !destInfo.deprecated ) )
395       {
396         QFont f = item->font();
397         f.setBold( true );
398         item->setFont( f );
399         item->setForeground( QBrush( QColor( 0, 120, 0 ) ) );
400       }
401 
402       if ( info.preferred && !info.deprecated && preferredInitialRow < 0 )
403       {
404         // try to select a "preferred" entry by default
405         preferredInitialRow = row;
406       }
407 
408       QString toolTipString;
409       if ( gridShiftTransformation( item->text() ) )
410       {
411         toolTipString.append( QStringLiteral( "<p><b>NTv2</b></p>" ) );
412       }
413 
414       if ( info.epsgCode > 0 )
415         toolTipString.append( QStringLiteral( "<p><b>EPSG Transformations Code:</b> %1</p>" ).arg( info.epsgCode ) );
416 
417       toolTipString.append( QStringLiteral( "<p><b>Source CRS:</b> %1</p><p><b>Destination CRS:</b> %2</p>" ).arg( info.sourceCrsDescription, info.destinationCrsDescription ) );
418 
419       if ( !info.remarks.isEmpty() )
420         toolTipString.append( QStringLiteral( "<p><b>Remarks:</b> %1</p>" ).arg( info.remarks ) );
421       if ( !info.scope.isEmpty() )
422         toolTipString.append( QStringLiteral( "<p><b>Scope:</b> %1</p>" ).arg( info.scope ) );
423       if ( info.preferred )
424         toolTipString.append( "<p><b>Preferred transformation</b></p>" );
425       if ( info.deprecated )
426         toolTipString.append( "<p><b>Deprecated transformation</b></p>" );
427 
428       item->setToolTip( toolTipString );
429 
430       if ( gridShiftTransformation( item->text() ) && !testGridShiftFileAvailability( item.get() ) )
431       {
432         itemDisabled = true;
433       }
434 
435       if ( !itemHidden )
436       {
437         if ( itemDisabled )
438         {
439           item->setFlags( Qt::NoItemFlags );
440         }
441         mCoordinateOperationTableWidget->setRowCount( row + 1 );
442         mCoordinateOperationTableWidget->setItem( row, i, item.release() );
443       }
444     }
445     row++;
446   }
447   Q_NOWARN_DEPRECATED_POP
448 #endif
449 
450   if ( mCoordinateOperationTableWidget->currentRow() < 0 )
451     mCoordinateOperationTableWidget->selectRow( preferredInitialRow >= 0 ? preferredInitialRow : 0 );
452 
453   mCoordinateOperationTableWidget->resizeColumnsToContents();
454 
455   tableCurrentItemChanged( nullptr, nullptr );
456 }
457 
~QgsCoordinateOperationWidget()458 QgsCoordinateOperationWidget::~QgsCoordinateOperationWidget()
459 {
460   QgsSettings settings;
461   settings.setValue( QStringLiteral( "Windows/DatumTransformDialog/hideDeprecated" ), mHideDeprecatedCheckBox->isChecked() );
462 
463   for ( int i = 0; i < 2; i++ )
464   {
465     settings.setValue( QStringLiteral( "Windows/DatumTransformDialog/columnWidths/%1" ).arg( i ), mCoordinateOperationTableWidget->columnWidth( i ) );
466   }
467 }
468 
defaultOperation() const469 QgsCoordinateOperationWidget::OperationDetails QgsCoordinateOperationWidget::defaultOperation() const
470 {
471   OperationDetails preferred;
472 
473 #if PROJ_VERSION_MAJOR>=6
474   // for proj 6, return the first available transform -- they are sorted by preference by proj already
475   for ( const QgsDatumTransform::TransformDetails &transform : qgis::as_const( mDatumTransforms ) )
476   {
477     if ( transform.isAvailable )
478     {
479       preferred.proj = transform.proj;
480       preferred.isAvailable = transform.isAvailable;
481       break;
482     }
483   }
484   return preferred;
485 #else
486   OperationDetails preferredNonDeprecated;
487   bool foundPreferredNonDeprecated = false;
488   bool foundPreferred  = false;
489   OperationDetails nonDeprecated;
490   bool foundNonDeprecated = false;
491   OperationDetails fallback;
492   bool foundFallback = false;
493 
494   Q_NOWARN_DEPRECATED_PUSH
495   for ( const QgsDatumTransform::TransformPair &transform : qgis::as_const( mDatumTransforms ) )
496   {
497     if ( transform.sourceTransformId == -1 && transform.destinationTransformId == -1 )
498       continue;
499 
500     const QgsDatumTransform::TransformInfo srcInfo = QgsDatumTransform::datumTransformInfo( transform.sourceTransformId );
501     const QgsDatumTransform::TransformInfo destInfo = QgsDatumTransform::datumTransformInfo( transform.destinationTransformId );
502     if ( !foundPreferredNonDeprecated && ( ( srcInfo.preferred && !srcInfo.deprecated ) || transform.sourceTransformId == -1 )
503          && ( ( destInfo.preferred && !destInfo.deprecated ) || transform.destinationTransformId == -1 ) )
504     {
505       preferredNonDeprecated.sourceTransformId = transform.sourceTransformId;
506       preferredNonDeprecated.destinationTransformId = transform.destinationTransformId;
507       foundPreferredNonDeprecated = true;
508     }
509     else if ( !foundPreferred && ( srcInfo.preferred || transform.sourceTransformId == -1 ) &&
510               ( destInfo.preferred || transform.destinationTransformId == -1 ) )
511     {
512       preferred.sourceTransformId = transform.sourceTransformId;
513       preferred.destinationTransformId = transform.destinationTransformId;
514       foundPreferred = true;
515     }
516     else if ( !foundNonDeprecated && ( !srcInfo.deprecated || transform.sourceTransformId == -1 )
517               && ( !destInfo.deprecated || transform.destinationTransformId == -1 ) )
518     {
519       nonDeprecated.sourceTransformId = transform.sourceTransformId;
520       nonDeprecated.destinationTransformId = transform.destinationTransformId;
521       foundNonDeprecated = true;
522     }
523     else if ( !foundFallback )
524     {
525       fallback.sourceTransformId = transform.sourceTransformId;
526       fallback.destinationTransformId = transform.destinationTransformId;
527       foundFallback = true;
528     }
529   }
530   Q_NOWARN_DEPRECATED_POP
531   if ( foundPreferredNonDeprecated )
532     return preferredNonDeprecated;
533   else if ( foundPreferred )
534     return preferred;
535   else if ( foundNonDeprecated )
536     return nonDeprecated;
537   else
538     return fallback;
539 #endif
540 }
541 
formatScope(const QString & s)542 QString QgsCoordinateOperationWidget::formatScope( const QString &s )
543 {
544   QString scope = s;
545 
546   QRegularExpression reGNSS( QStringLiteral( "\\bGNSS\\b" ) );
547   scope.replace( reGNSS, QObject::tr( "GNSS (Global Navigation Satellite System)" ) );
548 
549   QRegularExpression reCORS( QStringLiteral( "\\bCORS\\b" ) );
550   scope.replace( reCORS, QObject::tr( "CORS (Continually Operating Reference Station)" ) );
551 
552   return scope;
553 }
554 
selectedOperation() const555 QgsCoordinateOperationWidget::OperationDetails QgsCoordinateOperationWidget::selectedOperation() const
556 {
557   int row = mCoordinateOperationTableWidget->currentRow();
558   OperationDetails op;
559 
560   if ( row >= 0 )
561   {
562     QTableWidgetItem *srcItem = mCoordinateOperationTableWidget->item( row, 0 );
563     op.sourceTransformId = srcItem ? srcItem->data( TransformIdRole ).toInt() : -1;
564     QTableWidgetItem *destItem = mCoordinateOperationTableWidget->item( row, 1 );
565     op.destinationTransformId = destItem ? destItem->data( TransformIdRole ).toInt() : -1;
566     op.proj = srcItem ? srcItem->data( ProjRole ).toString() : QString();
567     op.isAvailable = srcItem ? srcItem->data( AvailableRole ).toBool() : true;
568     op.allowFallback = mAllowFallbackCheckBox->isChecked();
569   }
570   else
571   {
572     op.sourceTransformId = -1;
573     op.destinationTransformId = -1;
574     op.proj = QString();
575   }
576   return op;
577 }
578 
setSelectedOperation(const QgsCoordinateOperationWidget::OperationDetails & operation)579 void QgsCoordinateOperationWidget::setSelectedOperation( const QgsCoordinateOperationWidget::OperationDetails &operation )
580 {
581   int prevRow = mCoordinateOperationTableWidget->currentRow();
582   mBlockSignals++;
583   for ( int row = 0; row < mCoordinateOperationTableWidget->rowCount(); ++row )
584   {
585     QTableWidgetItem *srcItem = mCoordinateOperationTableWidget->item( row, 0 );
586 #if PROJ_VERSION_MAJOR>=6
587     if ( srcItem && srcItem->data( ProjRole ).toString() == operation.proj )
588     {
589       mCoordinateOperationTableWidget->selectRow( row );
590       break;
591     }
592 #else
593     QTableWidgetItem *destItem = mCoordinateOperationTableWidget->item( row, 1 );
594 
595     // eww, gross logic. Ah well, it's of extremely limited lifespan anyway... it'll be ripped out as soon as we can drop proj < 6 support
596     if ( ( srcItem && destItem && operation.sourceTransformId == srcItem->data( TransformIdRole ).toInt() &&
597            operation.destinationTransformId == destItem->data( TransformIdRole ).toInt() )
598          || ( srcItem && destItem && operation.destinationTransformId == srcItem->data( TransformIdRole ).toInt() &&
599               operation.sourceTransformId == destItem->data( TransformIdRole ).toInt() )
600          || ( srcItem && !destItem && operation.sourceTransformId == srcItem->data( TransformIdRole ).toInt() &&
601               operation.destinationTransformId == -1 )
602          || ( !srcItem && destItem && operation.destinationTransformId == destItem->data( TransformIdRole ).toInt() &&
603               operation.sourceTransformId == -1 )
604          || ( srcItem && !destItem && operation.destinationTransformId == srcItem->data( TransformIdRole ).toInt() &&
605               operation.sourceTransformId == -1 )
606          || ( !srcItem && destItem && operation.sourceTransformId == destItem->data( TransformIdRole ).toInt() &&
607               operation.destinationTransformId == -1 )
608        )
609     {
610       mCoordinateOperationTableWidget->selectRow( row );
611       break;
612     }
613 #endif
614   }
615 
616   bool fallbackChanged = mAllowFallbackCheckBox->isChecked() != operation.allowFallback;
617   mAllowFallbackCheckBox->setChecked( operation.allowFallback );
618   mBlockSignals--;
619 
620   if ( mCoordinateOperationTableWidget->currentRow() != prevRow || fallbackChanged )
621     emit operationChanged();
622 }
623 
setSelectedOperationUsingContext(const QgsCoordinateTransformContext & context)624 void QgsCoordinateOperationWidget::setSelectedOperationUsingContext( const QgsCoordinateTransformContext &context )
625 {
626 #if PROJ_VERSION_MAJOR>=6
627   const QString op = context.calculateCoordinateOperation( mSourceCrs, mDestinationCrs );
628   if ( !op.isEmpty() )
629   {
630     OperationDetails deets;
631     deets.proj = op;
632     deets.allowFallback = context.allowFallbackTransform( mSourceCrs, mDestinationCrs );
633     setSelectedOperation( deets );
634   }
635   else
636   {
637     setSelectedOperation( defaultOperation() );
638   }
639 
640 #else
641   if ( context.hasTransform( mSourceCrs, mDestinationCrs ) )
642   {
643     Q_NOWARN_DEPRECATED_PUSH
644     const QgsDatumTransform::TransformPair op = context.calculateDatumTransforms( mSourceCrs, mDestinationCrs );
645     Q_NOWARN_DEPRECATED_POP
646     OperationDetails deets;
647     deets.sourceTransformId = op.sourceTransformId;
648     deets.destinationTransformId = op.destinationTransformId;
649     setSelectedOperation( deets );
650   }
651   else
652   {
653     setSelectedOperation( defaultOperation() );
654   }
655 #endif
656 }
657 
setShowFallbackOption(bool visible)658 void QgsCoordinateOperationWidget::setShowFallbackOption( bool visible )
659 {
660   mAllowFallbackCheckBox->setVisible( visible );
661 }
662 
gridShiftTransformation(const QString & itemText) const663 bool QgsCoordinateOperationWidget::gridShiftTransformation( const QString &itemText ) const
664 {
665   return !itemText.isEmpty() && !itemText.contains( QLatin1String( "towgs84" ), Qt::CaseInsensitive );
666 }
667 
testGridShiftFileAvailability(QTableWidgetItem * item) const668 bool QgsCoordinateOperationWidget::testGridShiftFileAvailability( QTableWidgetItem *item ) const
669 {
670   if ( !item )
671   {
672     return true;
673   }
674 
675   QString itemText = item->text();
676   if ( itemText.isEmpty() )
677   {
678     return true;
679   }
680 
681   char *projLib = getenv( "PROJ_LIB" );
682   if ( !projLib ) //no information about installation directory
683   {
684     return true;
685   }
686 
687   QStringList itemEqualSplit = itemText.split( '=' );
688   QString filename;
689   for ( int i = 1; i < itemEqualSplit.size(); ++i )
690   {
691     if ( i > 1 )
692     {
693       filename.append( '=' );
694     }
695     filename.append( itemEqualSplit.at( i ) );
696   }
697 
698   QDir projDir( projLib );
699   if ( projDir.exists() )
700   {
701     //look if filename in directory
702     QStringList fileList = projDir.entryList();
703     QStringList::const_iterator fileIt = fileList.constBegin();
704     for ( ; fileIt != fileList.constEnd(); ++fileIt )
705     {
706 #if defined(Q_OS_WIN)
707       if ( fileIt->compare( filename, Qt::CaseInsensitive ) == 0 )
708 #else
709       if ( fileIt->compare( filename ) == 0 )
710 #endif //Q_OS_WIN
711       {
712         return true;
713       }
714     }
715     item->setToolTip( tr( "File '%1' not found in directory '%2'" ).arg( filename, projDir.absolutePath() ) );
716     return false; //not found in PROJ_LIB directory
717   }
718   return true;
719 }
720 
tableCurrentItemChanged(QTableWidgetItem *,QTableWidgetItem *)721 void QgsCoordinateOperationWidget::tableCurrentItemChanged( QTableWidgetItem *, QTableWidgetItem * )
722 {
723   int row = mCoordinateOperationTableWidget->currentRow();
724   if ( row < 0 )
725   {
726     mLabelSrcDescription->clear();
727     mLabelDstDescription->clear();
728 #if PROJ_VERSION_MAJOR>=6
729     mAreaCanvas->hide();
730     mInstallGridButton->hide();
731 #endif
732   }
733   else
734   {
735     QTableWidgetItem *srcItem = mCoordinateOperationTableWidget->item( row, 0 );
736     mLabelSrcDescription->setText( srcItem ? srcItem->toolTip() : QString() );
737     if ( srcItem )
738     {
739       // find area of intersection of operation, source and dest bounding boxes
740       // see https://github.com/OSGeo/PROJ/issues/1549 for justification
741       const QgsRectangle operationRect = srcItem->data( BoundsRole ).value< QgsRectangle >();
742       const QgsRectangle sourceRect = mSourceCrs.bounds();
743       const QgsRectangle destRect = mDestinationCrs.bounds();
744       QgsRectangle rect = operationRect.intersect( sourceRect );
745       rect = rect.intersect( destRect );
746 
747       mAreaCanvas->setPreviewRect( rect );
748 #if PROJ_VERSION_MAJOR>=6
749       mAreaCanvas->show();
750 
751       const QStringList missingGrids = srcItem->data( MissingGridsRole ).toStringList();
752       mInstallGridButton->setVisible( !missingGrids.empty() );
753       if ( !missingGrids.empty() )
754       {
755         mInstallGridButton->setText( tr( "Install “%1” Grid…" ).arg( missingGrids.at( 0 ) ) );
756       }
757 #endif
758     }
759     else
760     {
761       mAreaCanvas->setPreviewRect( QgsRectangle() );
762 #if PROJ_VERSION_MAJOR>=6
763       mAreaCanvas->hide();
764       mInstallGridButton->hide();
765 #endif
766     }
767     QTableWidgetItem *destItem = mCoordinateOperationTableWidget->item( row, 1 );
768     mLabelDstDescription->setText( destItem ? destItem->toolTip() : QString() );
769   }
770   OperationDetails newOp = selectedOperation();
771 #if PROJ_VERSION_MAJOR>=6
772   if ( newOp.proj != mPreviousOp.proj && !mBlockSignals )
773     emit operationChanged();
774 #else
775   if ( newOp.sourceTransformId != mPreviousOp.sourceTransformId ||
776        newOp.destinationTransformId != mPreviousOp.destinationTransformId )
777     emit operationChanged();
778 #endif
779   mPreviousOp = newOp;
780 }
781 
setSourceCrs(const QgsCoordinateReferenceSystem & sourceCrs)782 void QgsCoordinateOperationWidget::setSourceCrs( const QgsCoordinateReferenceSystem &sourceCrs )
783 {
784   mSourceCrs = sourceCrs;
785 #if PROJ_VERSION_MAJOR>=6
786   mDatumTransforms = QgsDatumTransform::operations( mSourceCrs, mDestinationCrs, mShowSupersededCheckBox->isChecked() );
787 #else
788   Q_NOWARN_DEPRECATED_PUSH
789   mDatumTransforms = QgsDatumTransform::datumTransformations( mSourceCrs, mDestinationCrs );
790   Q_NOWARN_DEPRECATED_POP
791 #endif
792   loadAvailableOperations();
793 }
794 
setDestinationCrs(const QgsCoordinateReferenceSystem & destinationCrs)795 void QgsCoordinateOperationWidget::setDestinationCrs( const QgsCoordinateReferenceSystem &destinationCrs )
796 {
797   mDestinationCrs = destinationCrs;
798 #if PROJ_VERSION_MAJOR>=6
799   mDatumTransforms = QgsDatumTransform::operations( mSourceCrs, mDestinationCrs, mShowSupersededCheckBox->isChecked() );
800 #else
801   Q_NOWARN_DEPRECATED_PUSH
802   mDatumTransforms = QgsDatumTransform::datumTransformations( mSourceCrs, mDestinationCrs );
803   Q_NOWARN_DEPRECATED_POP
804 #endif
805   loadAvailableOperations();
806 }
807 
showSupersededToggled(bool)808 void QgsCoordinateOperationWidget::showSupersededToggled( bool )
809 {
810 #if PROJ_VERSION_MAJOR>=6
811   mDatumTransforms = QgsDatumTransform::operations( mSourceCrs, mDestinationCrs, mShowSupersededCheckBox->isChecked() );
812 #else
813   Q_NOWARN_DEPRECATED_PUSH
814   mDatumTransforms = QgsDatumTransform::datumTransformations( mSourceCrs, mDestinationCrs );
815   Q_NOWARN_DEPRECATED_POP
816 #endif
817   loadAvailableOperations();
818 }
819 
installGrid()820 void QgsCoordinateOperationWidget::installGrid()
821 {
822 #if PROJ_VERSION_MAJOR>=6
823   int row = mCoordinateOperationTableWidget->currentRow();
824   QTableWidgetItem *srcItem = mCoordinateOperationTableWidget->item( row, 0 );
825   if ( !srcItem )
826     return;
827 
828   const QStringList missingGrids = srcItem->data( MissingGridsRole ).toStringList();
829   if ( missingGrids.empty() )
830     return;
831 
832   const QStringList missingGridPackagesNames = srcItem->data( MissingGridPackageNamesRole ).toStringList();
833   const QString packageName = missingGridPackagesNames.value( 0 );
834   const QStringList missingGridUrls = srcItem->data( MissingGridUrlsRole ).toStringList();
835   const QString gridUrl = missingGridUrls.value( 0 );
836 
837   QString downloadMessage;
838   if ( !packageName.isEmpty() )
839   {
840     downloadMessage = tr( "This grid is part of the “<i>%1</i>” package, available for download from <a href=\"%2\">%2</a>." ).arg( packageName, gridUrl );
841   }
842   else if ( !gridUrl.isEmpty() )
843   {
844     downloadMessage = tr( "This grid is available for download from <a href=\"%1\">%1</a>." ).arg( gridUrl );
845   }
846 
847   const QString longMessage = tr( "<p>This transformation requires the grid file “%1”, which is not available for use on the system.</p>" ).arg( missingGrids.at( 0 ) );
848 
849   QgsInstallGridShiftFileDialog *dlg = new QgsInstallGridShiftFileDialog( missingGrids.at( 0 ), this );
850   dlg->setAttribute( Qt::WA_DeleteOnClose );
851   dlg->setWindowTitle( tr( "Install Grid File" ) );
852   dlg->setDescription( longMessage );
853   dlg->setDownloadMessage( downloadMessage );
854   dlg->exec();
855 
856 #endif
857 }
858