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
18 #include "qgscoordinatereferencesystemregistry.h"
19 #include "qgscoordinatereferencesystem_p.h"
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
QgsCoordinateReferenceSystemRegistry(QObject * parent)35 QgsCoordinateReferenceSystemRegistry::QgsCoordinateReferenceSystemRegistry( QObject *parent )
36 : QObject( parent )
37 {
38
39 }
40
41 QgsCoordinateReferenceSystemRegistry::~QgsCoordinateReferenceSystemRegistry() = default;
42
userCrsList() const43 QList<QgsCoordinateReferenceSystemRegistry::UserCrsDetails> QgsCoordinateReferenceSystemRegistry::userCrsList() const
44 {
45 QList<QgsCoordinateReferenceSystemRegistry::UserCrsDetails> res;
46
47 //Setup connection to the existing custom CRS database:
48 sqlite3_database_unique_ptr 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 {
62 const QgsCoordinateReferenceSystem crs;
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
addUserCrs(const QgsCoordinateReferenceSystem & crs,const QString & name,QgsCoordinateReferenceSystem::Format nativeFormat)82 long QgsCoordinateReferenceSystemRegistry::addUserCrs( const QgsCoordinateReferenceSystem &crs, const QString &name, QgsCoordinateReferenceSystem::Format nativeFormat )
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 ("
122 + QgsSqliteUtils::quotedString( name )
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 }
130 sqlite3_database_unique_ptr database;
131 sqlite3_statement_unique_ptr statement;
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" )
137 .arg( QgsApplication::qgisUserDatabaseFilePath(),
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
162 QgsCoordinateReferenceSystem::invalidateCache();
163 QgsCoordinateTransform::invalidateCache();
164
165 if ( returnId != -1 )
166 {
167 emit userCrsAdded( crs.d->mAuthId );
168 emit crsDefinitionsChanged();
169 }
170
171 return returnId;
172 }
173
updateUserCrs(long id,const QgsCoordinateReferenceSystem & crs,const QString & name,QgsCoordinateReferenceSystem::Format nativeFormat)174 bool QgsCoordinateReferenceSystemRegistry::updateUserCrs( long id, const QgsCoordinateReferenceSystem &crs, const QString &name, QgsCoordinateReferenceSystem::Format nativeFormat )
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="
183 + QgsSqliteUtils::quotedString( name )
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
192 sqlite3_database_unique_ptr database;
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" )
198 .arg( QgsApplication::qgisUserDatabaseFilePath(),
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
232 QgsCoordinateReferenceSystem::invalidateCache();
233 QgsCoordinateTransform::invalidateCache();
234
235 if ( res )
236 {
237 emit userCrsChanged( crs.d->mAuthId );
238 emit crsDefinitionsChanged();
239 }
240
241 return res;
242 }
243
removeUserCrs(long id)244 bool QgsCoordinateReferenceSystemRegistry::removeUserCrs( long id )
245 {
246 sqlite3_database_unique_ptr database;
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(),
255 QgsApplication::qgisUserDatabaseFilePath() ) );
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
282 QgsCoordinateReferenceSystem::invalidateCache();
283 QgsCoordinateTransform::invalidateCache();
284
285 if ( res )
286 {
287 emit userCrsRemoved( id );
288 emit crsDefinitionsChanged();
289 }
290
291 return res;
292 }
293
294
insertProjection(const QString & projectionAcronym)295 bool QgsCoordinateReferenceSystemRegistry::insertProjection( const QString &projectionAcronym )
296 {
297 sqlite3_database_unique_ptr database;
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(),
305 QgsApplication::qgisUserDatabaseFilePath() ) );
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
projOperations() const349 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
celestialBodies() const377 QList< QgsCelestialBody> QgsCoordinateReferenceSystemRegistry::celestialBodies() const
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 }
412