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