QGIS API Documentation 3.28.0-Firenze (ed3ad0430f)
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
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
43QList<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 == Qgis::CrsDefinitionFormat::Wkt ? 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 == Qgis::CrsDefinitionFormat::Wkt ? 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 );
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 == Qgis::CrsDefinitionFormat::Wkt ? 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 );
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 );
289 }
290
291 return res;
292}
293
294
295bool 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
349QMap<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( QObject::tr( "Retrieving celestial bodies requires a QGIS build based on PROJ 8.1 or later" ) );
410#endif
411}
CrsDefinitionFormat
CRS definition formats.
Definition: qgis.h:1857
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, Qgis::CrsDefinitionFormat nativeFormat=Qgis::CrsDefinitionFormat::Wkt)
Updates the definition of the existing user CRS with matching id.
QgsCoordinateReferenceSystemRegistry(QObject *parent=nullptr)
Constructor for QgsCoordinateReferenceSystemRegistry, with the specified parent object.
long addUserCrs(const QgsCoordinateReferenceSystem &crs, const QString &name, Qgis::CrsDefinitionFormat nativeFormat=Qgis::CrsDefinitionFormat::Wkt)
Adds a new crs definition as a custom ("USER") CRS.
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.
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.
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:3009
struct projCtx_t PJ_CONTEXT
#define QgsDebugMsgLevel(str, level)
Definition: qgslogger.h:39
#define QgsDebugMsg(str)
Definition: qgslogger.h:38
const QgsCoordinateReferenceSystem & crs