QGIS API Documentation  3.22.4-Białowieża (ce8e65e95e)
qgsprojectionselectiontreewidget.cpp
Go to the documentation of this file.
1 /***************************************************************************
2  * qgsprojectionselector.cpp *
3  * Copyright (C) 2005 by Tim Sutton *
5  * *
6  * This program is free software; you can redistribute it and/or modify *
7  * it under the terms of the GNU General Public License as published by *
8  * the Free Software Foundation; either version 2 of the License, or *
9  * (at your option) any later version. *
10  ***************************************************************************/
12 
13 //standard includes
14 #include <sqlite3.h>
15 
16 //qgis includes
17 #include "qgis.h" //magic numbers here
18 #include "qgsapplication.h"
19 #include "qgslogger.h"
21 #include "qgsmessagelog.h"
22 #include "qgssettings.h"
23 #include "qgsrectangle.h"
25 #include "qgsdatums.h"
26 #include "qgsprojoperation.h"
27 #include "qgsstringutils.h"
28 
29 //qt includes
30 #include <QFileInfo>
31 #include <QHeaderView>
32 #include <QResizeEvent>
33 #include <QMessageBox>
34 #include <QRegularExpression>
35 
37  : QWidget( parent )
38 {
39  setupUi( this );
40 
41  QFont f = teProjection->font();
42  f.setPointSize( f.pointSize() - 2 );
43  teProjection->setFont( f );
44 
45  leSearch->setShowSearchIcon( true );
46 
47  connect( lstCoordinateSystems, &QTreeWidget::itemDoubleClicked, this, &QgsProjectionSelectionTreeWidget::lstCoordinateSystems_itemDoubleClicked );
48  connect( lstRecent, &QTreeWidget::itemDoubleClicked, this, &QgsProjectionSelectionTreeWidget::lstRecent_itemDoubleClicked );
49  connect( lstCoordinateSystems, &QTreeWidget::currentItemChanged, this, &QgsProjectionSelectionTreeWidget::lstCoordinateSystems_currentItemChanged );
50  connect( lstRecent, &QTreeWidget::currentItemChanged, this, &QgsProjectionSelectionTreeWidget::lstRecent_currentItemChanged );
51  connect( cbxHideDeprecated, &QCheckBox::stateChanged, this, &QgsProjectionSelectionTreeWidget::updateFilter );
52  connect( leSearch, &QgsFilterLineEdit::textChanged, this, &QgsProjectionSelectionTreeWidget::updateFilter );
53 
54  mAreaCanvas->setVisible( mShowMap );
55 
56  // Get the full path name to the sqlite3 spatial reference database.
57  mSrsDatabaseFileName = QgsApplication::srsDatabaseFilePath();
58 
59  lstCoordinateSystems->header()->setSectionResizeMode( AuthidColumn, QHeaderView::Stretch );
60  lstCoordinateSystems->header()->resizeSection( QgisCrsIdColumn, 0 );
61  lstCoordinateSystems->header()->setSectionResizeMode( QgisCrsIdColumn, QHeaderView::Fixed );
62 
63  // Hide (internal) ID column
64  lstCoordinateSystems->setColumnHidden( QgisCrsIdColumn, true );
65 
66  lstRecent->header()->setSectionResizeMode( AuthidColumn, QHeaderView::Stretch );
67  lstRecent->header()->resizeSection( QgisCrsIdColumn, 0 );
68  lstRecent->header()->setSectionResizeMode( QgisCrsIdColumn, QHeaderView::Fixed );
69 
70  // Hide (internal) ID column
71  lstRecent->setColumnHidden( QgisCrsIdColumn, true );
72 
74 
75  mCheckBoxNoProjection->setHidden( true );
76  mCheckBoxNoProjection->setEnabled( false );
77  connect( mCheckBoxNoProjection, &QCheckBox::toggled, this, [ = ]
78  {
79  if ( !mBlockSignals )
80  {
81  emit crsSelected();
83  }
84  } );
85  connect( mCheckBoxNoProjection, &QCheckBox::toggled, this, [ = ]( bool checked )
86  {
87  if ( mCheckBoxNoProjection->isEnabled() )
88  {
89  mFrameProjections->setDisabled( checked );
90  }
91  } );
92 
93  QgsSettings settings;
94  mSplitter->restoreState( settings.value( QStringLiteral( "Windows/ProjectionSelector/splitterState" ) ).toByteArray() );
95 }
96 
98 {
99  QgsSettings settings;
100  settings.setValue( QStringLiteral( "Windows/ProjectionSelector/splitterState" ), mSplitter->saveState() );
101 
102  // Push current projection to front, only if set
103  const QgsCoordinateReferenceSystem selectedCrs = crs();
104  if ( selectedCrs.isValid() )
106 }
107 
109 {
110  lstCoordinateSystems->header()->resizeSection( NameColumn, event->size().width() - 240 );
111  lstCoordinateSystems->header()->resizeSection( AuthidColumn, 240 );
112  lstCoordinateSystems->header()->resizeSection( QgisCrsIdColumn, 0 );
113 
114  lstRecent->header()->resizeSection( NameColumn, event->size().width() - 240 );
115  lstRecent->header()->resizeSection( AuthidColumn, 240 );
116  lstRecent->header()->resizeSection( QgisCrsIdColumn, 0 );
117 }
118 
120 {
121  if ( mInitialized )
122  return;
123 
124  // ensure the projection list view is actually populated
125  // before we show this widget
126  loadCrsList( &mCrsFilter );
127  loadUserCrsList( &mCrsFilter );
128 
129  if ( !mRecentProjListDone )
130  {
131  for ( const QgsCoordinateReferenceSystem &crs : std::as_const( mRecentProjections ) )
132  insertRecent( crs );
133  mRecentProjListDone = true;
134  }
135 
136  // apply deferred selection
137  mBlockSignals = true; // we've already emitted the signal, when the deferred crs was first set
138  applySelection();
139  mBlockSignals = false;
140 
141  emit initialized();
142 
143  // Pass up the inheritance hierarchy
144  QWidget::showEvent( event );
145  mInitialized = true;
146 }
147 
148 QString QgsProjectionSelectionTreeWidget::ogcWmsCrsFilterAsSqlExpression( QSet<QString> *crsFilter )
149 {
150  QString sqlExpression = QStringLiteral( "1" ); // it's "SQL" for "true"
151  QMap<QString, QStringList> authParts;
152 
153  if ( !crsFilter )
154  return sqlExpression;
155 
156  /*
157  Ref: WMS 1.3.0, section 6.7.3 "Layer CRS":
158 
159  Every Layer CRS has an identifier that is a character string. Two types of
160  Layer CRS identifiers are permitted: "label" and "URL" identifiers:
161 
162  Label: The identifier includes a namespace prefix, a colon, a numeric or
163  string code, and in some instances a comma followed by additional
164  parameters. This International Standard defines three namespaces:
165  CRS, EpsgCrsId and AUTO2 [...]
166 
167  URL: The identifier is a fully-qualified Uniform Resource Locator that
168  references a publicly-accessible file containing a definition of the CRS
169  that is compliant with ISO 19111.
170  */
171 
172  // iterate through all incoming CRSs
173 
174  const auto authIds { *crsFilter };
175  for ( const QString &auth_id : authIds )
176  {
177  QStringList parts = auth_id.split( ':' );
178 
179  if ( parts.size() < 2 )
180  continue;
181 
182  authParts[ parts.at( 0 ).toUpper()].append( parts.at( 1 ).toUpper() );
183  }
184 
185  if ( authParts.isEmpty() )
186  return sqlExpression;
187 
188  if ( !authParts.isEmpty() )
189  {
190  QString prefix = QStringLiteral( " AND (" );
191  for ( auto it = authParts.constBegin(); it != authParts.constEnd(); ++it )
192  {
193  sqlExpression += QStringLiteral( "%1(upper(auth_name)='%2' AND upper(auth_id) IN ('%3'))" )
194  .arg( prefix,
195  it.key(),
196  it.value().join( QLatin1String( "','" ) ) );
197  prefix = QStringLiteral( " OR " );
198  }
199  sqlExpression += ')';
200  }
201 
202  QgsDebugMsgLevel( "exiting with '" + sqlExpression + "'.", 4 );
203 
204  return sqlExpression;
205 }
206 
207 void QgsProjectionSelectionTreeWidget::applySelection( int column, QString value )
208 {
209  if ( !mProjListDone || !mUserProjListDone )
210  {
211  // defer selection until loaded
212  mSearchColumn = column;
213  mSearchValue = value;
214  return;
215  }
216 
217  if ( column == QgsProjectionSelectionTreeWidget::None )
218  {
219  // invoked deferred selection
220  column = mSearchColumn;
221  value = mSearchValue;
222 
223  mSearchColumn = QgsProjectionSelectionTreeWidget::None;
224  mSearchValue.clear();
225  }
226 
227  if ( column == QgsProjectionSelectionTreeWidget::None )
228  return;
229 
230  QList<QTreeWidgetItem *> nodes = lstCoordinateSystems->findItems( value, Qt::MatchExactly | Qt::MatchRecursive, column );
231  if ( !nodes.isEmpty() )
232  {
233  QgsDebugMsgLevel( QStringLiteral( "found %1,%2" ).arg( column ).arg( value ), 4 );
234  lstCoordinateSystems->setCurrentItem( nodes.first() );
235  }
236  else
237  {
238  QgsDebugMsgLevel( QStringLiteral( "nothing found for %1,%2" ).arg( column ).arg( value ), 4 );
239  // deselect the selected item to avoid confusing the user
240  lstCoordinateSystems->clearSelection();
241  lstRecent->clearSelection();
242  teProjection->clear();
243  }
244 }
245 
246 void QgsProjectionSelectionTreeWidget::insertRecent( const QgsCoordinateReferenceSystem &crs )
247 {
248  if ( !mProjListDone || !mUserProjListDone )
249  return;
250 
251  QList<QTreeWidgetItem *> nodes = lstCoordinateSystems->findItems( QString::number( crs.srsid() ), Qt::MatchExactly | Qt::MatchRecursive, QgisCrsIdColumn );
252  if ( nodes.isEmpty() )
253  return;
254 
255  lstRecent->insertTopLevelItem( 0, new QTreeWidgetItem( lstRecent, QStringList()
256  << nodes.first()->text( NameColumn )
257  << nodes.first()->text( AuthidColumn )
258  << nodes.first()->text( QgisCrsIdColumn ) ) );
259 }
260 
261 //note this line just returns the projection name!
262 QString QgsProjectionSelectionTreeWidget::selectedName()
263 {
264  // return the selected wkt name from the list view
265  QTreeWidgetItem *lvi = lstCoordinateSystems->currentItem();
266  return lvi ? lvi->text( NameColumn ) : QString();
267 }
268 
270 {
271  if ( !crs.isValid() )
272  {
273  mCheckBoxNoProjection->setChecked( true );
274  }
275  else
276  {
277  bool changed = false;
278  if ( !mInitialized )
279  {
280  changed = mDeferredLoadCrs != crs;
281  mDeferredLoadCrs = crs;
282  }
283  mBlockSignals = true;
284  mCheckBoxNoProjection->setChecked( false );
285  mBlockSignals = false;
286 
287  if ( !crs.authid().isEmpty() )
288  applySelection( AuthidColumn, crs.authid() );
289  else
290  loadUnknownCrs( crs );
291  if ( changed )
292  {
293  emit crsSelected();
295  }
296  }
297 }
298 
300 {
301  mAreaCanvas->setCanvasRect( rect );
302 }
303 
305 {
306  return mAreaCanvas->canvasRect();
307 }
308 
309 QString QgsProjectionSelectionTreeWidget::getSelectedExpression( const QString &expression ) const
310 {
311  // Only return the attribute if there is a node in the tree
312  // selected that has an srs_id. This prevents error if the user
313  // selects a top-level node rather than an actual coordinate
314  // system
315  //
316  // Get the selected node and make sure it is a srs andx
317  // not a top-level projection node
318  QTreeWidgetItem *lvi = lstCoordinateSystems->currentItem();
319  if ( !lvi || lvi->text( QgisCrsIdColumn ).isEmpty() )
320  return QString();
321 
322  //
323  // Determine if this is a user projection or a system on
324  // user projection defs all have srs_id >= 100000
325  //
326  QString databaseFileName;
327  if ( lvi->text( QgisCrsIdColumn ).toLong() >= USER_CRS_START_ID )
328  {
329  databaseFileName = QgsApplication::qgisUserDatabaseFilePath();
330  if ( !QFileInfo::exists( databaseFileName ) )
331  {
332  return QString();
333  }
334  }
335  else
336  {
337  databaseFileName = mSrsDatabaseFileName;
338  }
339 
340  //
341  // set up the database
342  // XXX We could probably hold the database open for the life of this object,
343  // assuming that it will never be used anywhere else. Given the low overhead,
344  // opening it each time seems to be a reasonable approach at this time.
345  sqlite3 *database = nullptr;
346  int rc = sqlite3_open_v2( databaseFileName.toUtf8().constData(), &database, SQLITE_OPEN_READONLY, nullptr );
347  if ( rc )
348  {
349  QgsMessageLog::logMessage( tr( "Resource Location Error" ), tr( "Error reading database file from: \n %1\n"
350  "Because of this the projection selector will not work…" ).arg( databaseFileName ),
351  Qgis::MessageLevel::Critical );
352  return QString();
353  }
354 
355  // prepare the sql statement
356  const char *tail = nullptr;
357  sqlite3_stmt *stmt = nullptr;
358  QString sql = QStringLiteral( "select %1 from tbl_srs where srs_id=%2" )
359  .arg( expression,
360  lvi->text( QgisCrsIdColumn ) );
361 
362  QgsDebugMsgLevel( QStringLiteral( "Finding selected attribute using : %1" ).arg( sql ), 4 );
363  rc = sqlite3_prepare( database, sql.toUtf8(), sql.toUtf8().length(), &stmt, &tail );
364  // XXX Need to free memory from the error msg if one is set
365  QString attributeValue;
366  if ( rc == SQLITE_OK && sqlite3_step( stmt ) == SQLITE_ROW )
367  {
368  // get the first row of the result set
369  attributeValue = QString::fromUtf8( ( char * )sqlite3_column_text( stmt, 0 ) );
370  }
371 
372  // close the statement
373  sqlite3_finalize( stmt );
374  // close the database
375  sqlite3_close( database );
376 
377  // return the srs
378  return attributeValue;
379 }
380 
382 {
383  if ( mCheckBoxNoProjection->isEnabled() && mCheckBoxNoProjection->isChecked() )
385 
386  if ( !mInitialized && mDeferredLoadCrs.isValid() )
387  return mDeferredLoadCrs;
388 
389  const QString srsIdString = getSelectedExpression( QStringLiteral( "srs_id" ) );
390  if ( !srsIdString.isEmpty() )
391  {
392  int srid = srsIdString.toLong();
393  if ( srid >= USER_CRS_START_ID )
394  return QgsCoordinateReferenceSystem::fromOgcWmsCrs( QStringLiteral( "USER:%1" ).arg( srid ) );
395  else
396  return QgsCoordinateReferenceSystem::fromOgcWmsCrs( getSelectedExpression( QStringLiteral( "upper(auth_name||':'||auth_id)" ) ) );
397  }
398  else
399  {
400  // custom CRS
401  QTreeWidgetItem *lvi = lstCoordinateSystems->currentItem();
402  if ( lvi && lvi->data( 0, RoleWkt ).isValid() )
403  return QgsCoordinateReferenceSystem::fromWkt( lvi->data( 0, RoleWkt ).toString() );
404  else if ( lvi && lvi->data( 0, RoleProj ).isValid() )
405  return QgsCoordinateReferenceSystem::fromProj( lvi->data( 0, RoleProj ).toString() );
406  else
408  }
409 }
410 
412 {
413  mCheckBoxNoProjection->setVisible( show );
414  mCheckBoxNoProjection->setEnabled( show );
415  if ( show )
416  {
417  mFrameProjections->setDisabled( mCheckBoxNoProjection->isChecked() );
418  }
419 }
420 
422 {
423  mShowMap = show;
424  mAreaCanvas->setVisible( show );
425 }
426 
428 {
429  return !mCheckBoxNoProjection->isHidden();
430 }
431 
433 {
434  mCheckBoxNoProjection->setText( text );
435 }
436 
438 {
439  return mShowMap;
440 }
441 
443 {
444  QTreeWidgetItem *item = lstCoordinateSystems->currentItem();
445  if ( mCheckBoxNoProjection->isChecked() )
446  return true;
447  else if ( !mInitialized && mDeferredLoadCrs.isValid() )
448  return true;
449  else
450  return item && ( !item->text( QgisCrsIdColumn ).isEmpty() || item->data( 0, RoleWkt ).isValid() );
451 }
452 
453 long QgsProjectionSelectionTreeWidget::selectedCrsId()
454 {
455  QTreeWidgetItem *item = lstCoordinateSystems->currentItem();
456 
457  if ( item && !item->text( QgisCrsIdColumn ).isEmpty() )
458  return lstCoordinateSystems->currentItem()->text( QgisCrsIdColumn ).toLong();
459  else
460  return 0;
461 }
462 
463 
464 void QgsProjectionSelectionTreeWidget::setOgcWmsCrsFilter( const QSet<QString> &crsFilter )
465 {
466  mCrsFilter = crsFilter;
467  mProjListDone = false;
468  mUserProjListDone = false;
469  lstCoordinateSystems->clear();
470 }
471 
472 void QgsProjectionSelectionTreeWidget::loadUserCrsList( QSet<QString> *crsFilter )
473 {
474  if ( mUserProjListDone )
475  return;
476 
477  QgsDebugMsgLevel( QStringLiteral( "Fetching user projection list..." ), 4 );
478 
479  // User defined coordinate system node
480  // Make in an italic font to distinguish them from real projections
481  mUserProjList = new QTreeWidgetItem( lstCoordinateSystems, QStringList( tr( "User Defined Coordinate Systems" ) ) );
482  mUserProjList->setFlags( mUserProjList->flags() & ~Qt::ItemIsSelectable );
483 
484  QFont fontTemp = mUserProjList->font( 0 );
485  fontTemp.setItalic( true );
486  fontTemp.setBold( true );
487  mUserProjList->setFont( 0, fontTemp );
488  mUserProjList->setIcon( 0, QgsApplication::getThemeIcon( QStringLiteral( "/user.svg" ) ) );
489 
490  const QList<QgsCoordinateReferenceSystemRegistry::UserCrsDetails> userCrsList = QgsApplication::coordinateReferenceSystemRegistry()->userCrsList();
491  for ( const QgsCoordinateReferenceSystemRegistry::UserCrsDetails &details : userCrsList )
492  {
493  const QString authid = QStringLiteral( "USER:%1" ).arg( details.id );
494  if ( crsFilter && !crsFilter->isEmpty() && !crsFilter->contains( authid ) && !crsFilter->contains( authid.toLower() ) )
495  continue;
496 
497  QTreeWidgetItem *newItem = new QTreeWidgetItem( mUserProjList, QStringList() << details.name );
498  newItem->setText( QgisCrsIdColumn, QString::number( details.id ) );
499  newItem->setText( AuthidColumn, authid );
500  }
501 
502  mUserProjListDone = true;
503 }
504 
505 void QgsProjectionSelectionTreeWidget::loadCrsList( QSet<QString> *crsFilter )
506 {
507  if ( mProjListDone )
508  return;
509 
510  // convert our Coordinate Reference System filter into the SQL expression
511  QString sqlFilter = ogcWmsCrsFilterAsSqlExpression( crsFilter );
512 
513  // Create the top-level nodes for the list view of projections
514  // Make in an italic font to distinguish them from real projections
515  //
516  // Geographic coordinate system node
517  mGeoList = new QTreeWidgetItem( lstCoordinateSystems, QStringList( tr( "Geographic Coordinate Systems" ) ) );
518  mGeoList->setFlags( mGeoList->flags() & ~Qt::ItemIsSelectable );
519 
520  QFont fontTemp = mGeoList->font( 0 );
521  fontTemp.setItalic( true );
522  fontTemp.setBold( true );
523  mGeoList->setFont( 0, fontTemp );
524  mGeoList->setIcon( 0, QgsApplication::getThemeIcon( QStringLiteral( "/mIconProjectionEnabled.svg" ) ) );
525 
526  // Projected coordinate system node
527  mProjList = new QTreeWidgetItem( lstCoordinateSystems, QStringList( tr( "Projected Coordinate Systems" ) ) );
528  mProjList->setFlags( mProjList->flags() & ~Qt::ItemIsSelectable );
529 
530  fontTemp = mProjList->font( 0 );
531  fontTemp.setItalic( true );
532  fontTemp.setBold( true );
533  mProjList->setFont( 0, fontTemp );
534  mProjList->setIcon( 0, QgsApplication::getThemeIcon( QStringLiteral( "/transformed.svg" ) ) );
535 
536  //bail out in case the projections db does not exist
537  //this is necessary in case the pc is running linux with a
538  //read only filesystem because otherwise sqlite will try
539  //to create the db file on the fly
540 
541  if ( !QFileInfo::exists( mSrsDatabaseFileName ) )
542  {
543  mProjListDone = true;
544  return;
545  }
546 
547  // open the database containing the spatial reference data
548  sqlite3 *database = nullptr;
549  int rc = sqlite3_open_v2( mSrsDatabaseFileName.toUtf8().constData(), &database, SQLITE_OPEN_READONLY, nullptr );
550  if ( rc )
551  {
552  // XXX This will likely never happen since on open, sqlite creates the
553  // database if it does not exist.
554  showDBMissingWarning( mSrsDatabaseFileName );
555  return;
556  }
557 
558  const char *tail = nullptr;
559  sqlite3_stmt *stmt = nullptr;
560  // Set up the query to retrieve the projection information needed to populate the list
561  //note I am giving the full field names for clarity here and in case someone
562  //changes the underlying view TS
563  QString sql = QStringLiteral( "select description, srs_id, upper(auth_name||':'||auth_id), is_geo, name, parameters, deprecated from vw_srs where %1 order by name,description" )
564  .arg( sqlFilter );
565 
566  rc = sqlite3_prepare( database, sql.toUtf8(), sql.toUtf8().length(), &stmt, &tail );
567  // XXX Need to free memory from the error msg if one is set
568  if ( rc == SQLITE_OK )
569  {
570  QTreeWidgetItem *newItem = nullptr;
571  // Cache some stuff to speed up creating of the list of projected
572  // spatial reference systems
573  QString previousSrsType;
574  QTreeWidgetItem *previousSrsTypeNode = nullptr;
575 
576  while ( sqlite3_step( stmt ) == SQLITE_ROW )
577  {
578  // check to see if the srs is geographic
579  int isGeo = sqlite3_column_int( stmt, 3 );
580  if ( isGeo )
581  {
582  // this is a geographic coordinate system
583  // Add it to the tree (field 0)
584  newItem = new QTreeWidgetItem( mGeoList, QStringList( QString::fromUtf8( ( char * )sqlite3_column_text( stmt, 0 ) ) ) );
585 
586  // display the authority name (field 2) in the second column of the list view
587  newItem->setText( AuthidColumn, QString::fromUtf8( ( char * )sqlite3_column_text( stmt, 2 ) ) );
588 
589  // display the qgis srs_id (field 1) in the third column of the list view
590  newItem->setText( QgisCrsIdColumn, QString::fromUtf8( ( char * )sqlite3_column_text( stmt, 1 ) ) );
591  }
592  else
593  {
594  // This is a projected srs
595  QTreeWidgetItem *node = nullptr;
596  QString srsType = QString::fromUtf8( ( char * )sqlite3_column_text( stmt, 4 ) );
597  if ( srsType.isEmpty() )
598  srsType = tr( "Other" );
599 
600  // Find the node for this type and add the projection to it
601  // If the node doesn't exist, create it
602  if ( srsType == previousSrsType )
603  {
604  node = previousSrsTypeNode;
605  }
606  else
607  {
608  // Different from last one, need to search
609  QList<QTreeWidgetItem *> nodes = lstCoordinateSystems->findItems( srsType, Qt::MatchExactly | Qt::MatchRecursive, NameColumn );
610  if ( nodes.isEmpty() )
611  {
612  // the node doesn't exist -- create it
613  // Make in an italic font to distinguish them from real projections
614  node = new QTreeWidgetItem( mProjList, QStringList( srsType ) );
615  node->setFlags( node->flags() & ~Qt::ItemIsSelectable );
616 
617  QFont fontTemp = node->font( 0 );
618  fontTemp.setItalic( true );
619  node->setFont( 0, fontTemp );
620  }
621  else
622  {
623  node = nodes.first();
624  }
625  // Update the cache.
626  previousSrsType = srsType;
627  previousSrsTypeNode = node;
628  }
629  // add the item, setting the projection name in the first column of the list view
630  newItem = new QTreeWidgetItem( node, QStringList( QString::fromUtf8( ( char * )sqlite3_column_text( stmt, 0 ) ) ) );
631  // display the authority id (field 2) in the second column of the list view
632  newItem->setText( AuthidColumn, QString::fromUtf8( ( char * )sqlite3_column_text( stmt, 2 ) ) );
633  // display the qgis srs_id (field 1) in the third column of the list view
634  newItem->setText( QgisCrsIdColumn, QString::fromUtf8( ( char * )sqlite3_column_text( stmt, 1 ) ) );
635  // expand also parent node
636  newItem->parent()->setExpanded( true );
637  }
638 
639  // display the qgis deprecated in the user data of the item
640  newItem->setData( 0, RoleDeprecated, QString::fromUtf8( ( char * )sqlite3_column_text( stmt, 6 ) ) );
641  newItem->setHidden( cbxHideDeprecated->isChecked() );
642  }
643  mProjList->setExpanded( true );
644  }
645 
646  // close the sqlite3 statement
647  sqlite3_finalize( stmt );
648  // close the database
649  sqlite3_close( database );
650 
651  mProjListDone = true;
652 }
653 
654 void QgsProjectionSelectionTreeWidget::loadUnknownCrs( const QgsCoordinateReferenceSystem &crs )
655 {
656  if ( !mUnknownList )
657  {
658  mUnknownList = new QTreeWidgetItem( lstCoordinateSystems, QStringList( tr( "Custom Coordinate Systems" ) ) );
659  mUnknownList->setFlags( mUnknownList->flags() & ~Qt::ItemIsSelectable );
660 
661  QFont fontTemp = mUnknownList->font( 0 );
662  fontTemp.setItalic( true );
663  fontTemp.setBold( true );
664  mUnknownList->setFont( 0, fontTemp );
665  mUnknownList->setIcon( 0, QgsApplication::getThemeIcon( QStringLiteral( "/user.svg" ) ) );
666  }
667 
668  QTreeWidgetItem *newItem = new QTreeWidgetItem( mUnknownList, QStringList( crs.description().isEmpty() ? QObject::tr( "Custom CRS" ) : crs.description() ) );
669  newItem->setData( 0, RoleWkt, crs.toWkt( QgsCoordinateReferenceSystem::WKT_PREFERRED ) );
670  newItem->setData( 0, RoleProj, crs.toProj() );
671 
672  lstCoordinateSystems->setCurrentItem( newItem );
673 }
674 
675 // New coordinate system selected from the list
676 void QgsProjectionSelectionTreeWidget::lstCoordinateSystems_currentItemChanged( QTreeWidgetItem *current, QTreeWidgetItem * )
677 {
678  QgsDebugMsgLevel( QStringLiteral( "Entered." ), 4 );
679 
680  if ( !current )
681  {
682  QgsDebugMsgLevel( QStringLiteral( "no current item" ), 4 );
683  return;
684  }
685 
686  lstCoordinateSystems->scrollToItem( current );
687 
688  // If the item has children, it's not an end node in the tree, and
689  // hence is just a grouping thingy, not an actual CRS.
690  if ( current->childCount() == 0 )
691  {
692  // Found a real CRS
693  if ( !mBlockSignals )
694  {
695  emit crsSelected();
696  emit hasValidSelectionChanged( true );
697  }
698 
699  updateBoundsPreview();
700 
701  const QString crsId = current->text( QgisCrsIdColumn );
702  if ( !crsId.isEmpty() )
703  {
704  QList<QTreeWidgetItem *> nodes = lstRecent->findItems( current->text( QgisCrsIdColumn ), Qt::MatchExactly, QgisCrsIdColumn );
705  if ( !nodes.isEmpty() )
706  {
707  QgsDebugMsgLevel( QStringLiteral( "found srs %1 in recent" ).arg( current->text( QgisCrsIdColumn ) ), 4 );
708  lstRecent->setCurrentItem( nodes.first() );
709  }
710  else
711  {
712  QgsDebugMsgLevel( QStringLiteral( "srs %1 not recent" ).arg( current->text( QgisCrsIdColumn ) ), 4 );
713  lstRecent->clearSelection();
714  lstCoordinateSystems->setFocus( Qt::OtherFocusReason );
715  }
716  }
717  else
718  {
719  lstRecent->clearSelection();
720  lstCoordinateSystems->setFocus( Qt::OtherFocusReason );
721  }
722  }
723  else
724  {
725  // Not a CRS - remove the highlight so the user doesn't get too confused
726  current->setSelected( false );
727  teProjection->clear();
728  lstRecent->clearSelection();
729  emit hasValidSelectionChanged( false );
730  }
731 }
732 
733 void QgsProjectionSelectionTreeWidget::lstCoordinateSystems_itemDoubleClicked( QTreeWidgetItem *current, int column )
734 {
735  Q_UNUSED( column )
736 
737  QgsDebugMsgLevel( QStringLiteral( "Entered." ), 4 );
738 
739  if ( !current )
740  {
741  QgsDebugMsgLevel( QStringLiteral( "no current item" ), 4 );
742  return;
743  }
744 
745  // If the item has children, it's not an end node in the tree, and
746  // hence is just a grouping thingy, not an actual CRS.
747  if ( current->childCount() == 0 )
749 }
750 
751 void QgsProjectionSelectionTreeWidget::lstRecent_currentItemChanged( QTreeWidgetItem *current, QTreeWidgetItem * )
752 {
753  QgsDebugMsgLevel( QStringLiteral( "Entered." ), 4 );
754 
755  if ( !current )
756  {
757  QgsDebugMsgLevel( QStringLiteral( "no current item" ), 4 );
758  return;
759  }
760 
761  lstRecent->scrollToItem( current );
762 
763  QList<QTreeWidgetItem *> nodes = lstCoordinateSystems->findItems( current->text( QgisCrsIdColumn ), Qt::MatchExactly | Qt::MatchRecursive, QgisCrsIdColumn );
764  if ( !nodes.isEmpty() )
765  lstCoordinateSystems->setCurrentItem( nodes.first() );
766 }
767 
768 void QgsProjectionSelectionTreeWidget::lstRecent_itemDoubleClicked( QTreeWidgetItem *current, int column )
769 {
770  Q_UNUSED( column )
771 
772  QgsDebugMsgLevel( QStringLiteral( "Entered." ), 4 );
773 
774  if ( !current )
775  {
776  QgsDebugMsgLevel( QStringLiteral( "no current item" ), 4 );
777  return;
778  }
779 
780  QList<QTreeWidgetItem *> nodes = lstCoordinateSystems->findItems( current->text( QgisCrsIdColumn ), Qt::MatchExactly | Qt::MatchRecursive, QgisCrsIdColumn );
781  if ( !nodes.isEmpty() )
783 }
784 
785 void QgsProjectionSelectionTreeWidget::updateFilter()
786 {
787  QString filterTxtCopy = QgsStringUtils::qRegExpEscape( leSearch->text() );
788  filterTxtCopy.replace( QRegularExpression( "\\s+" ), QStringLiteral( ".*" ) );
789  const QRegularExpression re( filterTxtCopy, QRegularExpression::PatternOption::CaseInsensitiveOption );
790 
791  const bool hideDeprecated = cbxHideDeprecated->isChecked();
792 
793  auto filterTreeWidget = [ = ]( QTreeWidget * tree )
794  {
795  QTreeWidgetItemIterator itr( tree );
796  while ( *itr )
797  {
798  if ( ( *itr )->childCount() == 0 ) // it's an end node aka a projection
799  {
800  if ( hideDeprecated && ( *itr )->data( 0, RoleDeprecated ).toBool() )
801  {
802  ( *itr )->setHidden( true );
803  if ( ( *itr )->isSelected() )
804  {
805  ( *itr )->setSelected( false );
806  teProjection->clear();
807  }
808  }
809  else if ( ( *itr )->text( NameColumn ).contains( re )
810  || ( *itr )->text( AuthidColumn ).contains( re )
811  )
812  {
813  ( *itr )->setHidden( false );
814  QTreeWidgetItem *parent = ( *itr )->parent();
815  while ( parent )
816  {
817  parent->setExpanded( true );
818  parent->setHidden( false );
819  parent = parent->parent();
820  }
821  }
822  else
823  {
824  ( *itr )->setHidden( true );
825  }
826  }
827  else
828  {
829  ( *itr )->setHidden( true );
830  }
831  ++itr;
832  }
833  };
834 
835  // filter recent crs's
836  filterTreeWidget( lstRecent );
837 
838  // filter crs's
839  filterTreeWidget( lstCoordinateSystems );
840 }
841 
843 {
844 }
845 
846 long QgsProjectionSelectionTreeWidget::getLargestCrsIdMatch( const QString &sql )
847 {
848  long srsId = 0;
849 
850  //
851  // Now perform the actual search
852  //
853 
854  sqlite3 *database = nullptr;
855  const char *tail = nullptr;
856  sqlite3_stmt *stmt = nullptr;
857  int result;
858 
859  // first we search the users db as any srsid there will be definition be greater than in sys db
860 
861  //check the db is available
862  QString databaseFileName = QgsApplication::qgisUserDatabaseFilePath();
863  if ( QFileInfo::exists( databaseFileName ) ) //only bother trying to open if the file exists
864  {
865  result = sqlite3_open_v2( databaseFileName.toUtf8().constData(), &database, SQLITE_OPEN_READONLY, nullptr );
866  if ( result )
867  {
868  // XXX This will likely never happen since on open, sqlite creates the
869  // database if it does not exist. But we checked earlier for its existence
870  // and aborted in that case. This is because we may be running from read only
871  // media such as live cd and don't want to force trying to create a db.
872  showDBMissingWarning( databaseFileName );
873  return 0;
874  }
875 
876  result = sqlite3_prepare( database, sql.toUtf8(), sql.toUtf8().length(), &stmt, &tail );
877  // XXX Need to free memory from the error msg if one is set
878  if ( result == SQLITE_OK && sqlite3_step( stmt ) == SQLITE_ROW )
879  {
880  QString srsIdString = QString::fromUtf8( ( char * )sqlite3_column_text( stmt, 0 ) );
881  srsId = srsIdString.toLong();
882  // close the sqlite3 statement
883  sqlite3_finalize( stmt );
884  sqlite3_close( database );
885  return srsId;
886  }
887  }
888  else
889  {
890  //only bother looking in srs.db if it wasn't found above
891  result = sqlite3_open_v2( mSrsDatabaseFileName.toUtf8().constData(), &database, SQLITE_OPEN_READONLY, nullptr );
892  if ( result )
893  {
894  QgsDebugMsg( QStringLiteral( "Can't open * user * database: %1" ).arg( sqlite3_errmsg( database ) ) );
895  //no need for assert because user db may not have been created yet
896  return 0;
897  }
898  }
899 
900  result = sqlite3_prepare( database, sql.toUtf8(), sql.toUtf8().length(), &stmt, &tail );
901  // XXX Need to free memory from the error msg if one is set
902  if ( result == SQLITE_OK && sqlite3_step( stmt ) == SQLITE_ROW )
903  {
904  QString srsIdString = QString::fromUtf8( ( char * )sqlite3_column_text( stmt, 0 ) );
905  srsId = srsIdString.toLong();
906  }
907 
908  // close the sqlite3 statement
909  sqlite3_finalize( stmt );
910  sqlite3_close( database );
911 
912  return srsId;
913 }
914 
915 void QgsProjectionSelectionTreeWidget::updateBoundsPreview()
916 {
917  QTreeWidgetItem *lvi = lstCoordinateSystems->currentItem();
918  if ( !lvi || ( lvi->text( QgisCrsIdColumn ).isEmpty() && !lvi->data( 0, RoleWkt ).isValid() ) )
919  return;
920 
921  QgsCoordinateReferenceSystem currentCrs = crs();
922  if ( !currentCrs.isValid() )
923  return;
924 
925  QgsRectangle rect = currentCrs.bounds();
926  QString extentString = tr( "Extent not known" );
927  mAreaCanvas->setPreviewRect( rect );
928  if ( !qgsDoubleNear( rect.area(), 0.0 ) )
929  {
930  extentString = QStringLiteral( "%1, %2, %3, %4" )
931  .arg( rect.xMinimum(), 0, 'f', 2 )
932  .arg( rect.yMinimum(), 0, 'f', 2 )
933  .arg( rect.xMaximum(), 0, 'f', 2 )
934  .arg( rect.yMaximum(), 0, 'f', 2 );
935  }
936 
937  QStringList properties;
938  if ( currentCrs.isGeographic() )
939  properties << tr( "Geographic (uses latitude and longitude for coordinates)" );
940  else
941  {
942  properties << tr( "Units: %1" ).arg( QgsUnitTypes::toString( currentCrs.mapUnits() ) );
943  }
944  properties << ( currentCrs.isDynamic() ? tr( "Dynamic (relies on a datum which is not plate-fixed)" ) : tr( "Static (relies on a datum which is plate-fixed)" ) );
945 
946  try
947  {
948  const QString celestialBody = currentCrs.celestialBodyName();
949  if ( !celestialBody.isEmpty() )
950  {
951  properties << tr( "Celestial body: %1" ).arg( celestialBody );
952  }
953  }
954  catch ( QgsNotSupportedException & )
955  {
956 
957  }
958 
959  try
960  {
961  const QgsDatumEnsemble ensemble = currentCrs.datumEnsemble();
962  if ( ensemble.isValid() )
963  {
964  QString id;
965  if ( !ensemble.code().isEmpty() )
966  id = QStringLiteral( "<i>%1</i> (%2:%3)" ).arg( ensemble.name(), ensemble.authority(), ensemble.code() );
967  else
968  id = QStringLiteral( "<i>%</i>”" ).arg( ensemble.name() );
969  if ( ensemble.accuracy() > 0 )
970  {
971  properties << tr( "Based on %1, which has a limited accuracy of <b>at best %2 meters</b>." ).arg( id ).arg( ensemble.accuracy() );
972  }
973  else
974  {
975  properties << tr( "Based on %1, which has a limited accuracy." ).arg( id );
976  }
977  }
978  }
979  catch ( QgsNotSupportedException & )
980  {
981 
982  }
983 
984  const QgsProjOperation operation = currentCrs.operation();
985  properties << tr( "Method: %1" ).arg( operation.description() );
986 
987  const QString propertiesString = QStringLiteral( "<dt><b>%1</b></dt><dd><ul><li>%2</li></ul></dd>" ).arg( tr( "Properties" ),
988  properties.join( QLatin1String( "</li><li>" ) ) );
989 
990  const QString extentHtml = QStringLiteral( "<dt><b>%1</b></dt><dd>%2</dd>" ).arg( tr( "Extent" ), extentString );
991  const QString wktString = QStringLiteral( "<dt><b>%1</b></dt><dd><code>%2</code></dd>" ).arg( tr( "WKT" ), currentCrs.toWkt( QgsCoordinateReferenceSystem::WKT_PREFERRED, true ).replace( '\n', QLatin1String( "<br>" ) ).replace( ' ', QLatin1String( "&nbsp;" ) ) );
992  const QString proj4String = QStringLiteral( "<dt><b>%1</b></dt><dd><code>%2</code></dd>" ).arg( tr( "Proj4" ), currentCrs.toProj() );
993 
994 #ifdef Q_OS_WIN
995  const int smallerPointSize = std::max( font().pointSize() - 1, 8 ); // bit less on windows, due to poor rendering of small point sizes
996 #else
997  const int smallerPointSize = std::max( font().pointSize() - 2, 6 );
998 #endif
999 
1000  teProjection->setText( QStringLiteral( "<div style=\"font-size: %1pt\"><h3>%2</h3><dl>" ).arg( smallerPointSize ).arg( selectedName() ) + propertiesString + wktString + proj4String + extentHtml + QStringLiteral( "</dl></div>" ) );
1001 }
1002 
1003 QStringList QgsProjectionSelectionTreeWidget::authorities()
1004 {
1005  sqlite3 *database = nullptr;
1006  const char *tail = nullptr;
1007  sqlite3_stmt *stmt = nullptr;
1008 
1009  int result = sqlite3_open_v2( mSrsDatabaseFileName.toUtf8().constData(), &database, SQLITE_OPEN_READONLY, nullptr );
1010  if ( result )
1011  {
1012  QgsDebugMsg( QStringLiteral( "Can't open * user * database: %1" ).arg( sqlite3_errmsg( database ) ) );
1013  //no need for assert because user db may not have been created yet
1014  return QStringList();
1015  }
1016 
1017  QString sql = QStringLiteral( "select distinct auth_name from tbl_srs" );
1018  result = sqlite3_prepare( database, sql.toUtf8(), sql.toUtf8().length(), &stmt, &tail );
1019 
1020  QStringList authorities;
1021  if ( result == SQLITE_OK )
1022  {
1023  while ( sqlite3_step( stmt ) == SQLITE_ROW )
1024  {
1025  authorities << QString::fromUtf8( ( char * )sqlite3_column_text( stmt, 0 ) );
1026  }
1027 
1028  }
1029 
1030  // close the sqlite3 statement
1031  sqlite3_finalize( stmt );
1032  sqlite3_close( database );
1033 
1034  return authorities;
1035 }
1036 
1037 QString QgsProjectionSelectionTreeWidget::sqlSafeString( const QString &theSQL ) const
1038 {
1039  QString retval = theSQL;
1040  retval.replace( '\\', QLatin1String( "\\\\" ) );
1041  retval.replace( '\"', QLatin1String( "\\\"" ) );
1042  retval.replace( '\'', QLatin1String( "\\'" ) );
1043  retval.replace( '%', QLatin1String( "\\%" ) );
1044  return retval;
1045 }
1046 
1047 void QgsProjectionSelectionTreeWidget::showDBMissingWarning( const QString &fileName )
1048 {
1049 
1050  QMessageBox::critical( this, tr( "Resource Location Error" ),
1051  tr( "Error reading database file from: \n %1\n"
1052  "Because of this the projection selector will not work…" )
1053  .arg( fileName ) );
1054 }
static QIcon getThemeIcon(const QString &name, const QColor &fillColor=QColor(), const QColor &strokeColor=QColor())
Helper to get a theme icon.
static QgsCoordinateReferenceSystemRegistry * coordinateReferenceSystemRegistry()
Returns the application's coordinate reference system (CRS) registry, which handles known CRS definit...
static QString qgisUserDatabaseFilePath()
Returns the path to the user qgis.db file.
static QString srsDatabaseFilePath()
Returns the path to the srs.db file.
QList< QgsCoordinateReferenceSystemRegistry::UserCrsDetails > userCrsList() const
Returns a list containing the details of all registered custom (user-defined) CRSes.
This class represents a coordinate reference system (CRS).
static QgsCoordinateReferenceSystem fromOgcWmsCrs(const QString &ogcCrs)
Creates a CRS from a given OGC WMS-format Coordinate Reference System string.
bool isValid() const
Returns whether this CRS is correctly initialized and usable.
QgsRectangle bounds() const
Returns the approximate bounds for the region the CRS is usable within.
QString toProj() const
Returns a Proj string representation of this CRS.
static void pushRecentCoordinateReferenceSystem(const QgsCoordinateReferenceSystem &crs)
Pushes a recently used CRS to the top of the recent CRS list.
QString description() const
Returns the descriptive name of the CRS, e.g., "WGS 84" or "GDA 94 / Vicgrid94".
QgsDatumEnsemble datumEnsemble() const SIP_THROW(QgsNotSupportedException)
Attempts to retrieve datum ensemble details from the CRS.
bool isDynamic() const
Returns true if the CRS is a dynamic CRS.
static QgsCoordinateReferenceSystem fromProj(const QString &proj)
Creates a CRS from a proj style formatted string.
static QList< QgsCoordinateReferenceSystem > recentCoordinateReferenceSystems()
Returns a list of recently used CRS.
QString authid() const
Returns the authority identifier for the CRS.
@ WKT_PREFERRED
Preferred format, matching the most recent WKT ISO standard. Currently an alias to WKT2_2019,...
static QgsCoordinateReferenceSystem fromWkt(const QString &wkt)
Creates a CRS from a WKT spatial ref sys definition string.
QString toWkt(WktVariant variant=WKT1_GDAL, bool multiline=false, int indentationWidth=4) const
Returns a WKT representation of this CRS.
QgsProjOperation operation() const
Returns information about the PROJ operation associated with the coordinate reference system,...
Q_GADGET QgsUnitTypes::DistanceUnit mapUnits
long srsid() const
Returns the internal CRS ID, if available.
QString celestialBodyName() const SIP_THROW(QgsNotSupportedException)
Attempts to retrieve the name of the celestial body associated with the CRS (e.g.
Contains information about a datum ensemble.
Definition: qgsdatums.h:95
QString code() const
Identification code, e.g.
Definition: qgsdatums.h:122
QString authority() const
Authority name, e.g.
Definition: qgsdatums.h:117
bool isValid() const
Returns true if the datum ensemble is a valid object, or false if it is a null/invalid object.
Definition: qgsdatums.h:102
QString name() const
Display name of datum ensemble.
Definition: qgsdatums.h:107
double accuracy() const
Positional accuracy (in meters).
Definition: qgsdatums.h:112
static void logMessage(const QString &message, const QString &tag=QString(), Qgis::MessageLevel level=Qgis::MessageLevel::Warning, bool notifyUser=true)
Adds a message to the log instance (and creates it if necessary).
Custom exception class which is raised when an operation is not supported.
Definition: qgsexception.h:118
Contains information about a PROJ operation.
QString description() const
Description.
void resizeEvent(QResizeEvent *event) override
void setPreviewRect(const QgsRectangle &rect)
Sets the initial "preview" rectangle for the bounds overview map.
void setShowBoundsMap(bool show)
Sets whether to show the bounds preview map.
void crsSelected()
Emitted when a projection is selected in the widget.
Q_DECL_DEPRECATED void pushProjectionToFront()
Marks the current selected projection for push to front of recent projections list.
QgsCoordinateReferenceSystem crs() const
Returns the CRS currently selected in the widget.
bool showNoProjection() const
Returns whether the "no/invalid" projection option is shown.
void setShowNoProjection(bool show)
Sets whether a "no/invalid" projection option should be shown.
bool showBoundsMap() const
Returns whether the bounds preview map is shown.
QgsRectangle previewRect() const
The initial "preview" rectangle for the bounds overview map.
QgsProjectionSelectionTreeWidget(QWidget *parent=nullptr)
Constructor for QgsProjectionSelectionTreeWidget.
void projectionDoubleClicked()
Emitted when a projection is double clicked in the list.
bool hasValidSelection() const
Returns true if the current selection in the widget is a valid choice.
void setOgcWmsCrsFilter(const QSet< QString > &crsFilter)
filters this widget by the given CRSs
void setNotSetText(const QString &text)
Sets the text to show for the not set option.
void initialized()
Notifies others that the widget is now fully initialized, including deferred selection of projection.
void hasValidSelectionChanged(bool isValid)
Emitted when the selection in the tree is changed from a valid selection to an invalid selection,...
void setCrs(const QgsCoordinateReferenceSystem &crs)
Sets the initial crs to show within the dialog.
A rectangle specified with double values.
Definition: qgsrectangle.h:42
double yMaximum() const SIP_HOLDGIL
Returns the y maximum value (top side of rectangle).
Definition: qgsrectangle.h:193
double xMaximum() const SIP_HOLDGIL
Returns the x maximum value (right side of rectangle).
Definition: qgsrectangle.h:183
double xMinimum() const SIP_HOLDGIL
Returns the x minimum value (left side of rectangle).
Definition: qgsrectangle.h:188
double yMinimum() const SIP_HOLDGIL
Returns the y minimum value (bottom side of rectangle).
Definition: qgsrectangle.h:198
double area() const SIP_HOLDGIL
Returns the area of the rectangle.
Definition: qgsrectangle.h:239
This class is a composition of two QSettings instances:
Definition: qgssettings.h:62
void setValue(const QString &key, const QVariant &value, QgsSettings::Section section=QgsSettings::NoSection)
Sets the value of setting key to value.
static QString qRegExpEscape(const QString &string)
Returns an escaped string matching the behavior of QRegExp::escape.
static Q_INVOKABLE QString toString(QgsUnitTypes::DistanceUnit unit)
Returns a translated string representing a distance unit.
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference)
Definition: qgis.h:1246
const int USER_CRS_START_ID
Magick number that determines whether a projection crsid is a system (srs.db) or user (~/....
Definition: qgis.h:1690
struct sqlite3 sqlite3
#define QgsDebugMsgLevel(str, level)
Definition: qgslogger.h:39
#define QgsDebugMsg(str)
Definition: qgslogger.h:38
const QgsCoordinateReferenceSystem & crs