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