1 /***************************************************************************
2                           qgscoordinatereferencesystem.cpp
3 
4                              -------------------
5     begin                : 2007
6     copyright            : (C) 2007 by Gary E. Sherman
7     email                : sherman@mrcc.com
8 ***************************************************************************/
9 
10 /***************************************************************************
11  *                                                                         *
12  *   This program is free software; you can redistribute it and/or modify  *
13  *   it under the terms of the GNU General Public License as published by  *
14  *   the Free Software Foundation; either version 2 of the License, or     *
15  *   (at your option) any later version.                                   *
16  *                                                                         *
17  ***************************************************************************/
18 #include "qgscoordinatereferencesystem.h"
19 #include "qgscoordinatereferencesystem_p.h"
20 
21 #include "qgscoordinatereferencesystem_legacy.h"
22 #include "qgscoordinatereferencesystemregistry.h"
23 #include "qgsreadwritelocker.h"
24 
25 #include <cmath>
26 
27 #include <QDir>
28 #include <QDomNode>
29 #include <QDomElement>
30 #include <QFileInfo>
31 #include <QRegularExpression>
32 #include <QTextStream>
33 #include <QFile>
34 
35 #include "qgsapplication.h"
36 #include "qgslogger.h"
37 #include "qgsmessagelog.h"
38 #include "qgis.h" //const vals declared here
39 #include "qgslocalec.h"
40 #include "qgssettings.h"
41 #include "qgsogrutils.h"
42 #include "qgsdatums.h"
43 #include "qgsprojectionfactors.h"
44 #include "qgsprojoperation.h"
45 
46 #include <sqlite3.h>
47 #include "qgsprojutils.h"
48 #include <proj.h>
49 #include <proj_experimental.h>
50 
51 //gdal and ogr includes (needed for == operator)
52 #include <ogr_srs_api.h>
53 #include <cpl_error.h>
54 #include <cpl_conv.h>
55 #include <cpl_csv.h>
56 
57 CUSTOM_CRS_VALIDATION QgsCoordinateReferenceSystem::sCustomSrsValidation = nullptr;
58 
59 typedef QHash< long, QgsCoordinateReferenceSystem > SrIdCrsCacheHash;
60 typedef QHash< QString, QgsCoordinateReferenceSystem > StringCrsCacheHash;
61 
62 Q_GLOBAL_STATIC( QReadWriteLock, sSrIdCacheLock )
63 Q_GLOBAL_STATIC( SrIdCrsCacheHash, sSrIdCache )
64 bool QgsCoordinateReferenceSystem::sDisableSrIdCache = false;
65 
66 Q_GLOBAL_STATIC( QReadWriteLock, sOgcLock )
67 Q_GLOBAL_STATIC( StringCrsCacheHash, sOgcCache )
68 bool QgsCoordinateReferenceSystem::sDisableOgcCache = false;
69 
70 Q_GLOBAL_STATIC( QReadWriteLock, sProj4CacheLock )
71 Q_GLOBAL_STATIC( StringCrsCacheHash, sProj4Cache )
72 bool QgsCoordinateReferenceSystem::sDisableProjCache = false;
73 
74 Q_GLOBAL_STATIC( QReadWriteLock, sCRSWktLock )
75 Q_GLOBAL_STATIC( StringCrsCacheHash, sWktCache )
76 bool QgsCoordinateReferenceSystem::sDisableWktCache = false;
77 
78 Q_GLOBAL_STATIC( QReadWriteLock, sCRSSrsIdLock )
79 Q_GLOBAL_STATIC( SrIdCrsCacheHash, sSrsIdCache )
80 bool QgsCoordinateReferenceSystem::sDisableSrsIdCache = false;
81 
82 Q_GLOBAL_STATIC( QReadWriteLock, sCrsStringLock )
83 Q_GLOBAL_STATIC( StringCrsCacheHash, sStringCache )
84 bool QgsCoordinateReferenceSystem::sDisableStringCache = false;
85 
getFullProjString(PJ * obj)86 QString getFullProjString( PJ *obj )
87 {
88   // see https://lists.osgeo.org/pipermail/proj/2019-May/008565.html, it's not sufficient to just
89   // use proj_as_proj_string
90   QgsProjUtils::proj_pj_unique_ptr boundCrs( proj_crs_create_bound_crs_to_WGS84( QgsProjContext::get(), obj, nullptr ) );
91   if ( boundCrs )
92   {
93     if ( const char *proj4src = proj_as_proj_string( QgsProjContext::get(), boundCrs.get(), PJ_PROJ_4, nullptr ) )
94     {
95       return QString( proj4src );
96     }
97   }
98 
99   return QString( proj_as_proj_string( QgsProjContext::get(), obj, PJ_PROJ_4, nullptr ) );
100 }
101 //--------------------------
102 
QgsCoordinateReferenceSystem()103 QgsCoordinateReferenceSystem::QgsCoordinateReferenceSystem()
104 {
105   static QgsCoordinateReferenceSystem nullCrs = QgsCoordinateReferenceSystem( QString() );
106 
107   d = nullCrs.d;
108 }
109 
QgsCoordinateReferenceSystem(const QString & definition)110 QgsCoordinateReferenceSystem::QgsCoordinateReferenceSystem( const QString &definition )
111 {
112   d = new QgsCoordinateReferenceSystemPrivate();
113   createFromString( definition );
114 }
115 
QgsCoordinateReferenceSystem(const long id,CrsType type)116 QgsCoordinateReferenceSystem::QgsCoordinateReferenceSystem( const long id, CrsType type )
117 {
118   d = new QgsCoordinateReferenceSystemPrivate();
119   Q_NOWARN_DEPRECATED_PUSH
120   createFromId( id, type );
121   Q_NOWARN_DEPRECATED_POP
122 }
123 
QgsCoordinateReferenceSystem(const QgsCoordinateReferenceSystem & srs)124 QgsCoordinateReferenceSystem::QgsCoordinateReferenceSystem( const QgsCoordinateReferenceSystem &srs )  //NOLINT
125   : d( srs.d )
126   , mValidationHint( srs.mValidationHint )
127 {
128 }
129 
operator =(const QgsCoordinateReferenceSystem & srs)130 QgsCoordinateReferenceSystem &QgsCoordinateReferenceSystem::operator=( const QgsCoordinateReferenceSystem &srs )  //NOLINT
131 {
132   d = srs.d;
133   mValidationHint = srs.mValidationHint;
134   return *this;
135 }
136 
validSrsIds()137 QList<long> QgsCoordinateReferenceSystem::validSrsIds()
138 {
139   QList<long> results;
140   // check both standard & user defined projection databases
141   QStringList dbs = QStringList() <<  QgsApplication::srsDatabaseFilePath() << QgsApplication::qgisUserDatabaseFilePath();
142 
143   const auto constDbs = dbs;
144   for ( const QString &db : constDbs )
145   {
146     QFileInfo myInfo( db );
147     if ( !myInfo.exists() )
148     {
149       QgsDebugMsg( "failed : " + db + " does not exist!" );
150       continue;
151     }
152 
153     sqlite3_database_unique_ptr database;
154     sqlite3_statement_unique_ptr statement;
155 
156     //check the db is available
157     int result = openDatabase( db, database );
158     if ( result != SQLITE_OK )
159     {
160       QgsDebugMsg( "failed : " + db + " could not be opened!" );
161       continue;
162     }
163 
164     QString sql = QStringLiteral( "select srs_id from tbl_srs" );
165     int rc;
166     statement = database.prepare( sql, rc );
167     while ( true )
168     {
169       // this one is an infinitive loop, intended to fetch any row
170       int ret = statement.step();
171 
172       if ( ret == SQLITE_DONE )
173       {
174         // there are no more rows to fetch - we can stop looping
175         break;
176       }
177 
178       if ( ret == SQLITE_ROW )
179       {
180         results.append( statement.columnAsInt64( 0 ) );
181       }
182       else
183       {
184         QgsMessageLog::logMessage( QObject::tr( "SQLite error: %2\nSQL: %1" ).arg( sql, sqlite3_errmsg( database.get() ) ), QObject::tr( "SpatiaLite" ) );
185         break;
186       }
187     }
188   }
189   std::sort( results.begin(), results.end() );
190   return results;
191 }
192 
fromOgcWmsCrs(const QString & ogcCrs)193 QgsCoordinateReferenceSystem QgsCoordinateReferenceSystem::fromOgcWmsCrs( const QString &ogcCrs )
194 {
195   QgsCoordinateReferenceSystem crs;
196   crs.createFromOgcWmsCrs( ogcCrs );
197   return crs;
198 }
199 
fromEpsgId(long epsg)200 QgsCoordinateReferenceSystem QgsCoordinateReferenceSystem::fromEpsgId( long epsg )
201 {
202   QgsCoordinateReferenceSystem res = fromOgcWmsCrs( "EPSG:" + QString::number( epsg ) );
203   if ( res.isValid() )
204     return res;
205 
206   // pre proj6 builds allowed use of ESRI: codes here (e.g. 54030), so we need to keep compatibility
207   res = fromOgcWmsCrs( "ESRI:" + QString::number( epsg ) );
208   if ( res.isValid() )
209     return res;
210 
211   return QgsCoordinateReferenceSystem();
212 }
213 
fromProj4(const QString & proj4)214 QgsCoordinateReferenceSystem QgsCoordinateReferenceSystem::fromProj4( const QString &proj4 )
215 {
216   return fromProj( proj4 );
217 }
218 
fromProj(const QString & proj)219 QgsCoordinateReferenceSystem QgsCoordinateReferenceSystem::fromProj( const QString &proj )
220 {
221   QgsCoordinateReferenceSystem crs;
222   crs.createFromProj( proj );
223   return crs;
224 }
225 
fromWkt(const QString & wkt)226 QgsCoordinateReferenceSystem QgsCoordinateReferenceSystem::fromWkt( const QString &wkt )
227 {
228   QgsCoordinateReferenceSystem crs;
229   crs.createFromWkt( wkt );
230   return crs;
231 }
232 
fromSrsId(long srsId)233 QgsCoordinateReferenceSystem QgsCoordinateReferenceSystem::fromSrsId( long srsId )
234 {
235   QgsCoordinateReferenceSystem crs;
236   crs.createFromSrsId( srsId );
237   return crs;
238 }
239 
~QgsCoordinateReferenceSystem()240 QgsCoordinateReferenceSystem::~QgsCoordinateReferenceSystem() //NOLINT
241 {
242 }
243 
createFromId(const long id,CrsType type)244 bool QgsCoordinateReferenceSystem::createFromId( const long id, CrsType type )
245 {
246   bool result = false;
247   switch ( type )
248   {
249     case InternalCrsId:
250       result = createFromSrsId( id );
251       break;
252     case PostgisCrsId:
253       Q_NOWARN_DEPRECATED_PUSH
254       result = createFromSrid( id );
255       Q_NOWARN_DEPRECATED_POP
256       break;
257     case EpsgCrsId:
258       result = createFromOgcWmsCrs( QStringLiteral( "EPSG:%1" ).arg( id ) );
259       break;
260     default:
261       //THIS IS BAD...THIS PART OF CODE SHOULD NEVER BE REACHED...
262       QgsDebugMsg( QStringLiteral( "Unexpected case reached!" ) );
263   };
264   return result;
265 }
266 
createFromString(const QString & definition)267 bool QgsCoordinateReferenceSystem::createFromString( const QString &definition )
268 {
269   if ( definition.isEmpty() )
270     return false;
271 
272   QgsReadWriteLocker locker( *sCrsStringLock(), QgsReadWriteLocker::Read );
273   if ( !sDisableStringCache )
274   {
275     QHash< QString, QgsCoordinateReferenceSystem >::const_iterator crsIt = sStringCache()->constFind( definition );
276     if ( crsIt != sStringCache()->constEnd() )
277     {
278       // found a match in the cache
279       *this = crsIt.value();
280       return d->mIsValid;
281     }
282   }
283   locker.unlock();
284 
285   bool result = false;
286   const thread_local QRegularExpression reCrsId( QStringLiteral( "^(epsg|esri|osgeo|ignf|zangi|iau2000|postgis|internal|user)\\:(\\w+)$" ), QRegularExpression::CaseInsensitiveOption );
287   QRegularExpressionMatch match = reCrsId.match( definition );
288   if ( match.capturedStart() == 0 )
289   {
290     QString authName = match.captured( 1 ).toLower();
291     if ( authName == QLatin1String( "epsg" ) )
292     {
293       result = createFromOgcWmsCrs( definition );
294     }
295     else if ( authName == QLatin1String( "postgis" ) )
296     {
297       const long id = match.captured( 2 ).toLong();
298       Q_NOWARN_DEPRECATED_PUSH
299       result = createFromSrid( id );
300       Q_NOWARN_DEPRECATED_POP
301     }
302     else if ( authName == QLatin1String( "esri" ) || authName == QLatin1String( "osgeo" ) || authName == QLatin1String( "ignf" ) || authName == QLatin1String( "zangi" ) || authName == QLatin1String( "iau2000" ) )
303     {
304       result = createFromOgcWmsCrs( definition );
305     }
306     else
307     {
308       const long id = match.captured( 2 ).toLong();
309       Q_NOWARN_DEPRECATED_PUSH
310       result = createFromId( id, InternalCrsId );
311       Q_NOWARN_DEPRECATED_POP
312     }
313   }
314   else
315   {
316     const thread_local QRegularExpression reCrsStr( QStringLiteral( "^(?:(wkt|proj4|proj)\\:)?(.+)$" ), QRegularExpression::CaseInsensitiveOption );
317     match = reCrsStr.match( definition );
318     if ( match.capturedStart() == 0 )
319     {
320       if ( match.captured( 1 ).startsWith( QLatin1String( "proj" ), Qt::CaseInsensitive ) )
321       {
322         result = createFromProj( match.captured( 2 ) );
323       }
324       else
325       {
326         result = createFromWkt( match.captured( 2 ) );
327       }
328     }
329   }
330 
331   locker.changeMode( QgsReadWriteLocker::Write );
332   if ( !sDisableStringCache )
333     sStringCache()->insert( definition, *this );
334   return result;
335 }
336 
createFromUserInput(const QString & definition)337 bool QgsCoordinateReferenceSystem::createFromUserInput( const QString &definition )
338 {
339   if ( definition.isEmpty() )
340     return false;
341 
342   QString userWkt;
343   OGRSpatialReferenceH crs = OSRNewSpatialReference( nullptr );
344 
345   if ( OSRSetFromUserInput( crs, definition.toLocal8Bit().constData() ) == OGRERR_NONE )
346   {
347     userWkt = QgsOgrUtils::OGRSpatialReferenceToWkt( crs );
348     OSRDestroySpatialReference( crs );
349   }
350   //QgsDebugMsg( "definition: " + definition + " wkt = " + wkt );
351   return createFromWkt( userWkt );
352 }
353 
setupESRIWktFix()354 void QgsCoordinateReferenceSystem::setupESRIWktFix()
355 {
356   // make sure towgs84 parameter is loaded if gdal >= 1.9
357   // this requires setting GDAL_FIX_ESRI_WKT=GEOGCS (see qgis bug #5598 and gdal bug #4673)
358   const char *configOld = CPLGetConfigOption( "GDAL_FIX_ESRI_WKT", "" );
359   const char *configNew = "GEOGCS";
360   // only set if it was not set, to let user change the value if needed
361   if ( strcmp( configOld, "" ) == 0 )
362   {
363     CPLSetConfigOption( "GDAL_FIX_ESRI_WKT", configNew );
364     if ( strcmp( configNew, CPLGetConfigOption( "GDAL_FIX_ESRI_WKT", "" ) ) != 0 )
365       QgsLogger::warning( QStringLiteral( "GDAL_FIX_ESRI_WKT could not be set to %1 : %2" )
366                           .arg( configNew, CPLGetConfigOption( "GDAL_FIX_ESRI_WKT", "" ) ) );
367     QgsDebugMsgLevel( QStringLiteral( "set GDAL_FIX_ESRI_WKT : %1" ).arg( configNew ), 4 );
368   }
369   else
370   {
371     QgsDebugMsgLevel( QStringLiteral( "GDAL_FIX_ESRI_WKT was already set : %1" ).arg( configNew ), 4 );
372   }
373 }
374 
createFromOgcWmsCrs(const QString & crs)375 bool QgsCoordinateReferenceSystem::createFromOgcWmsCrs( const QString &crs )
376 {
377   if ( crs.isEmpty() )
378     return false;
379 
380   QgsReadWriteLocker locker( *sOgcLock(), QgsReadWriteLocker::Read );
381   if ( !sDisableOgcCache )
382   {
383     QHash< QString, QgsCoordinateReferenceSystem >::const_iterator crsIt = sOgcCache()->constFind( crs );
384     if ( crsIt != sOgcCache()->constEnd() )
385     {
386       // found a match in the cache
387       *this = crsIt.value();
388       return d->mIsValid;
389     }
390   }
391   locker.unlock();
392 
393   QString wmsCrs = crs;
394 
395   thread_local const QRegularExpression re_uri( QRegularExpression::anchoredPattern( QStringLiteral( "http://www\\.opengis\\.net/def/crs/([^/]+).+/([^/]+)" ) ), QRegularExpression::CaseInsensitiveOption );
396   QRegularExpressionMatch match = re_uri.match( wmsCrs );
397   if ( match.hasMatch() )
398   {
399     wmsCrs = match.captured( 1 ) + ':' + match.captured( 2 );
400   }
401   else
402   {
403     thread_local const QRegularExpression re_urn( QRegularExpression::anchoredPattern( QStringLiteral( "urn:ogc:def:crs:([^:]+).+(?<=:)([^:]+)" ) ), QRegularExpression::CaseInsensitiveOption );
404     match = re_urn.match( wmsCrs );
405     if ( match.hasMatch() )
406     {
407       wmsCrs = match.captured( 1 ) + ':' + match.captured( 2 );
408     }
409     else
410     {
411       thread_local const QRegularExpression re_urn_custom( QRegularExpression::anchoredPattern( QStringLiteral( "(user|custom|qgis):(\\d+)" ) ), QRegularExpression::CaseInsensitiveOption );
412       match = re_urn_custom.match( wmsCrs );
413       if ( match.hasMatch() && createFromSrsId( match.captured( 2 ).toInt() ) )
414       {
415         locker.changeMode( QgsReadWriteLocker::Write );
416         if ( !sDisableOgcCache )
417           sOgcCache()->insert( crs, *this );
418         return d->mIsValid;
419       }
420     }
421   }
422 
423   // first chance for proj 6 - scan through legacy systems and try to use authid directly
424   const QString legacyKey = wmsCrs.toLower();
425   for ( auto it = sAuthIdToQgisSrsIdMap.constBegin(); it != sAuthIdToQgisSrsIdMap.constEnd(); ++it )
426   {
427     if ( it.key().compare( legacyKey, Qt::CaseInsensitive ) == 0 )
428     {
429       const QStringList parts = it.key().split( ':' );
430       const QString auth = parts.at( 0 );
431       const QString code = parts.at( 1 );
432       if ( loadFromAuthCode( auth, code ) )
433       {
434         locker.changeMode( QgsReadWriteLocker::Write );
435         if ( !sDisableOgcCache )
436           sOgcCache()->insert( crs, *this );
437         return d->mIsValid;
438       }
439     }
440   }
441 
442   if ( loadFromDatabase( QgsApplication::srsDatabaseFilePath(), QStringLiteral( "lower(auth_name||':'||auth_id)" ), wmsCrs.toLower() ) )
443   {
444     locker.changeMode( QgsReadWriteLocker::Write );
445     if ( !sDisableOgcCache )
446       sOgcCache()->insert( crs, *this );
447     return d->mIsValid;
448   }
449 
450   // NAD27
451   if ( wmsCrs.compare( QLatin1String( "CRS:27" ), Qt::CaseInsensitive ) == 0 ||
452        wmsCrs.compare( QLatin1String( "OGC:CRS27" ), Qt::CaseInsensitive ) == 0 )
453   {
454     // TODO: verify same axis orientation
455     return createFromOgcWmsCrs( QStringLiteral( "EPSG:4267" ) );
456   }
457 
458   // NAD83
459   if ( wmsCrs.compare( QLatin1String( "CRS:83" ), Qt::CaseInsensitive ) == 0 ||
460        wmsCrs.compare( QLatin1String( "OGC:CRS83" ), Qt::CaseInsensitive ) == 0 )
461   {
462     // TODO: verify same axis orientation
463     return createFromOgcWmsCrs( QStringLiteral( "EPSG:4269" ) );
464   }
465 
466   // WGS84
467   if ( wmsCrs.compare( QLatin1String( "CRS:84" ), Qt::CaseInsensitive ) == 0 ||
468        wmsCrs.compare( QLatin1String( "OGC:CRS84" ), Qt::CaseInsensitive ) == 0 )
469   {
470     if ( loadFromDatabase( QgsApplication::srsDatabaseFilePath(), QStringLiteral( "lower(auth_name||':'||auth_id)" ), QStringLiteral( "epsg:4326" ) ) )
471     {
472       d->mAxisInverted = false;
473       d->mAxisInvertedDirty = false;
474     }
475 
476     locker.changeMode( QgsReadWriteLocker::Write );
477     if ( !sDisableOgcCache )
478       sOgcCache()->insert( crs, *this );
479 
480     return d->mIsValid;
481   }
482 
483   locker.changeMode( QgsReadWriteLocker::Write );
484   if ( !sDisableOgcCache )
485     sOgcCache()->insert( crs, QgsCoordinateReferenceSystem() );
486   return d->mIsValid;
487 }
488 
489 // Misc helper functions -----------------------
490 
491 
validate()492 void QgsCoordinateReferenceSystem::validate()
493 {
494   if ( d->mIsValid || !sCustomSrsValidation )
495     return;
496 
497   // try to validate using custom validation routines
498   if ( sCustomSrsValidation )
499     sCustomSrsValidation( *this );
500 }
501 
createFromSrid(const long id)502 bool QgsCoordinateReferenceSystem::createFromSrid( const long id )
503 {
504   QgsReadWriteLocker locker( *sSrIdCacheLock(), QgsReadWriteLocker::Read );
505   if ( !sDisableSrIdCache )
506   {
507     QHash< long, QgsCoordinateReferenceSystem >::const_iterator crsIt = sSrIdCache()->constFind( id );
508     if ( crsIt != sSrIdCache()->constEnd() )
509     {
510       // found a match in the cache
511       *this = crsIt.value();
512       return d->mIsValid;
513     }
514   }
515   locker.unlock();
516 
517   // first chance for proj 6 - scan through legacy systems and try to use authid directly
518   for ( auto it = sAuthIdToQgisSrsIdMap.constBegin(); it != sAuthIdToQgisSrsIdMap.constEnd(); ++it )
519   {
520     if ( it.value().endsWith( QStringLiteral( ",%1" ).arg( id ) ) )
521     {
522       const QStringList parts = it.key().split( ':' );
523       const QString auth = parts.at( 0 );
524       const QString code = parts.at( 1 );
525       if ( loadFromAuthCode( auth, code ) )
526       {
527         locker.changeMode( QgsReadWriteLocker::Write );
528         if ( !sDisableSrIdCache )
529           sSrIdCache()->insert( id, *this );
530 
531         return d->mIsValid;
532       }
533     }
534   }
535 
536   bool result = loadFromDatabase( QgsApplication::srsDatabaseFilePath(), QStringLiteral( "srid" ), QString::number( id ) );
537 
538   locker.changeMode( QgsReadWriteLocker::Write );
539   if ( !sDisableSrIdCache )
540     sSrIdCache()->insert( id, *this );
541 
542   return result;
543 }
544 
createFromSrsId(const long id)545 bool QgsCoordinateReferenceSystem::createFromSrsId( const long id )
546 {
547   QgsReadWriteLocker locker( *sCRSSrsIdLock(), QgsReadWriteLocker::Read );
548   if ( !sDisableSrsIdCache )
549   {
550     QHash< long, QgsCoordinateReferenceSystem >::const_iterator crsIt = sSrsIdCache()->constFind( id );
551     if ( crsIt != sSrsIdCache()->constEnd() )
552     {
553       // found a match in the cache
554       *this = crsIt.value();
555       return d->mIsValid;
556     }
557   }
558   locker.unlock();
559 
560   // first chance for proj 6 - scan through legacy systems and try to use authid directly
561   for ( auto it = sAuthIdToQgisSrsIdMap.constBegin(); it != sAuthIdToQgisSrsIdMap.constEnd(); ++it )
562   {
563     if ( it.value().startsWith( QString::number( id ) + ',' ) )
564     {
565       const QStringList parts = it.key().split( ':' );
566       const QString auth = parts.at( 0 );
567       const QString code = parts.at( 1 );
568       if ( loadFromAuthCode( auth, code ) )
569       {
570         locker.changeMode( QgsReadWriteLocker::Write );
571         if ( !sDisableSrsIdCache )
572           sSrsIdCache()->insert( id, *this );
573         return d->mIsValid;
574       }
575     }
576   }
577 
578   bool result = loadFromDatabase( id < USER_CRS_START_ID ? QgsApplication::srsDatabaseFilePath() :
579                                   QgsApplication::qgisUserDatabaseFilePath(),
580                                   QStringLiteral( "srs_id" ), QString::number( id ) );
581 
582   locker.changeMode( QgsReadWriteLocker::Write );
583   if ( !sDisableSrsIdCache )
584     sSrsIdCache()->insert( id, *this );
585   return result;
586 }
587 
loadFromDatabase(const QString & db,const QString & expression,const QString & value)588 bool QgsCoordinateReferenceSystem::loadFromDatabase( const QString &db, const QString &expression, const QString &value )
589 {
590   d.detach();
591 
592   QgsDebugMsgLevel( "load CRS from " + db + " where " + expression + " is " + value, 3 );
593   d->mIsValid = false;
594   d->mWktPreferred.clear();
595 
596   QFileInfo myInfo( db );
597   if ( !myInfo.exists() )
598   {
599     QgsDebugMsg( "failed : " + db + " does not exist!" );
600     return d->mIsValid;
601   }
602 
603   sqlite3_database_unique_ptr database;
604   sqlite3_statement_unique_ptr statement;
605   int           myResult;
606   //check the db is available
607   myResult = openDatabase( db, database );
608   if ( myResult != SQLITE_OK )
609   {
610     return d->mIsValid;
611   }
612 
613   /*
614     srs_id INTEGER PRIMARY KEY,
615     description text NOT NULL,
616     projection_acronym text NOT NULL,
617     ellipsoid_acronym NOT NULL,
618     parameters text NOT NULL,
619     srid integer NOT NULL,
620     auth_name varchar NOT NULL,
621     auth_id integer NOT NULL,
622     is_geo integer NOT NULL);
623   */
624 
625   QString mySql = "select srs_id,description,projection_acronym,"
626                   "ellipsoid_acronym,parameters,srid,auth_name||':'||auth_id,is_geo,wkt "
627                   "from tbl_srs where " + expression + '=' + QgsSqliteUtils::quotedString( value ) + " order by deprecated";
628   statement = database.prepare( mySql, myResult );
629   QString wkt;
630   // XXX Need to free memory from the error msg if one is set
631   if ( myResult == SQLITE_OK && statement.step() == SQLITE_ROW )
632   {
633     d->mSrsId = statement.columnAsText( 0 ).toLong();
634     d->mDescription = statement.columnAsText( 1 );
635     d->mProjectionAcronym = statement.columnAsText( 2 );
636     d->mEllipsoidAcronym.clear();
637     d->mProj4 = statement.columnAsText( 4 );
638     d->mWktPreferred.clear();
639     d->mSRID = statement.columnAsText( 5 ).toLong();
640     d->mAuthId = statement.columnAsText( 6 );
641     d->mIsGeographic = statement.columnAsText( 7 ).toInt() != 0;
642     wkt = statement.columnAsText( 8 );
643     d->mAxisInvertedDirty = true;
644 
645     if ( d->mSrsId >= USER_CRS_START_ID && ( d->mAuthId.isEmpty() || d->mAuthId == QChar( ':' ) ) )
646     {
647       d->mAuthId = QStringLiteral( "USER:%1" ).arg( d->mSrsId );
648     }
649     else if ( !d->mAuthId.startsWith( QLatin1String( "USER:" ), Qt::CaseInsensitive ) )
650     {
651       QStringList parts = d->mAuthId.split( ':' );
652       QString auth = parts.at( 0 );
653       QString code = parts.at( 1 );
654 
655       {
656         QgsProjUtils::proj_pj_unique_ptr crs( proj_create_from_database( QgsProjContext::get(), auth.toLatin1(), code.toLatin1(), PJ_CATEGORY_CRS, false, nullptr ) );
657         d->setPj( QgsProjUtils::crsToSingleCrs( crs.get() ) );
658       }
659 
660       d->mIsValid = d->hasPj();
661       setMapUnits();
662     }
663 
664     if ( !d->mIsValid )
665     {
666       if ( !wkt.isEmpty() )
667       {
668         setWktString( wkt );
669         // set WKT string resets the description to that description embedded in the WKT, so manually overwrite this back to the
670         // value from the user DB
671         d->mDescription = statement.columnAsText( 1 );
672       }
673       else
674         setProjString( d->mProj4 );
675     }
676   }
677   else
678   {
679     QgsDebugMsgLevel( "failed : " + mySql, 4 );
680   }
681   return d->mIsValid;
682 }
683 
removeFromCacheObjectsBelongingToCurrentThread(PJ_CONTEXT * pj_context)684 void QgsCoordinateReferenceSystem::removeFromCacheObjectsBelongingToCurrentThread( PJ_CONTEXT *pj_context )
685 {
686   // Not completely sure about object order destruction after main() has
687   // exited. So it is safer to check sDisableCache before using sCacheLock
688   // in case sCacheLock would have been destroyed before the current TLS
689   // QgsProjContext object that has called us...
690 
691   if ( !sDisableSrIdCache )
692   {
693     QgsReadWriteLocker locker( *sSrIdCacheLock(), QgsReadWriteLocker::Write );
694     if ( !sDisableSrIdCache )
695     {
696       for ( auto it = sSrIdCache()->begin(); it != sSrIdCache()->end(); )
697       {
698         auto &v = it.value();
699         if ( v.d->removeObjectsBelongingToCurrentThread( pj_context ) )
700           it = sSrIdCache()->erase( it );
701         else
702           ++it;
703       }
704     }
705   }
706   if ( !sDisableOgcCache )
707   {
708     QgsReadWriteLocker locker( *sOgcLock(), QgsReadWriteLocker::Write );
709     if ( !sDisableOgcCache )
710     {
711       for ( auto it = sOgcCache()->begin(); it != sOgcCache()->end(); )
712       {
713         auto &v = it.value();
714         if ( v.d->removeObjectsBelongingToCurrentThread( pj_context ) )
715           it = sOgcCache()->erase( it );
716         else
717           ++it;
718       }
719     }
720   }
721   if ( !sDisableProjCache )
722   {
723     QgsReadWriteLocker locker( *sProj4CacheLock(), QgsReadWriteLocker::Write );
724     if ( !sDisableProjCache )
725     {
726       for ( auto it = sProj4Cache()->begin(); it != sProj4Cache()->end(); )
727       {
728         auto &v = it.value();
729         if ( v.d->removeObjectsBelongingToCurrentThread( pj_context ) )
730           it = sProj4Cache()->erase( it );
731         else
732           ++it;
733       }
734     }
735   }
736   if ( !sDisableWktCache )
737   {
738     QgsReadWriteLocker locker( *sCRSWktLock(), QgsReadWriteLocker::Write );
739     if ( !sDisableWktCache )
740     {
741       for ( auto it = sWktCache()->begin(); it != sWktCache()->end(); )
742       {
743         auto &v = it.value();
744         if ( v.d->removeObjectsBelongingToCurrentThread( pj_context ) )
745           it = sWktCache()->erase( it );
746         else
747           ++it;
748       }
749     }
750   }
751   if ( !sDisableSrsIdCache )
752   {
753     QgsReadWriteLocker locker( *sCRSSrsIdLock(), QgsReadWriteLocker::Write );
754     if ( !sDisableSrsIdCache )
755     {
756       for ( auto it = sSrsIdCache()->begin(); it != sSrsIdCache()->end(); )
757       {
758         auto &v = it.value();
759         if ( v.d->removeObjectsBelongingToCurrentThread( pj_context ) )
760           it = sSrsIdCache()->erase( it );
761         else
762           ++it;
763       }
764     }
765   }
766   if ( !sDisableStringCache )
767   {
768     QgsReadWriteLocker locker( *sCrsStringLock(), QgsReadWriteLocker::Write );
769     if ( !sDisableStringCache )
770     {
771       for ( auto it = sStringCache()->begin(); it != sStringCache()->end(); )
772       {
773         auto &v = it.value();
774         if ( v.d->removeObjectsBelongingToCurrentThread( pj_context ) )
775           it = sStringCache()->erase( it );
776         else
777           ++it;
778       }
779     }
780   }
781 }
782 
hasAxisInverted() const783 bool QgsCoordinateReferenceSystem::hasAxisInverted() const
784 {
785   if ( d->mAxisInvertedDirty )
786   {
787     d->mAxisInverted = QgsProjUtils::axisOrderIsSwapped( d->threadLocalProjObject() );
788     d->mAxisInvertedDirty = false;
789   }
790 
791   return d->mAxisInverted;
792 }
793 
createFromWkt(const QString & wkt)794 bool QgsCoordinateReferenceSystem::createFromWkt( const QString &wkt )
795 {
796   return createFromWktInternal( wkt, QString() );
797 }
798 
createFromWktInternal(const QString & wkt,const QString & description)799 bool QgsCoordinateReferenceSystem::createFromWktInternal( const QString &wkt, const QString &description )
800 {
801   if ( wkt.isEmpty() )
802     return false;
803 
804   d.detach();
805 
806   QgsReadWriteLocker locker( *sCRSWktLock(), QgsReadWriteLocker::Read );
807   if ( !sDisableWktCache )
808   {
809     QHash< QString, QgsCoordinateReferenceSystem >::const_iterator crsIt = sWktCache()->constFind( wkt );
810     if ( crsIt != sWktCache()->constEnd() )
811     {
812       // found a match in the cache
813       *this = crsIt.value();
814 
815       if ( !description.isEmpty() && d->mDescription.isEmpty() )
816       {
817         // now we have a name for a previously unknown CRS! Update the cached CRS accordingly, so that we use the name from now on...
818         d->mDescription = description;
819         locker.changeMode( QgsReadWriteLocker::Write );
820         sWktCache()->insert( wkt, *this );
821       }
822       return d->mIsValid;
823     }
824   }
825   locker.unlock();
826 
827   d->mIsValid = false;
828   d->mProj4.clear();
829   d->mWktPreferred.clear();
830   if ( wkt.isEmpty() )
831   {
832     QgsDebugMsgLevel( QStringLiteral( "theWkt is uninitialized, operation failed" ), 4 );
833     return d->mIsValid;
834   }
835 
836   // try to match against user crs
837   QgsCoordinateReferenceSystem::RecordMap record = getRecord( "select * from tbl_srs where wkt=" + QgsSqliteUtils::quotedString( wkt ) + " order by deprecated" );
838   if ( !record.empty() )
839   {
840     long srsId = record[QStringLiteral( "srs_id" )].toLong();
841     if ( srsId > 0 )
842     {
843       createFromSrsId( srsId );
844     }
845   }
846   else
847   {
848     setWktString( wkt );
849     if ( !description.isEmpty() )
850     {
851       d->mDescription = description;
852     }
853     if ( d->mSrsId == 0 )
854     {
855       // lastly, try a tolerant match of the created proj object against all user CRSes (allowing differences in parameter order during the comparison)
856       long id = matchToUserCrs();
857       if ( id >= USER_CRS_START_ID )
858       {
859         createFromSrsId( id );
860       }
861     }
862   }
863 
864   locker.changeMode( QgsReadWriteLocker::Write );
865   if ( !sDisableWktCache )
866     sWktCache()->insert( wkt, *this );
867 
868   return d->mIsValid;
869   //setMapunits will be called by createfromproj above
870 }
871 
isValid() const872 bool QgsCoordinateReferenceSystem::isValid() const
873 {
874   return d->mIsValid;
875 }
876 
createFromProj4(const QString & proj4String)877 bool QgsCoordinateReferenceSystem::createFromProj4( const QString &proj4String )
878 {
879   return createFromProj( proj4String );
880 }
881 
createFromProj(const QString & projString,const bool identify)882 bool QgsCoordinateReferenceSystem::createFromProj( const QString &projString, const bool identify )
883 {
884   if ( projString.isEmpty() )
885     return false;
886 
887   d.detach();
888 
889   if ( projString.trimmed().isEmpty() )
890   {
891     d->mIsValid = false;
892     d->mProj4.clear();
893     d->mWktPreferred.clear();
894     return false;
895   }
896 
897   QgsReadWriteLocker locker( *sProj4CacheLock(), QgsReadWriteLocker::Read );
898   if ( !sDisableProjCache )
899   {
900     QHash< QString, QgsCoordinateReferenceSystem >::const_iterator crsIt = sProj4Cache()->constFind( projString );
901     if ( crsIt != sProj4Cache()->constEnd() )
902     {
903       // found a match in the cache
904       *this = crsIt.value();
905       return d->mIsValid;
906     }
907   }
908   locker.unlock();
909 
910   //
911   // Examples:
912   // +proj=tmerc +lat_0=0 +lon_0=-62 +k=0.999500 +x_0=400000 +y_0=0
913   // +ellps=clrk80 +towgs84=-255,-15,71,0,0,0,0 +units=m +no_defs
914   //
915   // +proj=lcc +lat_1=46.8 +lat_0=46.8 +lon_0=2.337229166666664 +k_0=0.99987742
916   // +x_0=600000 +y_0=2200000 +a=6378249.2 +b=6356515.000000472 +units=m +no_defs
917   //
918   QString myProj4String = projString.trimmed();
919   myProj4String.remove( QStringLiteral( "+type=crs" ) );
920   myProj4String = myProj4String.trimmed();
921 
922   d->mIsValid = false;
923   d->mWktPreferred.clear();
924 
925   if ( identify )
926   {
927     // first, try to use proj to do this for us...
928     const QString projCrsString = myProj4String + ( myProj4String.contains( QStringLiteral( "+type=crs" ) ) ? QString() : QStringLiteral( " +type=crs" ) );
929     QgsProjUtils::proj_pj_unique_ptr crs( proj_create( QgsProjContext::get(), projCrsString.toLatin1().constData() ) );
930     if ( crs )
931     {
932       QString authName;
933       QString authCode;
934       if ( QgsProjUtils::identifyCrs( crs.get(), authName, authCode, QgsProjUtils::FlagMatchBoundCrsToUnderlyingSourceCrs ) )
935       {
936         const QString authid = QStringLiteral( "%1:%2" ).arg( authName, authCode );
937         if ( createFromOgcWmsCrs( authid ) )
938         {
939           locker.changeMode( QgsReadWriteLocker::Write );
940           if ( !sDisableProjCache )
941             sProj4Cache()->insert( projString, *this );
942           return d->mIsValid;
943         }
944       }
945     }
946 
947     // try a direct match against user crses
948     QgsCoordinateReferenceSystem::RecordMap myRecord = getRecord( "select * from tbl_srs where parameters=" + QgsSqliteUtils::quotedString( myProj4String ) + " order by deprecated" );
949     long id = 0;
950     if ( !myRecord.empty() )
951     {
952       id = myRecord[QStringLiteral( "srs_id" )].toLong();
953       if ( id >= USER_CRS_START_ID )
954       {
955         createFromSrsId( id );
956       }
957     }
958     if ( id < USER_CRS_START_ID )
959     {
960       // no direct matches, so go ahead and create a new proj object based on the proj string alone.
961       setProjString( myProj4String );
962 
963       // lastly, try a tolerant match of the created proj object against all user CRSes (allowing differences in parameter order during the comparison)
964       id = matchToUserCrs();
965       if ( id >= USER_CRS_START_ID )
966       {
967         createFromSrsId( id );
968       }
969     }
970   }
971   else
972   {
973     setProjString( myProj4String );
974   }
975 
976   locker.changeMode( QgsReadWriteLocker::Write );
977   if ( !sDisableProjCache )
978     sProj4Cache()->insert( projString, *this );
979 
980   return d->mIsValid;
981 }
982 
983 //private method meant for internal use by this class only
getRecord(const QString & sql)984 QgsCoordinateReferenceSystem::RecordMap QgsCoordinateReferenceSystem::getRecord( const QString &sql )
985 {
986   QString myDatabaseFileName;
987   QgsCoordinateReferenceSystem::RecordMap myMap;
988   QString myFieldName;
989   QString myFieldValue;
990   sqlite3_database_unique_ptr database;
991   sqlite3_statement_unique_ptr statement;
992   int           myResult;
993 
994   // Get the full path name to the sqlite3 spatial reference database.
995   myDatabaseFileName = QgsApplication::srsDatabaseFilePath();
996   QFileInfo myInfo( myDatabaseFileName );
997   if ( !myInfo.exists() )
998   {
999     QgsDebugMsg( "failed : " + myDatabaseFileName + " does not exist!" );
1000     return myMap;
1001   }
1002 
1003   //check the db is available
1004   myResult = openDatabase( myDatabaseFileName, database );
1005   if ( myResult != SQLITE_OK )
1006   {
1007     return myMap;
1008   }
1009 
1010   statement = database.prepare( sql, myResult );
1011   // XXX Need to free memory from the error msg if one is set
1012   if ( myResult == SQLITE_OK && statement.step() == SQLITE_ROW )
1013   {
1014     int myColumnCount = statement.columnCount();
1015     //loop through each column in the record adding its expression name and value to the map
1016     for ( int myColNo = 0; myColNo < myColumnCount; myColNo++ )
1017     {
1018       myFieldName = statement.columnName( myColNo );
1019       myFieldValue = statement.columnAsText( myColNo );
1020       myMap[myFieldName] = myFieldValue;
1021     }
1022     if ( statement.step() != SQLITE_DONE )
1023     {
1024       QgsDebugMsgLevel( QStringLiteral( "Multiple records found in srs.db" ), 4 );
1025       //be less fussy on proj 6 -- the db has MANY more entries!
1026     }
1027   }
1028   else
1029   {
1030     QgsDebugMsgLevel( "failed :  " + sql, 4 );
1031   }
1032 
1033   if ( myMap.empty() )
1034   {
1035     myDatabaseFileName = QgsApplication::qgisUserDatabaseFilePath();
1036     QFileInfo myFileInfo;
1037     myFileInfo.setFile( myDatabaseFileName );
1038     if ( !myFileInfo.exists() )
1039     {
1040       QgsDebugMsg( QStringLiteral( "user qgis.db not found" ) );
1041       return myMap;
1042     }
1043 
1044     //check the db is available
1045     myResult = openDatabase( myDatabaseFileName, database );
1046     if ( myResult != SQLITE_OK )
1047     {
1048       return myMap;
1049     }
1050 
1051     statement = database.prepare( sql, myResult );
1052     // XXX Need to free memory from the error msg if one is set
1053     if ( myResult == SQLITE_OK && statement.step() == SQLITE_ROW )
1054     {
1055       int myColumnCount = statement.columnCount();
1056       //loop through each column in the record adding its field name and value to the map
1057       for ( int myColNo = 0; myColNo < myColumnCount; myColNo++ )
1058       {
1059         myFieldName = statement.columnName( myColNo );
1060         myFieldValue = statement.columnAsText( myColNo );
1061         myMap[myFieldName] = myFieldValue;
1062       }
1063 
1064       if ( statement.step() != SQLITE_DONE )
1065       {
1066         QgsDebugMsgLevel( QStringLiteral( "Multiple records found in srs.db" ), 4 );
1067         myMap.clear();
1068       }
1069     }
1070     else
1071     {
1072       QgsDebugMsgLevel( "failed :  " + sql, 4 );
1073     }
1074   }
1075   return myMap;
1076 }
1077 
1078 // Accessors -----------------------------------
1079 
srsid() const1080 long QgsCoordinateReferenceSystem::srsid() const
1081 {
1082   return d->mSrsId;
1083 }
1084 
postgisSrid() const1085 long QgsCoordinateReferenceSystem::postgisSrid() const
1086 {
1087   return d->mSRID;
1088 }
1089 
authid() const1090 QString QgsCoordinateReferenceSystem::authid() const
1091 {
1092   return d->mAuthId;
1093 }
1094 
description() const1095 QString QgsCoordinateReferenceSystem::description() const
1096 {
1097   if ( d->mDescription.isNull() )
1098   {
1099     return QString();
1100   }
1101   else
1102   {
1103     return d->mDescription;
1104   }
1105 }
1106 
userFriendlyIdentifier(IdentifierType type) const1107 QString QgsCoordinateReferenceSystem::userFriendlyIdentifier( IdentifierType type ) const
1108 {
1109   QString id;
1110   if ( !authid().isEmpty() )
1111   {
1112     if ( type != ShortString && !description().isEmpty() )
1113       id = QStringLiteral( "%1 - %2" ).arg( authid(), description() );
1114     else
1115       id = authid();
1116   }
1117   else if ( !description().isEmpty() )
1118     id = description();
1119   else if ( type == ShortString )
1120     id = isValid() ? QObject::tr( "Custom CRS" ) : QObject::tr( "Unknown CRS" );
1121   else if ( !toWkt( WKT_PREFERRED ).isEmpty() )
1122     id = QObject::tr( "Custom CRS: %1" ).arg(
1123            type == MediumString ? ( toWkt( WKT_PREFERRED ).left( 50 ) + QString( QChar( 0x2026 ) ) )
1124            : toWkt( WKT_PREFERRED ) );
1125   else if ( !toProj().isEmpty() )
1126     id = QObject::tr( "Custom CRS: %1" ).arg( type == MediumString ? ( toProj().left( 50 ) + QString( QChar( 0x2026 ) ) )
1127          : toProj() );
1128   if ( !id.isEmpty() && !std::isnan( d->mCoordinateEpoch ) )
1129     id += QStringLiteral( " @ %1" ).arg( d->mCoordinateEpoch );
1130 
1131   return id;
1132 }
1133 
projectionAcronym() const1134 QString QgsCoordinateReferenceSystem::projectionAcronym() const
1135 {
1136   if ( d->mProjectionAcronym.isNull() )
1137   {
1138     return QString();
1139   }
1140   else
1141   {
1142     return d->mProjectionAcronym;
1143   }
1144 }
1145 
ellipsoidAcronym() const1146 QString QgsCoordinateReferenceSystem::ellipsoidAcronym() const
1147 {
1148   if ( d->mEllipsoidAcronym.isNull() )
1149   {
1150     if ( PJ *obj = d->threadLocalProjObject() )
1151     {
1152       QgsProjUtils::proj_pj_unique_ptr ellipsoid( proj_get_ellipsoid( QgsProjContext::get(), obj ) );
1153       if ( ellipsoid )
1154       {
1155         const QString ellipsoidAuthName( proj_get_id_auth_name( ellipsoid.get(), 0 ) );
1156         const QString ellipsoidAuthCode( proj_get_id_code( ellipsoid.get(), 0 ) );
1157         if ( !ellipsoidAuthName.isEmpty() && !ellipsoidAuthCode.isEmpty() )
1158           d->mEllipsoidAcronym = QStringLiteral( "%1:%2" ).arg( ellipsoidAuthName, ellipsoidAuthCode );
1159         else
1160         {
1161           double semiMajor, semiMinor, invFlattening;
1162           int semiMinorComputed = 0;
1163           if ( proj_ellipsoid_get_parameters( QgsProjContext::get(), ellipsoid.get(), &semiMajor, &semiMinor, &semiMinorComputed, &invFlattening ) )
1164           {
1165             d->mEllipsoidAcronym = QStringLiteral( "PARAMETER:%1:%2" ).arg( qgsDoubleToString( semiMajor ),
1166                                    qgsDoubleToString( semiMinor ) );
1167           }
1168           else
1169           {
1170             d->mEllipsoidAcronym.clear();
1171           }
1172         }
1173       }
1174     }
1175     return d->mEllipsoidAcronym;
1176   }
1177   else
1178   {
1179     return d->mEllipsoidAcronym;
1180   }
1181 }
1182 
toProj4() const1183 QString QgsCoordinateReferenceSystem::toProj4() const
1184 {
1185   return toProj();
1186 }
1187 
toProj() const1188 QString QgsCoordinateReferenceSystem::toProj() const
1189 {
1190   if ( !d->mIsValid )
1191     return QString();
1192 
1193   if ( d->mProj4.isEmpty() )
1194   {
1195     if ( PJ *obj = d->threadLocalProjObject() )
1196     {
1197       d->mProj4 = getFullProjString( obj );
1198     }
1199   }
1200   // Stray spaces at the end?
1201   return d->mProj4.trimmed();
1202 }
1203 
isGeographic() const1204 bool QgsCoordinateReferenceSystem::isGeographic() const
1205 {
1206   return d->mIsGeographic;
1207 }
1208 
isDynamic() const1209 bool QgsCoordinateReferenceSystem::isDynamic() const
1210 {
1211   const PJ *pj = projObject();
1212   if ( !pj )
1213     return false;
1214 
1215   return QgsProjUtils::isDynamic( pj );
1216 }
1217 
celestialBodyName() const1218 QString QgsCoordinateReferenceSystem::celestialBodyName() const
1219 {
1220   const PJ *pj = projObject();
1221   if ( !pj )
1222     return QString();
1223 
1224 #if PROJ_VERSION_MAJOR>8 || (PROJ_VERSION_MAJOR==8 && PROJ_VERSION_MINOR>=1)
1225   PJ_CONTEXT *context = QgsProjContext::get();
1226 
1227   return QString( proj_get_celestial_body_name( context, pj ) );
1228 #else
1229   throw QgsNotSupportedException( QStringLiteral( "Retrieving celestial body requires a QGIS build based on PROJ 8.1 or later" ) );
1230 #endif
1231 }
1232 
setCoordinateEpoch(double epoch)1233 void QgsCoordinateReferenceSystem::setCoordinateEpoch( double epoch )
1234 {
1235   if ( d->mCoordinateEpoch == epoch )
1236     return;
1237 
1238   // detaching clears the proj object, so we need to clone the existing one first
1239   QgsProjUtils::proj_pj_unique_ptr clone( proj_clone( QgsProjContext::get(), projObject() ) );
1240   d.detach();
1241   d->mCoordinateEpoch = epoch;
1242   d->setPj( std::move( clone ) );
1243 }
1244 
coordinateEpoch() const1245 double QgsCoordinateReferenceSystem::coordinateEpoch() const
1246 {
1247   return d->mCoordinateEpoch;
1248 }
1249 
datumEnsemble() const1250 QgsDatumEnsemble QgsCoordinateReferenceSystem::datumEnsemble() const
1251 {
1252   QgsDatumEnsemble res;
1253   res.mValid = false;
1254 
1255   const PJ *pj = projObject();
1256   if ( !pj )
1257     return res;
1258 
1259 #if PROJ_VERSION_MAJOR>=8
1260   PJ_CONTEXT *context = QgsProjContext::get();
1261 
1262   QgsProjUtils::proj_pj_unique_ptr ensemble = QgsProjUtils::crsToDatumEnsemble( pj );
1263   if ( !ensemble )
1264     return res;
1265 
1266   res.mValid = true;
1267   res.mName = QString( proj_get_name( ensemble.get() ) );
1268   res.mAuthority = QString( proj_get_id_auth_name( ensemble.get(), 0 ) );
1269   res.mCode = QString( proj_get_id_code( ensemble.get(), 0 ) );
1270   res.mRemarks = QString( proj_get_remarks( ensemble.get() ) );
1271   res.mScope = QString( proj_get_scope( ensemble.get() ) );
1272   res.mAccuracy = proj_datum_ensemble_get_accuracy( context, ensemble.get() );
1273 
1274   const int memberCount = proj_datum_ensemble_get_member_count( context, ensemble.get() );
1275   for ( int i = 0; i < memberCount; ++i )
1276   {
1277     QgsProjUtils::proj_pj_unique_ptr member( proj_datum_ensemble_get_member( context, ensemble.get(), i ) );
1278     if ( !member )
1279       continue;
1280 
1281     QgsDatumEnsembleMember details;
1282     details.mName = QString( proj_get_name( member.get() ) );
1283     details.mAuthority = QString( proj_get_id_auth_name( member.get(), 0 ) );
1284     details.mCode = QString( proj_get_id_code( member.get(), 0 ) );
1285     details.mRemarks = QString( proj_get_remarks( member.get() ) );
1286     details.mScope = QString( proj_get_scope( member.get() ) );
1287 
1288     res.mMembers << details;
1289   }
1290   return res;
1291 #else
1292   throw QgsNotSupportedException( QStringLiteral( "Calculating datum ensembles requires a QGIS build based on PROJ 8.0 or later" ) );
1293 #endif
1294 }
1295 
factors(const QgsPoint & point) const1296 QgsProjectionFactors QgsCoordinateReferenceSystem::factors( const QgsPoint &point ) const
1297 {
1298   QgsProjectionFactors res;
1299 
1300   // we have to make a transformation object corresponding to the crs
1301   QString projString = toProj();
1302   projString.replace( QLatin1String( "+type=crs" ), QString() );
1303 
1304   QgsProjUtils::proj_pj_unique_ptr transformation( proj_create( QgsProjContext::get(), projString.toUtf8().constData() ) );
1305   if ( !transformation )
1306     return res;
1307 
1308   PJ_COORD coord = proj_coord( 0, 0, 0, HUGE_VAL );
1309   coord.uv.u = point.x() * M_PI / 180.0;
1310   coord.uv.v = point.y() * M_PI / 180.0;
1311 
1312   proj_errno_reset( transformation.get() );
1313   const PJ_FACTORS pjFactors = proj_factors( transformation.get(), coord );
1314   if ( proj_errno( transformation.get() ) )
1315   {
1316     return res;
1317   }
1318 
1319   res.mIsValid = true;
1320   res.mMeridionalScale = pjFactors.meridional_scale;
1321   res.mParallelScale = pjFactors.parallel_scale;
1322   res.mArealScale = pjFactors.areal_scale;
1323   res.mAngularDistortion = pjFactors.angular_distortion;
1324   res.mMeridianParallelAngle = pjFactors.meridian_parallel_angle * 180 / M_PI;
1325   res.mMeridianConvergence = pjFactors.meridian_convergence * 180 / M_PI;
1326   res.mTissotSemimajor = pjFactors.tissot_semimajor;
1327   res.mTissotSemiminor = pjFactors.tissot_semiminor;
1328   res.mDxDlam = pjFactors.dx_dlam;
1329   res.mDxDphi = pjFactors.dx_dphi;
1330   res.mDyDlam = pjFactors.dy_dlam;
1331   res.mDyDphi = pjFactors.dy_dphi;
1332   return res;
1333 }
1334 
operation() const1335 QgsProjOperation QgsCoordinateReferenceSystem::operation() const
1336 {
1337   QgsProjOperation res;
1338 
1339   // we have to make a transformation object corresponding to the crs
1340   QString projString = toProj();
1341   projString.replace( QLatin1String( "+type=crs" ), QString() );
1342 
1343   QgsProjUtils::proj_pj_unique_ptr transformation( proj_create( QgsProjContext::get(), projString.toUtf8().constData() ) );
1344   if ( !transformation )
1345     return res;
1346 
1347   PJ_PROJ_INFO info = proj_pj_info( transformation.get() );
1348 
1349   if ( info.id )
1350   {
1351     return QgsApplication::coordinateReferenceSystemRegistry()->projOperations().value( QString( info.id ) );
1352   }
1353 
1354   return res;
1355 }
1356 
mapUnits() const1357 QgsUnitTypes::DistanceUnit QgsCoordinateReferenceSystem::mapUnits() const
1358 {
1359   if ( !d->mIsValid )
1360     return QgsUnitTypes::DistanceUnknownUnit;
1361 
1362   return d->mMapUnits;
1363 }
1364 
bounds() const1365 QgsRectangle QgsCoordinateReferenceSystem::bounds() const
1366 {
1367   if ( !d->mIsValid )
1368     return QgsRectangle();
1369 
1370   PJ *obj = d->threadLocalProjObject();
1371   if ( !obj )
1372     return QgsRectangle();
1373 
1374   double westLon = 0;
1375   double southLat = 0;
1376   double eastLon = 0;
1377   double northLat = 0;
1378 
1379   if ( !proj_get_area_of_use( QgsProjContext::get(), obj,
1380                               &westLon, &southLat, &eastLon, &northLat, nullptr ) )
1381     return QgsRectangle();
1382 
1383 
1384   // don't use the constructor which normalizes!
1385   QgsRectangle rect;
1386   rect.setXMinimum( westLon );
1387   rect.setYMinimum( southLat );
1388   rect.setXMaximum( eastLon );
1389   rect.setYMaximum( northLat );
1390   return rect;
1391 }
1392 
updateDefinition()1393 void QgsCoordinateReferenceSystem::updateDefinition()
1394 {
1395   if ( !d->mIsValid )
1396     return;
1397 
1398   if ( d->mSrsId >= USER_CRS_START_ID )
1399   {
1400     // user CRS, so update to new definition
1401     createFromSrsId( d->mSrsId );
1402   }
1403   else
1404   {
1405     // nothing to do -- only user CRS definitions can be changed
1406   }
1407 }
1408 
setProjString(const QString & proj4String)1409 void QgsCoordinateReferenceSystem::setProjString( const QString &proj4String )
1410 {
1411   d.detach();
1412   d->mProj4 = proj4String;
1413   d->mWktPreferred.clear();
1414 
1415   QgsLocaleNumC l;
1416   QString trimmed = proj4String.trimmed();
1417 
1418   trimmed += QLatin1String( " +type=crs" );
1419   PJ_CONTEXT *ctx = QgsProjContext::get();
1420 
1421   {
1422     d->setPj( QgsProjUtils::proj_pj_unique_ptr( proj_create( ctx, trimmed.toLatin1().constData() ) ) );
1423   }
1424 
1425   if ( !d->hasPj() )
1426   {
1427 #ifdef QGISDEBUG
1428     const int errNo = proj_context_errno( ctx );
1429     QgsDebugMsg( QStringLiteral( "proj string rejected: %1" ).arg( proj_errno_string( errNo ) ) );
1430 #endif
1431     d->mIsValid = false;
1432   }
1433   else
1434   {
1435     d->mEllipsoidAcronym.clear();
1436     d->mIsValid = true;
1437   }
1438 
1439   setMapUnits();
1440 }
1441 
setWktString(const QString & wkt)1442 bool QgsCoordinateReferenceSystem::setWktString( const QString &wkt )
1443 {
1444   bool res = false;
1445   d->mIsValid = false;
1446   d->mWktPreferred.clear();
1447 
1448   PROJ_STRING_LIST warnings = nullptr;
1449   PROJ_STRING_LIST grammerErrors = nullptr;
1450   {
1451     d->setPj( QgsProjUtils::proj_pj_unique_ptr( proj_create_from_wkt( QgsProjContext::get(), wkt.toLatin1().constData(), nullptr, &warnings, &grammerErrors ) ) );
1452   }
1453 
1454   res = d->hasPj();
1455   if ( !res )
1456   {
1457     QgsDebugMsg( QStringLiteral( "\n---------------------------------------------------------------" ) );
1458     QgsDebugMsg( QStringLiteral( "This CRS could *** NOT *** be set from the supplied Wkt " ) );
1459     QgsDebugMsg( "INPUT: " + wkt );
1460     for ( auto iter = warnings; iter && *iter; ++iter )
1461       QgsDebugMsg( *iter );
1462     for ( auto iter = grammerErrors; iter && *iter; ++iter )
1463       QgsDebugMsg( *iter );
1464     QgsDebugMsg( QStringLiteral( "---------------------------------------------------------------\n" ) );
1465   }
1466   proj_string_list_destroy( warnings );
1467   proj_string_list_destroy( grammerErrors );
1468 
1469   QgsReadWriteLocker locker( *sProj4CacheLock(), QgsReadWriteLocker::Unlocked );
1470   if ( !res )
1471   {
1472     locker.changeMode( QgsReadWriteLocker::Write );
1473     if ( !sDisableWktCache )
1474       sWktCache()->insert( wkt, *this );
1475     return d->mIsValid;
1476   }
1477 
1478   if ( d->hasPj() )
1479   {
1480     // try 1 - maybe we can directly grab the auth name and code from the crs already?
1481     QString authName( proj_get_id_auth_name( d->threadLocalProjObject(), 0 ) );
1482     QString authCode( proj_get_id_code( d->threadLocalProjObject(), 0 ) );
1483 
1484     if ( authName.isEmpty() || authCode.isEmpty() )
1485     {
1486       // try 2, use proj's identify method and see if there's a nice candidate we can use
1487       QgsProjUtils::identifyCrs( d->threadLocalProjObject(), authName, authCode );
1488     }
1489 
1490     if ( !authName.isEmpty() && !authCode.isEmpty() )
1491     {
1492       if ( loadFromAuthCode( authName, authCode ) )
1493       {
1494         locker.changeMode( QgsReadWriteLocker::Write );
1495         if ( !sDisableWktCache )
1496           sWktCache()->insert( wkt, *this );
1497         return d->mIsValid;
1498       }
1499     }
1500     else
1501     {
1502       // Still a valid CRS, just not a known one
1503       d->mIsValid = true;
1504       d->mDescription = QString( proj_get_name( d->threadLocalProjObject() ) );
1505     }
1506     setMapUnits();
1507   }
1508 
1509   return d->mIsValid;
1510 }
1511 
setMapUnits()1512 void QgsCoordinateReferenceSystem::setMapUnits()
1513 {
1514   if ( !d->mIsValid )
1515   {
1516     d->mMapUnits = QgsUnitTypes::DistanceUnknownUnit;
1517     return;
1518   }
1519 
1520   if ( !d->hasPj() )
1521   {
1522     d->mMapUnits = QgsUnitTypes::DistanceUnknownUnit;
1523     return;
1524   }
1525 
1526   PJ_CONTEXT *context = QgsProjContext::get();
1527   QgsProjUtils::proj_pj_unique_ptr crs( QgsProjUtils::crsToSingleCrs( d->threadLocalProjObject() ) );
1528   QgsProjUtils::proj_pj_unique_ptr coordinateSystem( proj_crs_get_coordinate_system( context, crs.get() ) );
1529   if ( !coordinateSystem )
1530   {
1531     d->mMapUnits = QgsUnitTypes::DistanceUnknownUnit;
1532     return;
1533   }
1534 
1535   const int axisCount = proj_cs_get_axis_count( context, coordinateSystem.get() );
1536   if ( axisCount > 0 )
1537   {
1538     const char *outUnitName = nullptr;
1539     // Read only first axis
1540     proj_cs_get_axis_info( context, coordinateSystem.get(), 0,
1541                            nullptr,
1542                            nullptr,
1543                            nullptr,
1544                            nullptr,
1545                            &outUnitName,
1546                            nullptr,
1547                            nullptr );
1548 
1549     const QString unitName( outUnitName );
1550 
1551     // proj unit names are freeform -- they differ from authority to authority :(
1552     // see https://lists.osgeo.org/pipermail/proj/2019-April/008444.html
1553     if ( unitName.compare( QLatin1String( "degree" ), Qt::CaseInsensitive ) == 0 ||
1554          unitName.compare( QLatin1String( "degree minute second" ), Qt::CaseInsensitive ) == 0 ||
1555          unitName.compare( QLatin1String( "degree minute second hemisphere" ), Qt::CaseInsensitive ) == 0 ||
1556          unitName.compare( QLatin1String( "degree minute" ), Qt::CaseInsensitive ) == 0 ||
1557          unitName.compare( QLatin1String( "degree hemisphere" ), Qt::CaseInsensitive ) == 0 ||
1558          unitName.compare( QLatin1String( "degree minute hemisphere" ), Qt::CaseInsensitive ) == 0 ||
1559          unitName.compare( QLatin1String( "hemisphere degree" ), Qt::CaseInsensitive ) == 0 ||
1560          unitName.compare( QLatin1String( "hemisphere degree minute" ), Qt::CaseInsensitive ) == 0 ||
1561          unitName.compare( QLatin1String( "hemisphere degree minute second" ), Qt::CaseInsensitive ) == 0 ||
1562          unitName.compare( QLatin1String( "degree (supplier to define representation)" ), Qt::CaseInsensitive ) == 0 )
1563       d->mMapUnits = QgsUnitTypes::DistanceDegrees;
1564     else if ( unitName.compare( QLatin1String( "metre" ), Qt::CaseInsensitive ) == 0
1565               || unitName.compare( QLatin1String( "m" ), Qt::CaseInsensitive ) == 0
1566               || unitName.compare( QLatin1String( "meter" ), Qt::CaseInsensitive ) == 0 )
1567       d->mMapUnits = QgsUnitTypes::DistanceMeters;
1568     // we don't differentiate between these, suck it imperial users!
1569     else if ( unitName.compare( QLatin1String( "US survey foot" ), Qt::CaseInsensitive ) == 0 ||
1570               unitName.compare( QLatin1String( "foot" ), Qt::CaseInsensitive ) == 0 )
1571       d->mMapUnits = QgsUnitTypes::DistanceFeet;
1572     else if ( unitName.compare( QLatin1String( "kilometre" ), Qt::CaseInsensitive ) == 0 )  //#spellok
1573       d->mMapUnits = QgsUnitTypes::DistanceKilometers;
1574     else if ( unitName.compare( QLatin1String( "centimetre" ), Qt::CaseInsensitive ) == 0 )  //#spellok
1575       d->mMapUnits = QgsUnitTypes::DistanceCentimeters;
1576     else if ( unitName.compare( QLatin1String( "millimetre" ), Qt::CaseInsensitive ) == 0 )  //#spellok
1577       d->mMapUnits = QgsUnitTypes::DistanceMillimeters;
1578     else if ( unitName.compare( QLatin1String( "Statute mile" ), Qt::CaseInsensitive ) == 0 )
1579       d->mMapUnits = QgsUnitTypes::DistanceMiles;
1580     else if ( unitName.compare( QLatin1String( "nautical mile" ), Qt::CaseInsensitive ) == 0 )
1581       d->mMapUnits = QgsUnitTypes::DistanceNauticalMiles;
1582     else if ( unitName.compare( QLatin1String( "yard" ), Qt::CaseInsensitive ) == 0 )
1583       d->mMapUnits = QgsUnitTypes::DistanceYards;
1584     // TODO - maybe more values to handle here?
1585     else
1586       d->mMapUnits = QgsUnitTypes::DistanceUnknownUnit;
1587     return;
1588   }
1589   else
1590   {
1591     d->mMapUnits = QgsUnitTypes::DistanceUnknownUnit;
1592     return;
1593   }
1594 }
1595 
1596 
findMatchingProj()1597 long QgsCoordinateReferenceSystem::findMatchingProj()
1598 {
1599   if ( d->mEllipsoidAcronym.isNull() || d->mProjectionAcronym.isNull()
1600        || !d->mIsValid )
1601   {
1602     QgsDebugMsgLevel( "QgsCoordinateReferenceSystem::findMatchingProj will only "
1603                       "work if prj acr ellipsoid acr and proj4string are set"
1604                       " and the current projection is valid!", 4 );
1605     return 0;
1606   }
1607 
1608   sqlite3_database_unique_ptr database;
1609   sqlite3_statement_unique_ptr statement;
1610   int myResult;
1611 
1612   // Set up the query to retrieve the projection information
1613   // needed to populate the list
1614   QString mySql = QString( "select srs_id,parameters from tbl_srs where "
1615                            "projection_acronym=%1 and ellipsoid_acronym=%2 order by deprecated" )
1616                   .arg( QgsSqliteUtils::quotedString( d->mProjectionAcronym ),
1617                         QgsSqliteUtils::quotedString( d->mEllipsoidAcronym ) );
1618   // Get the full path name to the sqlite3 spatial reference database.
1619   QString myDatabaseFileName = QgsApplication::srsDatabaseFilePath();
1620 
1621   //check the db is available
1622   myResult = openDatabase( myDatabaseFileName, database );
1623   if ( myResult != SQLITE_OK )
1624   {
1625     return 0;
1626   }
1627 
1628   statement = database.prepare( mySql, myResult );
1629   if ( myResult == SQLITE_OK )
1630   {
1631 
1632     while ( statement.step() == SQLITE_ROW )
1633     {
1634       QString mySrsId = statement.columnAsText( 0 );
1635       QString myProj4String = statement.columnAsText( 1 );
1636       if ( toProj() == myProj4String.trimmed() )
1637       {
1638         return mySrsId.toLong();
1639       }
1640     }
1641   }
1642 
1643   //
1644   // Try the users db now
1645   //
1646 
1647   myDatabaseFileName = QgsApplication::qgisUserDatabaseFilePath();
1648   //check the db is available
1649   myResult = openDatabase( myDatabaseFileName, database );
1650   if ( myResult != SQLITE_OK )
1651   {
1652     return 0;
1653   }
1654 
1655   statement = database.prepare( mySql, myResult );
1656 
1657   if ( myResult == SQLITE_OK )
1658   {
1659     while ( statement.step() == SQLITE_ROW )
1660     {
1661       QString mySrsId = statement.columnAsText( 0 );
1662       QString myProj4String = statement.columnAsText( 1 );
1663       if ( toProj() == myProj4String.trimmed() )
1664       {
1665         return mySrsId.toLong();
1666       }
1667     }
1668   }
1669 
1670   return 0;
1671 }
1672 
operator ==(const QgsCoordinateReferenceSystem & srs) const1673 bool QgsCoordinateReferenceSystem::operator==( const QgsCoordinateReferenceSystem &srs ) const
1674 {
1675   // shortcut
1676   if ( d == srs.d )
1677     return true;
1678 
1679   if ( !d->mIsValid && !srs.d->mIsValid )
1680     return true;
1681 
1682   if ( !d->mIsValid || !srs.d->mIsValid )
1683     return false;
1684 
1685   if ( !qgsNanCompatibleEquals( d->mCoordinateEpoch, srs.d->mCoordinateEpoch ) )
1686     return false;
1687 
1688   const bool isUser = d->mSrsId >= USER_CRS_START_ID;
1689   const bool otherIsUser = srs.d->mSrsId >= USER_CRS_START_ID;
1690   if ( isUser != otherIsUser )
1691     return false;
1692 
1693   // we can't directly compare authid for user crses -- the actual definition of these may have changed
1694   if ( !isUser && ( !d->mAuthId.isEmpty() || !srs.d->mAuthId.isEmpty() ) )
1695     return d->mAuthId == srs.d->mAuthId;
1696 
1697   return toWkt( WKT_PREFERRED ) == srs.toWkt( WKT_PREFERRED );
1698 }
1699 
operator !=(const QgsCoordinateReferenceSystem & srs) const1700 bool QgsCoordinateReferenceSystem::operator!=( const QgsCoordinateReferenceSystem &srs ) const
1701 {
1702   return  !( *this == srs );
1703 }
1704 
toWkt(WktVariant variant,bool multiline,int indentationWidth) const1705 QString QgsCoordinateReferenceSystem::toWkt( WktVariant variant, bool multiline, int indentationWidth ) const
1706 {
1707   if ( PJ *obj = d->threadLocalProjObject() )
1708   {
1709     const bool isDefaultPreferredFormat = variant == WKT_PREFERRED && !multiline;
1710     if ( isDefaultPreferredFormat && !d->mWktPreferred.isEmpty() )
1711     {
1712       // can use cached value
1713       return d->mWktPreferred;
1714     }
1715 
1716     PJ_WKT_TYPE type = PJ_WKT1_GDAL;
1717     switch ( variant )
1718     {
1719       case WKT1_GDAL:
1720         type = PJ_WKT1_GDAL;
1721         break;
1722       case WKT1_ESRI:
1723         type = PJ_WKT1_ESRI;
1724         break;
1725       case WKT2_2015:
1726         type = PJ_WKT2_2015;
1727         break;
1728       case WKT2_2015_SIMPLIFIED:
1729         type = PJ_WKT2_2015_SIMPLIFIED;
1730         break;
1731       case WKT2_2019:
1732         type = PJ_WKT2_2019;
1733         break;
1734       case WKT2_2019_SIMPLIFIED:
1735         type = PJ_WKT2_2019_SIMPLIFIED;
1736         break;
1737     }
1738 
1739     const QByteArray multiLineOption = QStringLiteral( "MULTILINE=%1" ).arg( multiline ? QStringLiteral( "YES" ) : QStringLiteral( "NO" ) ).toLocal8Bit();
1740     const QByteArray indentatationWidthOption = QStringLiteral( "INDENTATION_WIDTH=%1" ).arg( multiline ? QString::number( indentationWidth ) : QStringLiteral( "0" ) ).toLocal8Bit();
1741     const char *const options[] = {multiLineOption.constData(), indentatationWidthOption.constData(), nullptr};
1742     QString res = proj_as_wkt( QgsProjContext::get(), obj, type, options );
1743 
1744     if ( isDefaultPreferredFormat )
1745     {
1746       // cache result for later use
1747       d->mWktPreferred = res;
1748     }
1749 
1750     return res;
1751   }
1752   return QString();
1753 }
1754 
readXml(const QDomNode & node)1755 bool QgsCoordinateReferenceSystem::readXml( const QDomNode &node )
1756 {
1757   d.detach();
1758   bool result = true;
1759   QDomNode srsNode = node.namedItem( QStringLiteral( "spatialrefsys" ) );
1760 
1761   if ( ! srsNode.isNull() )
1762   {
1763     bool initialized = false;
1764 
1765     bool ok = false;
1766     long srsid = srsNode.namedItem( QStringLiteral( "srsid" ) ).toElement().text().toLong( &ok );
1767 
1768     QDomNode node;
1769 
1770     if ( ok && srsid > 0 && srsid < USER_CRS_START_ID )
1771     {
1772       node = srsNode.namedItem( QStringLiteral( "authid" ) );
1773       if ( !node.isNull() )
1774       {
1775         createFromOgcWmsCrs( node.toElement().text() );
1776         if ( isValid() )
1777         {
1778           initialized = true;
1779         }
1780       }
1781 
1782       if ( !initialized )
1783       {
1784         node = srsNode.namedItem( QStringLiteral( "epsg" ) );
1785         if ( !node.isNull() )
1786         {
1787           operator=( QgsCoordinateReferenceSystem::fromEpsgId( node.toElement().text().toLong() ) );
1788           if ( isValid() )
1789           {
1790             initialized = true;
1791           }
1792         }
1793       }
1794     }
1795 
1796     // if wkt is present, prefer that since it's lossless (unlike proj4 strings)
1797     if ( !initialized )
1798     {
1799       // before doing anything, we grab and set the stored CRS name (description).
1800       // this way if the stored CRS doesn't match anything available locally (i.e. from Proj's db
1801       // or the user's custom CRS list), then we will correctly show the CRS with its original
1802       // name (instead of just "custom crs")
1803       const QString description = srsNode.namedItem( QStringLiteral( "description" ) ).toElement().text();
1804 
1805       const QString wkt = srsNode.namedItem( QStringLiteral( "wkt" ) ).toElement().text();
1806       initialized = createFromWktInternal( wkt, description );
1807     }
1808 
1809     if ( !initialized )
1810     {
1811       node = srsNode.namedItem( QStringLiteral( "proj4" ) );
1812       const QString proj4 = node.toElement().text();
1813       initialized = createFromProj( proj4 );
1814     }
1815 
1816     if ( !initialized )
1817     {
1818       // Setting from elements one by one
1819       node = srsNode.namedItem( QStringLiteral( "proj4" ) );
1820       const QString proj4 = node.toElement().text();
1821       if ( !proj4.trimmed().isEmpty() )
1822         setProjString( node.toElement().text() );
1823 
1824       node = srsNode.namedItem( QStringLiteral( "srsid" ) );
1825       d->mSrsId = node.toElement().text().toLong();
1826 
1827       node = srsNode.namedItem( QStringLiteral( "srid" ) );
1828       d->mSRID = node.toElement().text().toLong();
1829 
1830       node = srsNode.namedItem( QStringLiteral( "authid" ) );
1831       d->mAuthId = node.toElement().text();
1832 
1833       node = srsNode.namedItem( QStringLiteral( "description" ) );
1834       d->mDescription = node.toElement().text();
1835 
1836       node = srsNode.namedItem( QStringLiteral( "projectionacronym" ) );
1837       d->mProjectionAcronym = node.toElement().text();
1838 
1839       node = srsNode.namedItem( QStringLiteral( "ellipsoidacronym" ) );
1840       d->mEllipsoidAcronym = node.toElement().text();
1841 
1842       node = srsNode.namedItem( QStringLiteral( "geographicflag" ) );
1843       d->mIsGeographic = node.toElement().text() == QLatin1String( "true" );
1844 
1845       d->mWktPreferred.clear();
1846 
1847       //make sure the map units have been set
1848       setMapUnits();
1849     }
1850 
1851     const QString epoch = srsNode.toElement().attribute( QStringLiteral( "coordinateEpoch" ) );
1852     if ( !epoch.isEmpty() )
1853     {
1854       bool epochOk = false;
1855       d->mCoordinateEpoch = epoch.toDouble( &epochOk );
1856       if ( !epochOk )
1857         d->mCoordinateEpoch = std::numeric_limits< double >::quiet_NaN();
1858     }
1859     else
1860     {
1861       d->mCoordinateEpoch = std::numeric_limits< double >::quiet_NaN();
1862     }
1863   }
1864   else
1865   {
1866     // Return empty CRS if none was found in the XML.
1867     d = new QgsCoordinateReferenceSystemPrivate();
1868     result = false;
1869   }
1870   return result;
1871 }
1872 
writeXml(QDomNode & node,QDomDocument & doc) const1873 bool QgsCoordinateReferenceSystem::writeXml( QDomNode &node, QDomDocument &doc ) const
1874 {
1875   QDomElement layerNode = node.toElement();
1876   QDomElement srsElement = doc.createElement( QStringLiteral( "spatialrefsys" ) );
1877 
1878   if ( std::isfinite( d->mCoordinateEpoch ) )
1879   {
1880     srsElement.setAttribute( QStringLiteral( "coordinateEpoch" ), d->mCoordinateEpoch );
1881   }
1882 
1883   QDomElement wktElement = doc.createElement( QStringLiteral( "wkt" ) );
1884   wktElement.appendChild( doc.createTextNode( toWkt( WKT_PREFERRED ) ) );
1885   srsElement.appendChild( wktElement );
1886 
1887   QDomElement proj4Element = doc.createElement( QStringLiteral( "proj4" ) );
1888   proj4Element.appendChild( doc.createTextNode( toProj() ) );
1889   srsElement.appendChild( proj4Element );
1890 
1891   QDomElement srsIdElement = doc.createElement( QStringLiteral( "srsid" ) );
1892   srsIdElement.appendChild( doc.createTextNode( QString::number( srsid() ) ) );
1893   srsElement.appendChild( srsIdElement );
1894 
1895   QDomElement sridElement = doc.createElement( QStringLiteral( "srid" ) );
1896   sridElement.appendChild( doc.createTextNode( QString::number( postgisSrid() ) ) );
1897   srsElement.appendChild( sridElement );
1898 
1899   QDomElement authidElement = doc.createElement( QStringLiteral( "authid" ) );
1900   authidElement.appendChild( doc.createTextNode( authid() ) );
1901   srsElement.appendChild( authidElement );
1902 
1903   QDomElement descriptionElement = doc.createElement( QStringLiteral( "description" ) );
1904   descriptionElement.appendChild( doc.createTextNode( description() ) );
1905   srsElement.appendChild( descriptionElement );
1906 
1907   QDomElement projectionAcronymElement = doc.createElement( QStringLiteral( "projectionacronym" ) );
1908   projectionAcronymElement.appendChild( doc.createTextNode( projectionAcronym() ) );
1909   srsElement.appendChild( projectionAcronymElement );
1910 
1911   QDomElement ellipsoidAcronymElement = doc.createElement( QStringLiteral( "ellipsoidacronym" ) );
1912   ellipsoidAcronymElement.appendChild( doc.createTextNode( ellipsoidAcronym() ) );
1913   srsElement.appendChild( ellipsoidAcronymElement );
1914 
1915   QDomElement geographicFlagElement = doc.createElement( QStringLiteral( "geographicflag" ) );
1916   QString geoFlagText = QStringLiteral( "false" );
1917   if ( isGeographic() )
1918   {
1919     geoFlagText = QStringLiteral( "true" );
1920   }
1921 
1922   geographicFlagElement.appendChild( doc.createTextNode( geoFlagText ) );
1923   srsElement.appendChild( geographicFlagElement );
1924 
1925   layerNode.appendChild( srsElement );
1926 
1927   return true;
1928 }
1929 
1930 //
1931 // Static helper methods below this point only please!
1932 //
1933 
1934 
1935 // Returns the whole proj4 string for the selected srsid
1936 //this is a static method! NOTE I've made it private for now to reduce API clutter TS
projFromSrsId(const int srsId)1937 QString QgsCoordinateReferenceSystem::projFromSrsId( const int srsId )
1938 {
1939   QString myDatabaseFileName;
1940   QString myProjString;
1941   QString mySql = QStringLiteral( "select parameters from tbl_srs where srs_id = %1 order by deprecated" ).arg( srsId );
1942 
1943   //
1944   // Determine if this is a user projection or a system on
1945   // user projection defs all have srs_id >= 100000
1946   //
1947   if ( srsId >= USER_CRS_START_ID )
1948   {
1949     myDatabaseFileName = QgsApplication::qgisUserDatabaseFilePath();
1950     QFileInfo myFileInfo;
1951     myFileInfo.setFile( myDatabaseFileName );
1952     if ( !myFileInfo.exists() ) //its unlikely that this condition will ever be reached
1953     {
1954       QgsDebugMsg( QStringLiteral( "users qgis.db not found" ) );
1955       return QString();
1956     }
1957   }
1958   else //must be  a system projection then
1959   {
1960     myDatabaseFileName = QgsApplication::srsDatabaseFilePath();
1961   }
1962 
1963   sqlite3_database_unique_ptr database;
1964   sqlite3_statement_unique_ptr statement;
1965 
1966   int rc;
1967   rc = openDatabase( myDatabaseFileName, database );
1968   if ( rc )
1969   {
1970     return QString();
1971   }
1972 
1973   statement = database.prepare( mySql, rc );
1974 
1975   if ( rc == SQLITE_OK )
1976   {
1977     if ( statement.step() == SQLITE_ROW )
1978     {
1979       myProjString = statement.columnAsText( 0 );
1980     }
1981   }
1982 
1983   return myProjString;
1984 }
1985 
openDatabase(const QString & path,sqlite3_database_unique_ptr & database,bool readonly)1986 int QgsCoordinateReferenceSystem::openDatabase( const QString &path, sqlite3_database_unique_ptr &database, bool readonly )
1987 {
1988   int myResult;
1989   if ( readonly )
1990     myResult = database.open_v2( path, SQLITE_OPEN_READONLY, nullptr );
1991   else
1992     myResult = database.open( path );
1993 
1994   if ( myResult != SQLITE_OK )
1995   {
1996     QgsDebugMsg( "Can't open database: " + database.errorMessage() );
1997     // XXX This will likely never happen since on open, sqlite creates the
1998     //     database if it does not exist.
1999     // ... unfortunately it happens on Windows
2000     QgsMessageLog::logMessage( QObject::tr( "Could not open CRS database %1\nError(%2): %3" )
2001                                .arg( path )
2002                                .arg( myResult )
2003                                .arg( database.errorMessage() ), QObject::tr( "CRS" ) );
2004   }
2005   return myResult;
2006 }
2007 
setCustomCrsValidation(CUSTOM_CRS_VALIDATION f)2008 void QgsCoordinateReferenceSystem::setCustomCrsValidation( CUSTOM_CRS_VALIDATION f )
2009 {
2010   sCustomSrsValidation = f;
2011 }
2012 
customCrsValidation()2013 CUSTOM_CRS_VALIDATION QgsCoordinateReferenceSystem::customCrsValidation()
2014 {
2015   return sCustomSrsValidation;
2016 }
2017 
debugPrint()2018 void QgsCoordinateReferenceSystem::debugPrint()
2019 {
2020   QgsDebugMsg( QStringLiteral( "***SpatialRefSystem***" ) );
2021   QgsDebugMsg( "* Valid : " + ( d->mIsValid ? QString( "true" ) : QString( "false" ) ) );
2022   QgsDebugMsg( "* SrsId : " + QString::number( d->mSrsId ) );
2023   QgsDebugMsg( "* Proj4 : " + toProj() );
2024   QgsDebugMsg( "* WKT   : " + toWkt( WKT_PREFERRED ) );
2025   QgsDebugMsg( "* Desc. : " + d->mDescription );
2026   if ( mapUnits() == QgsUnitTypes::DistanceMeters )
2027   {
2028     QgsDebugMsg( QStringLiteral( "* Units : meters" ) );
2029   }
2030   else if ( mapUnits() == QgsUnitTypes::DistanceFeet )
2031   {
2032     QgsDebugMsg( QStringLiteral( "* Units : feet" ) );
2033   }
2034   else if ( mapUnits() == QgsUnitTypes::DistanceDegrees )
2035   {
2036     QgsDebugMsg( QStringLiteral( "* Units : degrees" ) );
2037   }
2038 }
2039 
setValidationHint(const QString & html)2040 void QgsCoordinateReferenceSystem::setValidationHint( const QString &html )
2041 {
2042   mValidationHint = html;
2043 }
2044 
validationHint()2045 QString QgsCoordinateReferenceSystem::validationHint()
2046 {
2047   return mValidationHint;
2048 }
2049 
saveAsUserCrs(const QString & name,Format nativeFormat)2050 long QgsCoordinateReferenceSystem::saveAsUserCrs( const QString &name, Format nativeFormat )
2051 {
2052   return QgsApplication::coordinateReferenceSystemRegistry()->addUserCrs( *this, name, nativeFormat );
2053 }
2054 
getRecordCount()2055 long QgsCoordinateReferenceSystem::getRecordCount()
2056 {
2057   sqlite3_database_unique_ptr database;
2058   sqlite3_statement_unique_ptr statement;
2059   int           myResult;
2060   long          myRecordCount = 0;
2061   //check the db is available
2062   myResult = database.open_v2( QgsApplication::qgisUserDatabaseFilePath(), SQLITE_OPEN_READONLY, nullptr );
2063   if ( myResult != SQLITE_OK )
2064   {
2065     QgsDebugMsg( QStringLiteral( "Can't open database: %1" ).arg( database.errorMessage() ) );
2066     return 0;
2067   }
2068   // Set up the query to retrieve the projection information needed to populate the ELLIPSOID list
2069   QString mySql = QStringLiteral( "select count(*) from tbl_srs" );
2070   statement = database.prepare( mySql, myResult );
2071   if ( myResult == SQLITE_OK )
2072   {
2073     if ( statement.step() == SQLITE_ROW )
2074     {
2075       QString myRecordCountString = statement.columnAsText( 0 );
2076       myRecordCount = myRecordCountString.toLong();
2077     }
2078   }
2079   return myRecordCount;
2080 }
2081 
testIsGeographic(PJ * crs)2082 bool testIsGeographic( PJ *crs )
2083 {
2084   PJ_CONTEXT *pjContext = QgsProjContext::get();
2085   bool isGeographic = false;
2086   QgsProjUtils::proj_pj_unique_ptr coordinateSystem( proj_crs_get_coordinate_system( pjContext, crs ) );
2087   if ( coordinateSystem )
2088   {
2089     const int axisCount = proj_cs_get_axis_count( pjContext, coordinateSystem.get() );
2090     if ( axisCount > 0 )
2091     {
2092       const char *outUnitAuthName = nullptr;
2093       const char *outUnitAuthCode = nullptr;
2094       // Read only first axis
2095       proj_cs_get_axis_info( pjContext, coordinateSystem.get(), 0,
2096                              nullptr,
2097                              nullptr,
2098                              nullptr,
2099                              nullptr,
2100                              nullptr,
2101                              &outUnitAuthName,
2102                              &outUnitAuthCode );
2103 
2104       if ( outUnitAuthName && outUnitAuthCode )
2105       {
2106         const char *unitCategory = nullptr;
2107         if ( proj_uom_get_info_from_database( pjContext, outUnitAuthName, outUnitAuthCode, nullptr, nullptr, &unitCategory ) )
2108         {
2109           isGeographic = QString( unitCategory ).compare( QLatin1String( "angular" ), Qt::CaseInsensitive ) == 0;
2110         }
2111       }
2112     }
2113   }
2114   return isGeographic;
2115 }
2116 
getOperationAndEllipsoidFromProjString(const QString & proj,QString & operation,QString & ellipsoid)2117 void getOperationAndEllipsoidFromProjString( const QString &proj, QString &operation, QString &ellipsoid )
2118 {
2119   thread_local const QRegularExpression projRegExp( QStringLiteral( "\\+proj=(\\S+)" ) );
2120   const QRegularExpressionMatch projMatch = projRegExp.match( proj );
2121   if ( !projMatch.hasMatch() )
2122   {
2123     QgsDebugMsgLevel( QStringLiteral( "no +proj argument found [%2]" ).arg( proj ), 2 );
2124     return;
2125   }
2126   operation = projMatch.captured( 1 );
2127 
2128   thread_local const QRegularExpression ellipseRegExp( QStringLiteral( "\\+(?:ellps|datum)=(\\S+)" ) );
2129   const QRegularExpressionMatch ellipseMatch = projRegExp.match( proj );
2130   if ( ellipseMatch.hasMatch() )
2131   {
2132     ellipsoid = ellipseMatch.captured( 1 );
2133   }
2134   else
2135   {
2136     // satisfy not null constraint on ellipsoid_acronym field
2137     // possibly we should drop the constraint, yet the crses with missing ellipsoid_acronym are malformed
2138     // and will result in oddities within other areas of QGIS (e.g. project ellipsoid won't be correctly
2139     // set for these CRSes). Better just hack around and make the constraint happy for now,
2140     // and hope that the definitions get corrected in future.
2141     ellipsoid = "";
2142   }
2143 }
2144 
2145 
loadFromAuthCode(const QString & auth,const QString & code)2146 bool QgsCoordinateReferenceSystem::loadFromAuthCode( const QString &auth, const QString &code )
2147 {
2148   d.detach();
2149   d->mIsValid = false;
2150   d->mWktPreferred.clear();
2151 
2152   PJ_CONTEXT *pjContext = QgsProjContext::get();
2153   QgsProjUtils::proj_pj_unique_ptr crs( proj_create_from_database( pjContext, auth.toUtf8().constData(), code.toUtf8().constData(), PJ_CATEGORY_CRS, false, nullptr ) );
2154   if ( !crs )
2155   {
2156     return false;
2157   }
2158 
2159   switch ( proj_get_type( crs.get() ) )
2160   {
2161     case PJ_TYPE_VERTICAL_CRS:
2162       return false;
2163 
2164     default:
2165       break;
2166   }
2167 
2168   crs = QgsProjUtils::crsToSingleCrs( crs.get() );
2169 
2170   QString proj4 = getFullProjString( crs.get() );
2171   proj4.replace( QLatin1String( "+type=crs" ), QString() );
2172   proj4 = proj4.trimmed();
2173 
2174   d->mIsValid = true;
2175   d->mProj4 = proj4;
2176   d->mWktPreferred.clear();
2177   d->mDescription = QString( proj_get_name( crs.get() ) );
2178   d->mAuthId = QStringLiteral( "%1:%2" ).arg( auth, code );
2179   d->mIsGeographic = testIsGeographic( crs.get() );
2180   d->mAxisInvertedDirty = true;
2181   QString operation;
2182   QString ellipsoid;
2183   getOperationAndEllipsoidFromProjString( proj4, operation, ellipsoid );
2184   d->mProjectionAcronym = operation;
2185   d->mEllipsoidAcronym.clear();
2186   d->setPj( std::move( crs ) );
2187 
2188   const QString dbVals = sAuthIdToQgisSrsIdMap.value( QStringLiteral( "%1:%2" ).arg( auth, code ).toUpper() );
2189   QString srsId;
2190   QString srId;
2191   if ( !dbVals.isEmpty() )
2192   {
2193     const QStringList parts = dbVals.split( ',' );
2194     d->mSrsId = parts.at( 0 ).toInt();
2195     d->mSRID = parts.at( 1 ).toInt();
2196   }
2197 
2198   setMapUnits();
2199 
2200   return true;
2201 }
2202 
userSrsIds()2203 QList<long> QgsCoordinateReferenceSystem::userSrsIds()
2204 {
2205   QList<long> results;
2206   // check user defined projection database
2207   const QString db = QgsApplication::qgisUserDatabaseFilePath();
2208 
2209   QFileInfo myInfo( db );
2210   if ( !myInfo.exists() )
2211   {
2212     QgsDebugMsg( "failed : " + db + " does not exist!" );
2213     return results;
2214   }
2215 
2216   sqlite3_database_unique_ptr database;
2217   sqlite3_statement_unique_ptr statement;
2218 
2219   //check the db is available
2220   int result = openDatabase( db, database );
2221   if ( result != SQLITE_OK )
2222   {
2223     QgsDebugMsg( "failed : " + db + " could not be opened!" );
2224     return results;
2225   }
2226 
2227   QString sql = QStringLiteral( "select srs_id from tbl_srs where srs_id >= %1" ).arg( USER_CRS_START_ID );
2228   int rc;
2229   statement = database.prepare( sql, rc );
2230   while ( true )
2231   {
2232     int ret = statement.step();
2233 
2234     if ( ret == SQLITE_DONE )
2235     {
2236       // there are no more rows to fetch - we can stop looping
2237       break;
2238     }
2239 
2240     if ( ret == SQLITE_ROW )
2241     {
2242       results.append( statement.columnAsInt64( 0 ) );
2243     }
2244     else
2245     {
2246       QgsMessageLog::logMessage( QObject::tr( "SQLite error: %2\nSQL: %1" ).arg( sql, sqlite3_errmsg( database.get() ) ), QObject::tr( "SpatiaLite" ) );
2247       break;
2248     }
2249   }
2250 
2251   return results;
2252 }
2253 
matchToUserCrs() const2254 long QgsCoordinateReferenceSystem::matchToUserCrs() const
2255 {
2256   PJ *obj = d->threadLocalProjObject();
2257   if ( !obj )
2258     return 0;
2259 
2260   const QList< long > ids = userSrsIds();
2261   for ( long id : ids )
2262   {
2263     QgsCoordinateReferenceSystem candidate = QgsCoordinateReferenceSystem::fromSrsId( id );
2264     if ( candidate.projObject() && proj_is_equivalent_to( obj, candidate.projObject(), PJ_COMP_EQUIVALENT ) )
2265     {
2266       return id;
2267     }
2268   }
2269   return 0;
2270 }
2271 
sync_db_proj_logger(void *,int level,const char * message)2272 static void sync_db_proj_logger( void * /* user_data */, int level, const char *message )
2273 {
2274 #ifndef QGISDEBUG
2275   Q_UNUSED( message )
2276 #endif
2277   if ( level == PJ_LOG_ERROR )
2278   {
2279     QgsDebugMsgLevel( QStringLiteral( "PROJ: %1" ).arg( message ), 2 );
2280   }
2281   else if ( level == PJ_LOG_DEBUG )
2282   {
2283     QgsDebugMsgLevel( QStringLiteral( "PROJ: %1" ).arg( message ), 3 );
2284   }
2285 }
2286 
syncDatabase()2287 int QgsCoordinateReferenceSystem::syncDatabase()
2288 {
2289   setlocale( LC_ALL, "C" );
2290   QString dbFilePath = QgsApplication::srsDatabaseFilePath();
2291 
2292   int inserted = 0, updated = 0, deleted = 0, errors = 0;
2293 
2294   QgsDebugMsgLevel( QStringLiteral( "Load srs db from: %1" ).arg( QgsApplication::srsDatabaseFilePath().toLocal8Bit().constData() ), 4 );
2295 
2296   sqlite3_database_unique_ptr database;
2297   if ( database.open( dbFilePath ) != SQLITE_OK )
2298   {
2299     QgsDebugMsg( QStringLiteral( "Could not open database: %1 (%2)\n" ).arg( QgsApplication::srsDatabaseFilePath(), database.errorMessage() ) );
2300     return -1;
2301   }
2302 
2303   if ( sqlite3_exec( database.get(), "BEGIN TRANSACTION", nullptr, nullptr, nullptr ) != SQLITE_OK )
2304   {
2305     QgsDebugMsg( QStringLiteral( "Could not begin transaction: %1 (%2)\n" ).arg( QgsApplication::srsDatabaseFilePath(), database.errorMessage() ) );
2306     return -1;
2307   }
2308 
2309   sqlite3_statement_unique_ptr statement;
2310   int result;
2311   char *errMsg = nullptr;
2312 
2313   if ( sqlite3_exec( database.get(), "create table tbl_info (proj_major INT, proj_minor INT, proj_patch INT)", nullptr, nullptr, nullptr ) == SQLITE_OK )
2314   {
2315     QString sql = QStringLiteral( "INSERT INTO tbl_info(proj_major, proj_minor, proj_patch) VALUES (%1, %2,%3)" )
2316                   .arg( QString::number( PROJ_VERSION_MAJOR ),
2317                         QString::number( PROJ_VERSION_MINOR ),
2318                         QString::number( PROJ_VERSION_PATCH ) );
2319     if ( sqlite3_exec( database.get(), sql.toUtf8(), nullptr, nullptr, &errMsg ) != SQLITE_OK )
2320     {
2321       QgsDebugMsg( QStringLiteral( "Could not execute: %1 [%2/%3]\n" ).arg(
2322                      sql,
2323                      database.errorMessage(),
2324                      errMsg ? errMsg : "(unknown error)" ) );
2325       if ( errMsg )
2326         sqlite3_free( errMsg );
2327       return -1;
2328     }
2329   }
2330   else
2331   {
2332     // retrieve last update details
2333     QString sql = QStringLiteral( "SELECT proj_major, proj_minor, proj_patch FROM tbl_info" );
2334     statement = database.prepare( sql, result );
2335     if ( result != SQLITE_OK )
2336     {
2337       QgsDebugMsg( QStringLiteral( "Could not prepare: %1 [%2]\n" ).arg( sql, database.errorMessage() ) );
2338       return -1;
2339     }
2340     if ( statement.step() == SQLITE_ROW )
2341     {
2342       int major = statement.columnAsInt64( 0 );
2343       int minor = statement.columnAsInt64( 1 );
2344       int patch = statement.columnAsInt64( 2 );
2345       if ( major == PROJ_VERSION_MAJOR && minor == PROJ_VERSION_MINOR && patch == PROJ_VERSION_PATCH )
2346         // yay, nothing to do!
2347         return 0;
2348     }
2349     else
2350     {
2351       QgsDebugMsg( QStringLiteral( "Could not retrieve previous CRS sync PROJ version number" ) );
2352       return -1;
2353     }
2354   }
2355 
2356   PJ_CONTEXT *pjContext = QgsProjContext::get();
2357   // silence proj warnings
2358   proj_log_func( pjContext, nullptr, sync_db_proj_logger );
2359 
2360   PROJ_STRING_LIST authorities = proj_get_authorities_from_database( pjContext );
2361 
2362   int nextSrsId = 63561;
2363   int nextSrId = 520003561;
2364   for ( auto authIter = authorities; authIter && *authIter; ++authIter )
2365   {
2366     const QString authority( *authIter );
2367     QgsDebugMsgLevel( QStringLiteral( "Loading authority '%1'" ).arg( authority ), 2 );
2368     PROJ_STRING_LIST codes = proj_get_codes_from_database( pjContext, *authIter, PJ_TYPE_CRS, true );
2369 
2370     QStringList allCodes;
2371 
2372     for ( auto codesIter = codes; codesIter && *codesIter; ++codesIter )
2373     {
2374       const QString code( *codesIter );
2375       allCodes << QgsSqliteUtils::quotedString( code );
2376       QgsDebugMsgLevel( QStringLiteral( "Loading code '%1'" ).arg( code ), 4 );
2377       QgsProjUtils::proj_pj_unique_ptr crs( proj_create_from_database( pjContext, *authIter, *codesIter, PJ_CATEGORY_CRS, false, nullptr ) );
2378       if ( !crs )
2379       {
2380         QgsDebugMsg( QStringLiteral( "Could not load '%1:%2'" ).arg( authority, code ) );
2381         continue;
2382       }
2383 
2384       switch ( proj_get_type( crs.get() ) )
2385       {
2386         case PJ_TYPE_VERTICAL_CRS: // don't need these in the CRS db
2387           continue;
2388 
2389         default:
2390           break;
2391       }
2392 
2393       crs = QgsProjUtils::crsToSingleCrs( crs.get() );
2394 
2395       QString proj4 = getFullProjString( crs.get() );
2396       proj4.replace( QLatin1String( "+type=crs" ), QString() );
2397       proj4 = proj4.trimmed();
2398 
2399       if ( proj4.isEmpty() )
2400       {
2401         QgsDebugMsgLevel( QStringLiteral( "No proj4 for '%1:%2'" ).arg( authority, code ), 2 );
2402         // satisfy not null constraint
2403         proj4 = "";
2404       }
2405 
2406       const bool deprecated = proj_is_deprecated( crs.get() );
2407       const QString name( proj_get_name( crs.get() ) );
2408 
2409       QString sql = QStringLiteral( "SELECT parameters,description,deprecated FROM tbl_srs WHERE auth_name='%1' AND auth_id='%2'" ).arg( authority, code );
2410       statement = database.prepare( sql, result );
2411       if ( result != SQLITE_OK )
2412       {
2413         QgsDebugMsg( QStringLiteral( "Could not prepare: %1 [%2]\n" ).arg( sql, database.errorMessage() ) );
2414         continue;
2415       }
2416 
2417       QString srsProj4;
2418       QString srsDesc;
2419       bool srsDeprecated = deprecated;
2420       if ( statement.step() == SQLITE_ROW )
2421       {
2422         srsProj4 = statement.columnAsText( 0 );
2423         srsDesc = statement.columnAsText( 1 );
2424         srsDeprecated = statement.columnAsText( 2 ).toInt() != 0;
2425       }
2426 
2427       if ( !srsProj4.isEmpty() || !srsDesc.isEmpty() )
2428       {
2429         if ( proj4 != srsProj4 || name != srsDesc || deprecated != srsDeprecated )
2430         {
2431           errMsg = nullptr;
2432           sql = QStringLiteral( "UPDATE tbl_srs SET parameters=%1,description=%2,deprecated=%3 WHERE auth_name=%4 AND auth_id=%5" )
2433                 .arg( QgsSqliteUtils::quotedString( proj4 ) )
2434                 .arg( QgsSqliteUtils::quotedString( name ) )
2435                 .arg( deprecated ? 1 : 0 )
2436                 .arg( QgsSqliteUtils::quotedString( authority ), QgsSqliteUtils::quotedString( code ) );
2437 
2438           if ( sqlite3_exec( database.get(), sql.toUtf8(), nullptr, nullptr, &errMsg ) != SQLITE_OK )
2439           {
2440             QgsDebugMsg( QStringLiteral( "Could not execute: %1 [%2/%3]\n" ).arg(
2441                            sql,
2442                            database.errorMessage(),
2443                            errMsg ? errMsg : "(unknown error)" ) );
2444             if ( errMsg )
2445               sqlite3_free( errMsg );
2446             errors++;
2447           }
2448           else
2449           {
2450             updated++;
2451           }
2452         }
2453       }
2454       else
2455       {
2456         // there's a not-null contraint on these columns, so we must use empty strings instead
2457         QString operation = "";
2458         QString ellps = "";
2459         getOperationAndEllipsoidFromProjString( proj4, operation, ellps );
2460         const bool isGeographic = testIsGeographic( crs.get() );
2461 
2462         // work out srid and srsid
2463         const QString dbVals = sAuthIdToQgisSrsIdMap.value( QStringLiteral( "%1:%2" ).arg( authority, code ) );
2464         QString srsId;
2465         QString srId;
2466         if ( !dbVals.isEmpty() )
2467         {
2468           const QStringList parts = dbVals.split( ',' );
2469           srsId = parts.at( 0 );
2470           srId = parts.at( 1 );
2471         }
2472         if ( srId.isEmpty() )
2473         {
2474           srId = QString::number( nextSrId );
2475           nextSrId++;
2476         }
2477         if ( srsId.isEmpty() )
2478         {
2479           srsId = QString::number( nextSrsId );
2480           nextSrsId++;
2481         }
2482 
2483         if ( !srsId.isEmpty() )
2484         {
2485           sql = QStringLiteral( "INSERT INTO tbl_srs(srs_id, description,projection_acronym,ellipsoid_acronym,parameters,srid,auth_name,auth_id,is_geo,deprecated) VALUES (%1, %2,%3,%4,%5,%6,%7,%8,%9,%10)" )
2486                 .arg( srsId )
2487                 .arg( QgsSqliteUtils::quotedString( name ),
2488                       QgsSqliteUtils::quotedString( operation ),
2489                       QgsSqliteUtils::quotedString( ellps ),
2490                       QgsSqliteUtils::quotedString( proj4 ) )
2491                 .arg( srId )
2492                 .arg( QgsSqliteUtils::quotedString( authority ) )
2493                 .arg( QgsSqliteUtils::quotedString( code ) )
2494                 .arg( isGeographic ? 1 : 0 )
2495                 .arg( deprecated ? 1 : 0 );
2496         }
2497         else
2498         {
2499           sql = QStringLiteral( "INSERT INTO tbl_srs(description,projection_acronym,ellipsoid_acronym,parameters,srid,auth_name,auth_id,is_geo,deprecated) VALUES (%2,%3,%4,%5,%6,%7,%8,%9,%10)" )
2500                 .arg( QgsSqliteUtils::quotedString( name ),
2501                       QgsSqliteUtils::quotedString( operation ),
2502                       QgsSqliteUtils::quotedString( ellps ),
2503                       QgsSqliteUtils::quotedString( proj4 ) )
2504                 .arg( srId )
2505                 .arg( QgsSqliteUtils::quotedString( authority ) )
2506                 .arg( QgsSqliteUtils::quotedString( code ) )
2507                 .arg( isGeographic ? 1 : 0 )
2508                 .arg( deprecated ? 1 : 0 );
2509         }
2510 
2511         errMsg = nullptr;
2512         if ( sqlite3_exec( database.get(), sql.toUtf8(), nullptr, nullptr, &errMsg ) == SQLITE_OK )
2513         {
2514           inserted++;
2515         }
2516         else
2517         {
2518           qCritical( "Could not execute: %s [%s/%s]\n",
2519                      sql.toLocal8Bit().constData(),
2520                      sqlite3_errmsg( database.get() ),
2521                      errMsg ? errMsg : "(unknown error)" );
2522           errors++;
2523 
2524           if ( errMsg )
2525             sqlite3_free( errMsg );
2526         }
2527       }
2528     }
2529 
2530     proj_string_list_destroy( codes );
2531 
2532     const QString sql = QStringLiteral( "DELETE FROM tbl_srs WHERE auth_name='%1' AND NOT auth_id IN (%2)" ).arg( authority, allCodes.join( ',' ) );
2533     if ( sqlite3_exec( database.get(), sql.toUtf8(), nullptr, nullptr, nullptr ) == SQLITE_OK )
2534     {
2535       deleted = sqlite3_changes( database.get() );
2536     }
2537     else
2538     {
2539       errors++;
2540       qCritical( "Could not execute: %s [%s]\n",
2541                  sql.toLocal8Bit().constData(),
2542                  sqlite3_errmsg( database.get() ) );
2543     }
2544 
2545   }
2546   proj_string_list_destroy( authorities );
2547 
2548   QString sql = QStringLiteral( "UPDATE tbl_info set proj_major=%1,proj_minor=%2,proj_patch=%3" )
2549                 .arg( QString::number( PROJ_VERSION_MAJOR ),
2550                       QString::number( PROJ_VERSION_MINOR ),
2551                       QString::number( PROJ_VERSION_PATCH ) );
2552   if ( sqlite3_exec( database.get(), sql.toUtf8(), nullptr, nullptr, &errMsg ) != SQLITE_OK )
2553   {
2554     QgsDebugMsg( QStringLiteral( "Could not execute: %1 [%2/%3]\n" ).arg(
2555                    sql,
2556                    database.errorMessage(),
2557                    errMsg ? errMsg : "(unknown error)" ) );
2558     if ( errMsg )
2559       sqlite3_free( errMsg );
2560     return -1;
2561   }
2562 
2563   if ( sqlite3_exec( database.get(), "COMMIT", nullptr, nullptr, nullptr ) != SQLITE_OK )
2564   {
2565     QgsDebugMsg( QStringLiteral( "Could not commit transaction: %1 [%2]\n" ).arg(
2566                    QgsApplication::srsDatabaseFilePath(),
2567                    sqlite3_errmsg( database.get() ) )
2568                );
2569     return -1;
2570   }
2571 
2572 #ifdef QGISDEBUG
2573   QgsDebugMsgLevel( QStringLiteral( "CRS update (inserted:%1 updated:%2 deleted:%3 errors:%4)" ).arg( inserted ).arg( updated ).arg( deleted ).arg( errors ), 4 );
2574 #else
2575   Q_UNUSED( deleted )
2576 #endif
2577 
2578   if ( errors > 0 )
2579     return -errors;
2580   else
2581     return updated + inserted;
2582 }
2583 
stringCache()2584 const QHash<QString, QgsCoordinateReferenceSystem> &QgsCoordinateReferenceSystem::stringCache()
2585 {
2586   return *sStringCache();
2587 }
2588 
projCache()2589 const QHash<QString, QgsCoordinateReferenceSystem> &QgsCoordinateReferenceSystem::projCache()
2590 {
2591   return *sProj4Cache();
2592 }
2593 
ogcCache()2594 const QHash<QString, QgsCoordinateReferenceSystem> &QgsCoordinateReferenceSystem::ogcCache()
2595 {
2596   return *sOgcCache();
2597 }
2598 
wktCache()2599 const QHash<QString, QgsCoordinateReferenceSystem> &QgsCoordinateReferenceSystem::wktCache()
2600 {
2601   return *sWktCache();
2602 }
2603 
srIdCache()2604 const QHash<long, QgsCoordinateReferenceSystem> &QgsCoordinateReferenceSystem::srIdCache()
2605 {
2606   return *sSrIdCache();
2607 }
2608 
srsIdCache()2609 const QHash<long, QgsCoordinateReferenceSystem> &QgsCoordinateReferenceSystem::srsIdCache()
2610 {
2611   return *sSrsIdCache();
2612 }
2613 
geographicCrsAuthId() const2614 QString QgsCoordinateReferenceSystem::geographicCrsAuthId() const
2615 {
2616   if ( isGeographic() )
2617   {
2618     return d->mAuthId;
2619   }
2620   else if ( PJ *obj = d->threadLocalProjObject() )
2621   {
2622     QgsProjUtils::proj_pj_unique_ptr geoCrs( proj_crs_get_geodetic_crs( QgsProjContext::get(), obj ) );
2623     return geoCrs ? QStringLiteral( "%1:%2" ).arg( proj_get_id_auth_name( geoCrs.get(), 0 ), proj_get_id_code( geoCrs.get(), 0 ) ) : QString();
2624   }
2625   else
2626   {
2627     return QString();
2628   }
2629 }
2630 
projObject() const2631 PJ *QgsCoordinateReferenceSystem::projObject() const
2632 {
2633   return d->threadLocalProjObject();
2634 }
2635 
recentProjections()2636 QStringList QgsCoordinateReferenceSystem::recentProjections()
2637 {
2638   QStringList projections;
2639   const QList<QgsCoordinateReferenceSystem> res = recentCoordinateReferenceSystems();
2640   projections.reserve( res.size() );
2641   for ( const QgsCoordinateReferenceSystem &crs : res )
2642   {
2643     projections << QString::number( crs.srsid() );
2644   }
2645   return projections;
2646 }
2647 
recentCoordinateReferenceSystems()2648 QList<QgsCoordinateReferenceSystem> QgsCoordinateReferenceSystem::recentCoordinateReferenceSystems()
2649 {
2650   QList<QgsCoordinateReferenceSystem> res;
2651 
2652   // Read settings from persistent storage
2653   QgsSettings settings;
2654   QStringList projectionsProj4  = settings.value( QStringLiteral( "UI/recentProjectionsProj4" ) ).toStringList();
2655   QStringList projectionsWkt = settings.value( QStringLiteral( "UI/recentProjectionsWkt" ) ).toStringList();
2656   QStringList projectionsAuthId = settings.value( QStringLiteral( "UI/recentProjectionsAuthId" ) ).toStringList();
2657   int max = std::max( projectionsAuthId.size(), std::max( projectionsProj4.size(), projectionsWkt.size() ) );
2658   res.reserve( max );
2659   for ( int i = 0; i < max; ++i )
2660   {
2661     const QString proj = projectionsProj4.value( i );
2662     const QString wkt = projectionsWkt.value( i );
2663     const QString authid = projectionsAuthId.value( i );
2664 
2665     QgsCoordinateReferenceSystem crs;
2666     if ( !authid.isEmpty() )
2667       crs = QgsCoordinateReferenceSystem( authid );
2668     if ( !crs.isValid() && !wkt.isEmpty() )
2669       crs.createFromWkt( wkt );
2670     if ( !crs.isValid() && !proj.isEmpty() )
2671       crs.createFromProj( wkt );
2672 
2673     if ( crs.isValid() )
2674       res << crs;
2675   }
2676   return res;
2677 }
2678 
pushRecentCoordinateReferenceSystem(const QgsCoordinateReferenceSystem & crs)2679 void QgsCoordinateReferenceSystem::pushRecentCoordinateReferenceSystem( const QgsCoordinateReferenceSystem &crs )
2680 {
2681   // we only want saved and standard CRSes in the recent list
2682   if ( crs.srsid() == 0 || !crs.isValid() )
2683     return;
2684 
2685   QList<QgsCoordinateReferenceSystem> recent = recentCoordinateReferenceSystems();
2686   recent.removeAll( crs );
2687   recent.insert( 0, crs );
2688 
2689   // trim to max 30 items
2690   recent = recent.mid( 0, 30 );
2691   QStringList authids;
2692   authids.reserve( recent.size() );
2693   QStringList proj;
2694   proj.reserve( recent.size() );
2695   QStringList wkt;
2696   wkt.reserve( recent.size() );
2697   for ( const QgsCoordinateReferenceSystem &c : std::as_const( recent ) )
2698   {
2699     authids << c.authid();
2700     proj << c.toProj();
2701     wkt << c.toWkt( WKT_PREFERRED );
2702   }
2703 
2704   QgsSettings settings;
2705   settings.setValue( QStringLiteral( "UI/recentProjectionsAuthId" ), authids );
2706   settings.setValue( QStringLiteral( "UI/recentProjectionsWkt" ), wkt );
2707   settings.setValue( QStringLiteral( "UI/recentProjectionsProj4" ), proj );
2708 }
2709 
invalidateCache(bool disableCache)2710 void QgsCoordinateReferenceSystem::invalidateCache( bool disableCache )
2711 {
2712   sSrIdCacheLock()->lockForWrite();
2713   if ( !sDisableSrIdCache )
2714   {
2715     if ( disableCache )
2716       sDisableSrIdCache = true;
2717     sSrIdCache()->clear();
2718   }
2719   sSrIdCacheLock()->unlock();
2720 
2721   sOgcLock()->lockForWrite();
2722   if ( !sDisableOgcCache )
2723   {
2724     if ( disableCache )
2725       sDisableOgcCache = true;
2726     sOgcCache()->clear();
2727   }
2728   sOgcLock()->unlock();
2729 
2730   sProj4CacheLock()->lockForWrite();
2731   if ( !sDisableProjCache )
2732   {
2733     if ( disableCache )
2734       sDisableProjCache = true;
2735     sProj4Cache()->clear();
2736   }
2737   sProj4CacheLock()->unlock();
2738 
2739   sCRSWktLock()->lockForWrite();
2740   if ( !sDisableWktCache )
2741   {
2742     if ( disableCache )
2743       sDisableWktCache = true;
2744     sWktCache()->clear();
2745   }
2746   sCRSWktLock()->unlock();
2747 
2748   sCRSSrsIdLock()->lockForWrite();
2749   if ( !sDisableSrsIdCache )
2750   {
2751     if ( disableCache )
2752       sDisableSrsIdCache = true;
2753     sSrsIdCache()->clear();
2754   }
2755   sCRSSrsIdLock()->unlock();
2756 
2757   sCrsStringLock()->lockForWrite();
2758   if ( !sDisableStringCache )
2759   {
2760     if ( disableCache )
2761       sDisableStringCache = true;
2762     sStringCache()->clear();
2763   }
2764   sCrsStringLock()->unlock();
2765 }
2766 
2767 // invalid < regular < user
operator >(const QgsCoordinateReferenceSystem & c1,const QgsCoordinateReferenceSystem & c2)2768 bool operator> ( const QgsCoordinateReferenceSystem &c1, const QgsCoordinateReferenceSystem &c2 )
2769 {
2770   if ( c1.d == c2.d )
2771     return false;
2772 
2773   if ( !c1.d->mIsValid && !c2.d->mIsValid )
2774     return false;
2775 
2776   if ( !c1.d->mIsValid && c2.d->mIsValid )
2777     return false;
2778 
2779   if ( c1.d->mIsValid && !c2.d->mIsValid )
2780     return true;
2781 
2782   const bool c1IsUser = c1.d->mSrsId >= USER_CRS_START_ID;
2783   const bool c2IsUser = c2.d->mSrsId >= USER_CRS_START_ID;
2784 
2785   if ( c1IsUser && !c2IsUser )
2786     return true;
2787 
2788   if ( !c1IsUser && c2IsUser )
2789     return false;
2790 
2791   if ( !c1IsUser && !c2IsUser )
2792   {
2793     if ( c1.d->mAuthId != c2.d->mAuthId )
2794       return c1.d->mAuthId > c2.d->mAuthId;
2795   }
2796   else
2797   {
2798     const QString wkt1 = c1.toWkt( QgsCoordinateReferenceSystem::WKT_PREFERRED );
2799     const QString wkt2 = c2.toWkt( QgsCoordinateReferenceSystem::WKT_PREFERRED );
2800     if ( wkt1 != wkt2 )
2801       return wkt1 > wkt2;
2802   }
2803 
2804   if ( c1.d->mCoordinateEpoch == c2.d->mCoordinateEpoch )
2805     return false;
2806 
2807   if ( std::isnan( c1.d->mCoordinateEpoch ) && std::isnan( c2.d->mCoordinateEpoch ) )
2808     return false;
2809 
2810   if ( std::isnan( c1.d->mCoordinateEpoch ) && !std::isnan( c2.d->mCoordinateEpoch ) )
2811     return false;
2812 
2813   if ( !std::isnan( c1.d->mCoordinateEpoch ) && std::isnan( c2.d->mCoordinateEpoch ) )
2814     return true;
2815 
2816   return c1.d->mCoordinateEpoch > c2.d->mCoordinateEpoch;
2817 }
2818 
operator <(const QgsCoordinateReferenceSystem & c1,const QgsCoordinateReferenceSystem & c2)2819 bool operator< ( const QgsCoordinateReferenceSystem &c1, const QgsCoordinateReferenceSystem &c2 )
2820 {
2821   if ( c1.d == c2.d )
2822     return false;
2823 
2824   if ( !c1.d->mIsValid && !c2.d->mIsValid )
2825     return false;
2826 
2827   if ( c1.d->mIsValid && !c2.d->mIsValid )
2828     return false;
2829 
2830   if ( !c1.d->mIsValid && c2.d->mIsValid )
2831     return true;
2832 
2833   const bool c1IsUser = c1.d->mSrsId >= USER_CRS_START_ID;
2834   const bool c2IsUser = c2.d->mSrsId >= USER_CRS_START_ID;
2835 
2836   if ( !c1IsUser && c2IsUser )
2837     return true;
2838 
2839   if ( c1IsUser && !c2IsUser )
2840     return false;
2841 
2842   if ( !c1IsUser && !c2IsUser )
2843   {
2844     if ( c1.d->mAuthId != c2.d->mAuthId )
2845       return c1.d->mAuthId < c2.d->mAuthId;
2846   }
2847   else
2848   {
2849     const QString wkt1 = c1.toWkt( QgsCoordinateReferenceSystem::WKT_PREFERRED );
2850     const QString wkt2 = c2.toWkt( QgsCoordinateReferenceSystem::WKT_PREFERRED );
2851     if ( wkt1 != wkt2 )
2852       return wkt1 < wkt2;
2853   }
2854 
2855   if ( c1.d->mCoordinateEpoch == c2.d->mCoordinateEpoch )
2856     return false;
2857 
2858   if ( std::isnan( c1.d->mCoordinateEpoch ) && std::isnan( c2.d->mCoordinateEpoch ) )
2859     return false;
2860 
2861   if ( !std::isnan( c1.d->mCoordinateEpoch ) && std::isnan( c2.d->mCoordinateEpoch ) )
2862     return false;
2863 
2864   if ( std::isnan( c1.d->mCoordinateEpoch ) && !std::isnan( c2.d->mCoordinateEpoch ) )
2865     return true;
2866 
2867   return c1.d->mCoordinateEpoch < c2.d->mCoordinateEpoch;
2868 }
2869 
operator >=(const QgsCoordinateReferenceSystem & c1,const QgsCoordinateReferenceSystem & c2)2870 bool operator>= ( const QgsCoordinateReferenceSystem &c1, const QgsCoordinateReferenceSystem &c2 )
2871 {
2872   return !( c1 < c2 );
2873 }
operator <=(const QgsCoordinateReferenceSystem & c1,const QgsCoordinateReferenceSystem & c2)2874 bool operator<= ( const QgsCoordinateReferenceSystem &c1, const QgsCoordinateReferenceSystem &c2 )
2875 {
2876   return !( c1 > c2 );
2877 }
2878