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