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