QGIS API Documentation  3.23.0-Master (dd0cd13a00)
qgscoordinatereferencesystemregistry.cpp
Go to the documentation of this file.
1 /***************************************************************************
2  qgscoordinatereferencesystemregistry.cpp
3  -------------------
4  begin : January 2021
5  copyright : (C) 2021 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 
20 #include "qgscoordinatetransform.h"
21 #include "qgsapplication.h"
22 #include "qgslogger.h"
23 #include "qgsmessagelog.h"
24 #include "qgssqliteutils.h"
25 #include "qgscelestialbody.h"
26 #include "qgsprojutils.h"
27 #include "qgsruntimeprofiler.h"
28 #include "qgsexception.h"
29 #include "qgsprojoperation.h"
30 
31 #include <sqlite3.h>
32 #include <mutex>
33 #include <proj.h>
34 
36  : QObject( parent )
37 {
38 
39 }
40 
42 
43 QList<QgsCoordinateReferenceSystemRegistry::UserCrsDetails> QgsCoordinateReferenceSystemRegistry::userCrsList() const
44 {
45  QList<QgsCoordinateReferenceSystemRegistry::UserCrsDetails> res;
46 
47  //Setup connection to the existing custom CRS database:
49  //check the db is available
50  int result = database.open_v2( QgsApplication::qgisUserDatabaseFilePath(), SQLITE_OPEN_READONLY, nullptr );
51  if ( result != SQLITE_OK )
52  {
53  QgsDebugMsg( QStringLiteral( "Can't open database: %1" ).arg( database.errorMessage() ) );
54  return res;
55  }
56 
57  const QString sql = QStringLiteral( "select srs_id,description,parameters, wkt from tbl_srs" );
58  QgsDebugMsgLevel( QStringLiteral( "Query to populate existing list:%1" ).arg( sql ), 4 );
59  sqlite3_statement_unique_ptr preparedStatement = database.prepare( sql, result );
60  if ( result == SQLITE_OK )
61  {
63  while ( preparedStatement.step() == SQLITE_ROW )
64  {
65  UserCrsDetails details;
66  details.id = preparedStatement.columnAsText( 0 ).toLong();
67  details.name = preparedStatement.columnAsText( 1 );
68  details.proj = preparedStatement.columnAsText( 2 );
69  details.wkt = preparedStatement.columnAsText( 3 );
70 
71  if ( !details.wkt.isEmpty() )
72  details.crs.createFromWkt( details.wkt );
73  else
74  details.crs.createFromProj( details.proj );
75 
76  res << details;
77  }
78  }
79  return res;
80 }
81 
83 {
84  if ( !crs.isValid() )
85  {
86  QgsDebugMsgLevel( QStringLiteral( "Can't save an invalid CRS!" ), 4 );
87  return -1;
88  }
89 
90  QString mySql;
91 
92  QString proj4String = crs.d->mProj4;
93  if ( proj4String.isEmpty() )
94  {
95  proj4String = crs.toProj();
96  }
97  const QString wktString = crs.toWkt( QgsCoordinateReferenceSystem::WKT_PREFERRED );
98 
99  // ellipsoid acroynym column is incorrectly marked as not null in many crs database instances,
100  // hack around this by using an empty string instead
101  const QString quotedEllipsoidString = crs.ellipsoidAcronym().isNull() ? QStringLiteral( "''" ) : QgsSqliteUtils::quotedString( crs.ellipsoidAcronym() );
102 
103  //if this is the first record we need to ensure that its srs_id is 10000. For
104  //any rec after that sqlite3 will take care of the autonumbering
105  //this was done to support sqlite 3.0 as it does not yet support
106  //the autoinc related system tables.
107  if ( QgsCoordinateReferenceSystem::getRecordCount() == 0 )
108  {
109  mySql = "insert into tbl_srs (srs_id,description,projection_acronym,ellipsoid_acronym,parameters,is_geo,wkt) values ("
110  + QString::number( USER_CRS_START_ID )
111  + ',' + QgsSqliteUtils::quotedString( name )
112  + ',' + ( !crs.d->mProjectionAcronym.isEmpty() ? QgsSqliteUtils::quotedString( crs.d->mProjectionAcronym ) : QStringLiteral( "''" ) )
113  + ',' + quotedEllipsoidString
114  + ',' + ( !proj4String.isEmpty() ? QgsSqliteUtils::quotedString( proj4String ) : QStringLiteral( "''" ) )
115  + ",0," // <-- is_geo shamelessly hard coded for now
116  + ( nativeFormat == QgsCoordinateReferenceSystem::FormatWkt ? QgsSqliteUtils::quotedString( wktString ) : QStringLiteral( "''" ) )
117  + ')';
118  }
119  else
120  {
121  mySql = "insert into tbl_srs (description,projection_acronym,ellipsoid_acronym,parameters,is_geo,wkt) values ("
123  + ',' + ( !crs.d->mProjectionAcronym.isEmpty() ? QgsSqliteUtils::quotedString( crs.d->mProjectionAcronym ) : QStringLiteral( "''" ) )
124  + ',' + quotedEllipsoidString
125  + ',' + ( !proj4String.isEmpty() ? QgsSqliteUtils::quotedString( proj4String ) : QStringLiteral( "''" ) )
126  + ",0," // <-- is_geo shamelessly hard coded for now
127  + ( nativeFormat == QgsCoordinateReferenceSystem::FormatWkt ? QgsSqliteUtils::quotedString( wktString ) : QStringLiteral( "''" ) )
128  + ')';
129  }
132  //check the db is available
133  int myResult = database.open( QgsApplication::qgisUserDatabaseFilePath() );
134  if ( myResult != SQLITE_OK )
135  {
136  QgsDebugMsg( QStringLiteral( "Can't open or create database %1: %2" )
138  database.errorMessage() ) );
139  return false;
140  }
141  statement = database.prepare( mySql, myResult );
142 
143  qint64 returnId = -1;
144  if ( myResult == SQLITE_OK && statement.step() == SQLITE_DONE )
145  {
146  QgsMessageLog::logMessage( QObject::tr( "Saved user CRS [%1]" ).arg( crs.toProj() ), QObject::tr( "CRS" ) );
147 
148  returnId = sqlite3_last_insert_rowid( database.get() );
149  crs.d->mSrsId = returnId;
150  crs.d->mAuthId = QStringLiteral( "USER:%1" ).arg( returnId );
151  crs.d->mDescription = name;
152  }
153 
154  if ( returnId != -1 )
155  {
156  // If we have a projection acronym not in the user db previously, add it.
157  // This is a must, or else we can't select it from the vw_srs table.
158  // Actually, add it always and let the SQL PRIMARY KEY remove duplicates.
159  insertProjection( crs.projectionAcronym() );
160  }
161 
164 
165  if ( returnId != -1 )
166  {
167  emit userCrsAdded( crs.d->mAuthId );
168  emit crsDefinitionsChanged();
169  }
170 
171  return returnId;
172 }
173 
175 {
176  if ( !crs.isValid() )
177  {
178  QgsDebugMsgLevel( QStringLiteral( "Can't save an invalid CRS!" ), 4 );
179  return false;
180  }
181 
182  const QString sql = "update tbl_srs set description="
184  + ",projection_acronym=" + ( !crs.projectionAcronym().isEmpty() ? QgsSqliteUtils::quotedString( crs.projectionAcronym() ) : QStringLiteral( "''" ) )
185  + ",ellipsoid_acronym=" + ( !crs.ellipsoidAcronym().isEmpty() ? QgsSqliteUtils::quotedString( crs.ellipsoidAcronym() ) : QStringLiteral( "''" ) )
186  + ",parameters=" + ( !crs.toProj().isEmpty() ? QgsSqliteUtils::quotedString( crs.toProj() ) : QStringLiteral( "''" ) )
187  + ",is_geo=0" // <--shamelessly hard coded for now
188  + ",wkt=" + ( nativeFormat == QgsCoordinateReferenceSystem::FormatWkt ? QgsSqliteUtils::quotedString( crs.toWkt( QgsCoordinateReferenceSystem::WKT_PREFERRED, false ) ) : QStringLiteral( "''" ) )
189  + " where srs_id=" + QgsSqliteUtils::quotedString( QString::number( id ) )
190  ;
191 
193  //check the db is available
194  const int myResult = database.open( QgsApplication::qgisUserDatabaseFilePath() );
195  if ( myResult != SQLITE_OK )
196  {
197  QgsDebugMsg( QStringLiteral( "Can't open or create database %1: %2" )
199  database.errorMessage() ) );
200  return false;
201  }
202 
203  bool res = true;
204  QString errorMessage;
205  if ( database.exec( sql, errorMessage ) != SQLITE_OK )
206  {
207  QgsMessageLog::logMessage( QObject::tr( "Error saving user CRS [%1]: %2" ).arg( crs.toProj(), errorMessage ), QObject::tr( "CRS" ) );
208  res = false;
209  }
210  else
211  {
212  const int changed = sqlite3_changes( database.get() );
213  if ( changed )
214  {
215  QgsMessageLog::logMessage( QObject::tr( "Saved user CRS [%1]" ).arg( crs.toProj() ), QObject::tr( "CRS" ) );
216  }
217  else
218  {
219  QgsMessageLog::logMessage( QObject::tr( "Error saving user CRS [%1]: No matching ID found in database" ).arg( crs.toProj() ), QObject::tr( "CRS" ) );
220  res = false;
221  }
222  }
223 
224  if ( res )
225  {
226  // If we have a projection acronym not in the user db previously, add it.
227  // This is a must, or else we can't select it from the vw_srs table.
228  // Actually, add it always and let the SQL PRIMARY KEY remove duplicates.
229  insertProjection( crs.projectionAcronym() );
230  }
231 
234 
235  if ( res )
236  {
237  emit userCrsChanged( crs.d->mAuthId );
238  emit crsDefinitionsChanged();
239  }
240 
241  return res;
242 }
243 
245 {
247 
248  const QString sql = "delete from tbl_srs where srs_id=" + QgsSqliteUtils::quotedString( QString::number( id ) );
249  QgsDebugMsgLevel( sql, 4 );
250  //check the db is available
251  int result = database.open( QgsApplication::qgisUserDatabaseFilePath() );
252  if ( result != SQLITE_OK )
253  {
254  QgsDebugMsg( QStringLiteral( "Can't open database: %1 \n please notify QGIS developers of this error \n %2 (file name) " ).arg( database.errorMessage(),
256  return false;
257  }
258 
259  bool res = true;
260  {
261  sqlite3_statement_unique_ptr preparedStatement = database.prepare( sql, result );
262  if ( result != SQLITE_OK || preparedStatement.step() != SQLITE_DONE )
263  {
264  QgsDebugMsg( QStringLiteral( "failed to remove custom CRS from database: %1 [%2]" ).arg( sql, database.errorMessage() ) );
265  res = false;
266  }
267  else
268  {
269  const int changed = sqlite3_changes( database.get() );
270  if ( changed )
271  {
272  QgsMessageLog::logMessage( QObject::tr( "Removed user CRS [%1]" ).arg( id ), QObject::tr( "CRS" ) );
273  }
274  else
275  {
276  QgsMessageLog::logMessage( QObject::tr( "Error removing user CRS [%1]: No matching ID found in database" ).arg( id ), QObject::tr( "CRS" ) );
277  res = false;
278  }
279  }
280  }
281 
284 
285  if ( res )
286  {
287  emit userCrsRemoved( id );
288  emit crsDefinitionsChanged();
289  }
290 
291  return res;
292 }
293 
294 
295 bool QgsCoordinateReferenceSystemRegistry::insertProjection( const QString &projectionAcronym )
296 {
298  sqlite3_database_unique_ptr srsDatabase;
299  QString sql;
300  //check the db is available
301  int result = database.open( QgsApplication::qgisUserDatabaseFilePath() );
302  if ( result != SQLITE_OK )
303  {
304  QgsDebugMsg( QStringLiteral( "Can't open database: %1 \n please notify QGIS developers of this error \n %2 (file name) " ).arg( database.errorMessage(),
306  return false;
307  }
308  int srsResult = srsDatabase.open( QgsApplication::srsDatabaseFilePath() );
309  if ( result != SQLITE_OK )
310  {
311  QgsDebugMsg( QStringLiteral( "Can't open database %1 [%2]" ).arg( QgsApplication::srsDatabaseFilePath(),
312  srsDatabase.errorMessage() ) );
313  return false;
314  }
315 
316  // Set up the query to retrieve the projection information needed to populate the PROJECTION list
317  const QString srsSql = "select acronym,name,notes,parameters from tbl_projection where acronym=" + QgsSqliteUtils::quotedString( projectionAcronym );
318 
319  sqlite3_statement_unique_ptr srsPreparedStatement = srsDatabase.prepare( srsSql, srsResult );
320  if ( srsResult == SQLITE_OK )
321  {
322  if ( srsPreparedStatement.step() == SQLITE_ROW )
323  {
324  QgsDebugMsgLevel( QStringLiteral( "Trying to insert projection" ), 4 );
325  // We have the result from system srs.db. Now insert into user db.
326  sql = "insert into tbl_projection(acronym,name,notes,parameters) values ("
327  + QgsSqliteUtils::quotedString( srsPreparedStatement.columnAsText( 0 ) )
328  + ',' + QgsSqliteUtils::quotedString( srsPreparedStatement.columnAsText( 1 ) )
329  + ',' + QgsSqliteUtils::quotedString( srsPreparedStatement.columnAsText( 2 ) )
330  + ',' + QgsSqliteUtils::quotedString( srsPreparedStatement.columnAsText( 3 ) )
331  + ')';
332  sqlite3_statement_unique_ptr preparedStatement = database.prepare( sql, result );
333  if ( result != SQLITE_OK || preparedStatement.step() != SQLITE_DONE )
334  {
335  QgsDebugMsg( QStringLiteral( "Could not insert projection into database: %1 [%2]" ).arg( sql, database.errorMessage() ) );
336  return false;
337  }
338  }
339  }
340  else
341  {
342  QgsDebugMsg( QStringLiteral( "prepare failed: %1 [%2]" ).arg( srsSql, srsDatabase.errorMessage() ) );
343  return false;
344  }
345 
346  return true;
347 }
348 
349 QMap<QString, QgsProjOperation> QgsCoordinateReferenceSystemRegistry::projOperations() const
350 {
351  static std::once_flag initialized;
352  std::call_once( initialized, [ = ]
353  {
354  const QgsScopedRuntimeProfile profile( QObject::tr( "Initialize PROJ operations" ) );
355 
356  const PJ_OPERATIONS *operation = proj_list_operations();
357  while ( operation && operation->id )
358  {
359  QgsProjOperation value;
360  value.mValid = true;
361  value.mId = QString( operation->id );
362 
363  const QString description( *operation->descr );
364  const QStringList descriptionParts = description.split( QStringLiteral( "\n\t" ) );
365  value.mDescription = descriptionParts.value( 0 );
366  value.mDetails = descriptionParts.mid( 1 ).join( '\n' );
367 
368  mProjOperations.insert( value.id(), value );
369 
370  operation++;
371  }
372  } );
373 
374  return mProjOperations;
375 }
376 
378 {
379 #if PROJ_VERSION_MAJOR>8 || (PROJ_VERSION_MAJOR==8 && PROJ_VERSION_MINOR>=1)
380 
381  static std::once_flag initialized;
382  std::call_once( initialized, [ = ]
383  {
384  QgsScopedRuntimeProfile profile( QObject::tr( "Initialize celestial bodies" ) );
385 
386  PJ_CONTEXT *context = QgsProjContext::get();
387 
388  int resultCount = 0;
389  PROJ_CELESTIAL_BODY_INFO **list = proj_get_celestial_body_list_from_database( context, nullptr, &resultCount );
390  mCelestialBodies.reserve( resultCount );
391  for ( int i = 0; i < resultCount; i++ )
392  {
393  const PROJ_CELESTIAL_BODY_INFO *info = list[ i ];
394  if ( !info )
395  break;
396 
397  QgsCelestialBody body;
398  body.mValid = true;
399  body.mAuthority = QString( info->auth_name );
400  body.mName = QString( info->name );
401 
402  mCelestialBodies << body;
403  }
404  proj_celestial_body_list_destroy( list );
405  } );
406 
407  return mCelestialBodies;
408 #else
409  throw QgsNotSupportedException( QStringLiteral( "Retrieving celestial bodies requires a QGIS build based on PROJ 8.1 or later" ) );
410 #endif
411 }
static QString qgisUserDatabaseFilePath()
Returns the path to the user qgis.db file.
static QString srsDatabaseFilePath()
Returns the path to the srs.db file.
Contains information about a celestial body.
QgsCoordinateReferenceSystem crs
QgsCoordinateReferenceSystem object representing the user-defined CRS.
void userCrsAdded(const QString &id)
Emitted whenever a new user CRS definition is added.
void userCrsChanged(const QString &id)
Emitted whenever an existing user CRS definition is changed.
QList< QgsCelestialBody > celestialBodies() const
Returns a list of all known celestial bodies.
void userCrsRemoved(long id)
Emitted when the user CRS with matching id is removed from the database.
void crsDefinitionsChanged()
Emitted whenever an operation has caused any of the known CRS definitions (including user-defined CRS...
bool updateUserCrs(long id, const QgsCoordinateReferenceSystem &crs, const QString &name, QgsCoordinateReferenceSystem::Format nativeFormat=QgsCoordinateReferenceSystem::FormatWkt)
Updates the definition of the existing user CRS with matching id.
QgsCoordinateReferenceSystemRegistry(QObject *parent=nullptr)
Constructor for QgsCoordinateReferenceSystemRegistry, with the specified parent object.
bool removeUserCrs(long id)
Removes the existing user CRS with matching id.
QList< QgsCoordinateReferenceSystemRegistry::UserCrsDetails > userCrsList() const
Returns a list containing the details of all registered custom (user-defined) CRSes.
long addUserCrs(const QgsCoordinateReferenceSystem &crs, const QString &name, QgsCoordinateReferenceSystem::Format nativeFormat=QgsCoordinateReferenceSystem::FormatWkt)
Adds a new crs definition as a custom ("USER") CRS.
QMap< QString, QgsProjOperation > projOperations() const
Returns a map of all valid PROJ operations.
This class represents a coordinate reference system (CRS).
bool isValid() const
Returns whether this CRS is correctly initialized and usable.
bool createFromWkt(const QString &wkt)
Sets this CRS using a WKT definition.
QString toProj() const
Returns a Proj string representation of this CRS.
QString ellipsoidAcronym() const
Returns the ellipsoid acronym for the ellipsoid used by the CRS.
QString projectionAcronym() const
Returns the projection acronym for the projection used by the CRS.
bool createFromProj(const QString &projString, bool identify=true)
Sets this CRS by passing it a PROJ style formatted string.
Format
Projection definition formats.
@ FormatWkt
WKT format (always recommended over proj string format)
static void invalidateCache(bool disableCache=false)
Clears the internal cache used to initialize QgsCoordinateReferenceSystem objects.
@ WKT_PREFERRED
Preferred format, matching the most recent WKT ISO standard. Currently an alias to WKT2_2019,...
QString toWkt(WktVariant variant=WKT1_GDAL, bool multiline=false, int indentationWidth=4) const
Returns a WKT representation of this CRS.
static void invalidateCache(bool disableCache=false)
Clears the internal cache used to initialize QgsCoordinateTransform objects.
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
static PJ_CONTEXT * get()
Returns a thread local instance of a proj context, safe for use in the current thread.
Contains information about a PROJ operation.
QString id() const
ID of operation.
Scoped object for logging of the runtime for a single operation or group of operations.
static QString quotedString(const QString &value)
Returns a quoted string value, surround by ' characters and with special characters correctly escaped...
Unique pointer for sqlite3 databases, which automatically closes the database when the pointer goes o...
sqlite3_statement_unique_ptr prepare(const QString &sql, int &resultCode) const
Prepares a sql statement, returning the result.
int open(const QString &path)
Opens the database at the specified file path.
QString errorMessage() const
Returns the most recent error message encountered by the database.
int open_v2(const QString &path, int flags, const char *zVfs)
Opens the database at the specified file path.
int exec(const QString &sql, QString &errorMessage) const
Executes the sql command in the database.
Unique pointer for sqlite3 prepared statements, which automatically finalizes the statement when the ...
QString columnAsText(int column) const
Returns the column value from the current statement row as a string.
int step()
Steps to the next record in the statement, returning the sqlite3 result code.
const int USER_CRS_START_ID
Magick number that determines whether a projection crsid is a system (srs.db) or user (~/....
Definition: qgis.h:1948
struct projCtx_t PJ_CONTEXT
#define QgsDebugMsgLevel(str, level)
Definition: qgslogger.h:39
#define QgsDebugMsg(str)
Definition: qgslogger.h:38
const QgsCoordinateReferenceSystem & crs