QGIS API Documentation 3.41.0-Master (3440c17df1d)
Loading...
Searching...
No Matches
qgsprojutils.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgsprojutils.h
3 -------------------
4 begin : March 2019
5 copyright : (C) 2019 by Nyall Dawson
6 email : nyall dot dawson at gmail dot com
7***************************************************************************/
8
9/***************************************************************************
10 * *
11 * This program is free software; you can redistribute it and/or modify *
12 * it under the terms of the GNU General Public License as published by *
13 * the Free Software Foundation; either version 2 of the License, or *
14 * (at your option) any later version. *
15 * *
16 ***************************************************************************/
17#include "qgsprojutils.h"
18#include "qgis.h"
20#include "qgsexception.h"
21#include "qgslogger.h"
22#include <QString>
23#include <QSet>
24#include <QRegularExpression>
25#include <QDate>
26
27#include <proj.h>
28#include <proj_experimental.h>
29
30#if defined(USE_THREAD_LOCAL) && !defined(Q_OS_WIN)
31thread_local QgsProjContext QgsProjContext::sProjContext;
32#else
33QThreadStorage< QgsProjContext * > QgsProjContext::sProjContext;
34#endif
35
37{
38 mContext = proj_context_create();
39}
40
42{
43 // Call removeFromCacheObjectsBelongingToCurrentThread() before
44 // destroying the context
45 QgsCoordinateTransform::removeFromCacheObjectsBelongingToCurrentThread( mContext );
46 QgsCoordinateReferenceSystem::removeFromCacheObjectsBelongingToCurrentThread( mContext );
47 proj_context_destroy( mContext );
48}
49
51{
52#if defined(USE_THREAD_LOCAL) && !defined(Q_OS_WIN)
53 return sProjContext.mContext;
54#else
55 PJ_CONTEXT *pContext = nullptr;
56 if ( sProjContext.hasLocalData() )
57 {
58 pContext = sProjContext.localData()->mContext;
59 }
60 else
61 {
62 sProjContext.setLocalData( new QgsProjContext() );
63 pContext = sProjContext.localData()->mContext;
64 }
65 return pContext;
66#endif
67}
68
70{
71 proj_destroy( object );
72}
73
74bool QgsProjUtils::usesAngularUnit( const QString &projDef )
75{
76 const QString crsDef = QStringLiteral( "%1 +type=crs" ).arg( projDef );
78 QgsProjUtils::proj_pj_unique_ptr projSingleOperation( proj_create( context, crsDef.toUtf8().constData() ) );
79 if ( !projSingleOperation )
80 return false;
81
82 QgsProjUtils::proj_pj_unique_ptr coordinateSystem( proj_crs_get_coordinate_system( context, projSingleOperation.get() ) );
83 if ( !coordinateSystem )
84 return false;
85
86 const int axisCount = proj_cs_get_axis_count( context, coordinateSystem.get() );
87 if ( axisCount > 0 )
88 {
89 const char *outUnitAuthName = nullptr;
90 const char *outUnitAuthCode = nullptr;
91 // Read only first axis
92 proj_cs_get_axis_info( context, coordinateSystem.get(), 0,
93 nullptr,
94 nullptr,
95 nullptr,
96 nullptr,
97 nullptr,
98 &outUnitAuthName,
99 &outUnitAuthCode );
100
101 if ( outUnitAuthName && outUnitAuthCode )
102 {
103 const char *unitCategory = nullptr;
104 if ( proj_uom_get_info_from_database( context, outUnitAuthName, outUnitAuthCode, nullptr, nullptr, &unitCategory ) )
105 {
106 return QString( unitCategory ).compare( QLatin1String( "angular" ), Qt::CaseInsensitive ) == 0;
107 }
108 }
109 }
110 return false;
111}
112
114{
115 //ported from https://github.com/pramsey/postgis/blob/7ecf6839c57a838e2c8540001a3cd35b78a730db/liblwgeom/lwgeom_transform.c#L299
116 if ( !crs )
117 return false;
118
119 PJ_CONTEXT *context = QgsProjContext::get();
120
122 if ( !horizCrs )
123 return false;
124
125 QgsProjUtils::proj_pj_unique_ptr pjCs( proj_crs_get_coordinate_system( context, horizCrs.get() ) );
126 if ( !pjCs )
127 return false;
128
129 const int axisCount = proj_cs_get_axis_count( context, pjCs.get() );
130 if ( axisCount > 0 )
131 {
132 const char *outDirection = nullptr;
133 // Read only first axis, see if it is degrees / north
134
135 proj_cs_get_axis_info( context, pjCs.get(), 0,
136 nullptr,
137 nullptr,
138 &outDirection,
139 nullptr,
140 nullptr,
141 nullptr,
142 nullptr
143 );
144 return QString( outDirection ).compare( QLatin1String( "north" ), Qt::CaseInsensitive ) == 0;
145 }
146 return false;
147}
148
150{
151 // ported from GDAL OGRSpatialReference::IsDynamic()
152 bool isDynamic = false;
153 PJ_CONTEXT *context = QgsProjContext::get();
154
155 // prefer horizontal crs if possible
157 if ( !crs )
158 candidate = unboundCrs( crs );
159
160 proj_pj_unique_ptr datum( candidate ? proj_crs_get_datum( context, candidate.get() ) : nullptr );
161 if ( datum )
162 {
163 const PJ_TYPE type = proj_get_type( datum.get() );
164 isDynamic = type == PJ_TYPE_DYNAMIC_GEODETIC_REFERENCE_FRAME ||
165 type == PJ_TYPE_DYNAMIC_VERTICAL_REFERENCE_FRAME;
166 if ( !isDynamic )
167 {
168 const QString authName( proj_get_id_auth_name( datum.get(), 0 ) );
169 const QString code( proj_get_id_code( datum.get(), 0 ) );
170 if ( authName == QLatin1String( "EPSG" ) && code == QLatin1String( "6326" ) )
171 {
172 isDynamic = true;
173 }
174 }
175 }
176 else
177 {
178 proj_pj_unique_ptr ensemble( candidate ? proj_crs_get_datum_ensemble( context, candidate.get() ) : nullptr );
179 if ( ensemble )
180 {
181 proj_pj_unique_ptr member( proj_datum_ensemble_get_member( context, ensemble.get(), 0 ) );
182 if ( member )
183 {
184 const PJ_TYPE type = proj_get_type( member.get() );
185 isDynamic = type == PJ_TYPE_DYNAMIC_GEODETIC_REFERENCE_FRAME ||
186 type == PJ_TYPE_DYNAMIC_VERTICAL_REFERENCE_FRAME;
187 }
188 }
189 }
190 return isDynamic;
191}
192
194{
195 if ( !crs )
196 return nullptr;
197
198 PJ_CONTEXT *context = QgsProjContext::get();
199 switch ( proj_get_type( crs ) )
200 {
201 case PJ_TYPE_COMPOUND_CRS:
202 {
203 int i = 0;
204 QgsProjUtils::proj_pj_unique_ptr res( proj_crs_get_sub_crs( context, crs, i ) );
205 while ( res && ( proj_get_type( res.get() ) == PJ_TYPE_VERTICAL_CRS || proj_get_type( res.get() ) == PJ_TYPE_TEMPORAL_CRS ) )
206 {
207 i++;
208 res.reset( proj_crs_get_sub_crs( context, crs, i ) );
209 }
210 return res;
211 }
212
213 case PJ_TYPE_VERTICAL_CRS:
214 return nullptr;
215
216 // maybe other types to handle??
217
218 default:
219 return unboundCrs( crs );
220 }
221
222#ifndef _MSC_VER // unreachable
223 return nullptr;
224#endif
225}
226
228{
229 if ( !crs )
230 return nullptr;
231
232 PJ_CONTEXT *context = QgsProjContext::get();
233 switch ( proj_get_type( crs ) )
234 {
235 case PJ_TYPE_COMPOUND_CRS:
236 {
237 int i = 0;
238 QgsProjUtils::proj_pj_unique_ptr res( proj_crs_get_sub_crs( context, crs, i ) );
239 while ( res && ( proj_get_type( res.get() ) != PJ_TYPE_VERTICAL_CRS ) )
240 {
241 i++;
242 res.reset( proj_crs_get_sub_crs( context, crs, i ) );
243 }
244 return res;
245 }
246
247 case PJ_TYPE_VERTICAL_CRS:
248 return QgsProjUtils::proj_pj_unique_ptr( proj_clone( context, crs ) );
249
250 // maybe other types to handle??
251
252 default:
253 return nullptr;
254 }
255
257}
258
260{
261 if ( !crs )
262 return false;
263
264 PJ_CONTEXT *context = QgsProjContext::get();
265
266 switch ( proj_get_type( crs ) )
267 {
268 case PJ_TYPE_COMPOUND_CRS:
269 {
270 int i = 0;
271 QgsProjUtils::proj_pj_unique_ptr res( proj_crs_get_sub_crs( context, crs, i ) );
272 while ( res )
273 {
274 if ( hasVerticalAxis( res.get() ) )
275 return true;
276 i++;
277 res.reset( proj_crs_get_sub_crs( context, crs, i ) );
278 }
279 return false;
280 }
281
282 // maybe other types to handle like this??
283
284 default:
285 break;
286 }
287
288 QgsProjUtils::proj_pj_unique_ptr pjCs( proj_crs_get_coordinate_system( context, crs ) );
289 if ( !pjCs )
290 return false;
291
292 const int axisCount = proj_cs_get_axis_count( context, pjCs.get() );
293 for ( int axisIndex = 0; axisIndex < axisCount; ++axisIndex )
294 {
295 const char *outDirection = nullptr;
296 proj_cs_get_axis_info( context, pjCs.get(), axisIndex,
297 nullptr,
298 nullptr,
299 &outDirection,
300 nullptr,
301 nullptr,
302 nullptr,
303 nullptr
304 );
305 const QString outDirectionString = QString( outDirection );
306 if ( outDirectionString.compare( QLatin1String( "geocentricZ" ), Qt::CaseInsensitive ) == 0
307 || outDirectionString.compare( QLatin1String( "up" ), Qt::CaseInsensitive ) == 0
308 || outDirectionString.compare( QLatin1String( "down" ), Qt::CaseInsensitive ) == 0 )
309 {
310 return true;
311 }
312 }
313 return false;
314}
315
317{
318 if ( !crs )
319 return nullptr;
320
321 PJ_CONTEXT *context = QgsProjContext::get();
322 switch ( proj_get_type( crs ) )
323 {
324 case PJ_TYPE_BOUND_CRS:
325 return QgsProjUtils::proj_pj_unique_ptr( proj_get_source_crs( context, crs ) );
326
327 // maybe other types to handle??
328
329 default:
330 return QgsProjUtils::proj_pj_unique_ptr( proj_clone( context, crs ) );
331 }
332
333#ifndef _MSC_VER // unreachable
334 return nullptr;
335#endif
336}
337
339{
340 if ( !crs )
341 return nullptr;
342
343#if PROJ_VERSION_MAJOR>=8
344 PJ_CONTEXT *context = QgsProjContext::get();
346 if ( !candidate ) // purely vertical CRS
347 candidate = unboundCrs( crs );
348
349 if ( !candidate )
350 return nullptr;
351
352 return QgsProjUtils::proj_pj_unique_ptr( proj_crs_get_datum_ensemble( context, candidate.get() ) );
353#else
354 throw QgsNotSupportedException( QObject::tr( "Calculating datum ensembles requires a QGIS build based on PROJ 8.0 or later" ) );
355#endif
356}
357
358void QgsProjUtils::proj_collecting_logger( void *user_data, int /*level*/, const char *message )
359{
360 QStringList *dest = reinterpret_cast< QStringList * >( user_data );
361 QString messageString( message );
362 messageString.replace( QLatin1String( "internal_proj_create: " ), QString() );
363 dest->append( messageString );
364}
365
366void QgsProjUtils::proj_logger( void *, int level, const char *message )
367{
368#ifdef QGISDEBUG
369 if ( level == PJ_LOG_ERROR )
370 {
371 const QString messageString( message );
372 if ( messageString == QLatin1String( "push: Invalid latitude" ) )
373 {
374 // these messages tend to spam the console as they can be repeated 1000s of times
375 QgsDebugMsgLevel( messageString, 3 );
376 }
377 else
378 {
379 QgsDebugError( messageString );
380 }
381 }
382 else if ( level == PJ_LOG_DEBUG )
383 {
384 QgsDebugMsgLevel( QString( message ), 3 );
385 }
386#else
387 ( void )level;
388 ( void )message;
389#endif
390}
391
392QgsProjUtils::proj_pj_unique_ptr QgsProjUtils::createCompoundCrs( const PJ *horizontalCrs, const PJ *verticalCrs, QStringList *errors )
393{
394 if ( !horizontalCrs || !verticalCrs )
395 return nullptr;
396
397 PJ_CONTEXT *context = QgsProjContext::get();
398 // collect errors instead of dumping them to terminal
399
401
402 // const cast here is for compatibility with proj < 9.5
403 QgsProjUtils::proj_pj_unique_ptr compoundCrs( proj_create_compound_crs( context,
404 nullptr,
405 const_cast< PJ *>( horizontalCrs ),
406 const_cast< PJ * >( verticalCrs ) ) );
407
408 if ( errors )
409 *errors = projLogger.errors();
410
411 return compoundCrs;
412}
413
414bool QgsProjUtils::identifyCrs( const PJ *crs, QString &authName, QString &authCode, IdentifyFlags flags )
415{
416 authName.clear();
417 authCode.clear();
418
419 if ( !crs )
420 return false;
421
422 int *confidence = nullptr;
423 if ( PJ_OBJ_LIST *crsList = proj_identify( QgsProjContext::get(), crs, nullptr, nullptr, &confidence ) )
424 {
425 const int count = proj_list_get_count( crsList );
426 int bestConfidence = 0;
428 for ( int i = 0; i < count; ++i )
429 {
430 if ( confidence[i] >= bestConfidence )
431 {
432 QgsProjUtils::proj_pj_unique_ptr candidateCrs( proj_list_get( QgsProjContext::get(), crsList, i ) );
433 switch ( proj_get_type( candidateCrs.get() ) )
434 {
435 case PJ_TYPE_BOUND_CRS:
436 // proj_identify also matches bound CRSes to the source CRS. But they are not the same as the source CRS, so we don't
437 // consider them a candidate for a match here (depending on the identify flags, that is!)
439 break;
440 else
441 continue;
442
443 default:
444 break;
445 }
446
447 candidateCrs = QgsProjUtils::unboundCrs( candidateCrs.get() );
448 const QString authName( proj_get_id_auth_name( candidateCrs.get(), 0 ) );
449 // if a match is identical confidence, we prefer EPSG codes for compatibility with earlier qgis conversions
450 if ( confidence[i] > bestConfidence || ( confidence[i] == bestConfidence && authName == QLatin1String( "EPSG" ) ) )
451 {
452 bestConfidence = confidence[i];
453 matchedCrs = std::move( candidateCrs );
454 }
455 }
456 }
457 proj_list_destroy( crsList );
458 proj_int_list_destroy( confidence );
459 if ( matchedCrs && bestConfidence >= 70 )
460 {
461 authName = QString( proj_get_id_auth_name( matchedCrs.get(), 0 ) );
462 authCode = QString( proj_get_id_code( matchedCrs.get(), 0 ) );
463 }
464 }
465 return !authName.isEmpty() && !authCode.isEmpty();
466}
467
469{
470 if ( projDef.isEmpty() )
471 return true;
472
473 PJ_CONTEXT *context = QgsProjContext::get();
474 QgsProjUtils::proj_pj_unique_ptr coordinateOperation( proj_create( context, projDef.toUtf8().constData() ) );
475 if ( !coordinateOperation )
476 return false;
477
478 return static_cast< bool >( proj_coordoperation_is_instantiable( context, coordinateOperation.get() ) );
479}
480
481QList<QgsDatumTransform::GridDetails> QgsProjUtils::gridsUsed( const QString &proj )
482{
483 const thread_local QRegularExpression regex( QStringLiteral( "\\+(?:nad)?grids=(.*?)\\s" ) );
484
485 QList< QgsDatumTransform::GridDetails > grids;
486 QRegularExpressionMatchIterator matches = regex.globalMatch( proj );
487 while ( matches.hasNext() )
488 {
489 const QRegularExpressionMatch match = matches.next();
490 const QString gridName = match.captured( 1 );
492 grid.shortName = gridName;
493 const char *fullName = nullptr;
494 const char *packageName = nullptr;
495 const char *url = nullptr;
496 int directDownload = 0;
497 int openLicense = 0;
498 int available = 0;
499 proj_grid_get_info_from_database( QgsProjContext::get(), gridName.toUtf8().constData(), &fullName, &packageName, &url, &directDownload, &openLicense, &available );
500 grid.fullName = QString( fullName );
501 grid.packageName = QString( packageName );
502 grid.url = QString( url );
503 grid.directDownload = directDownload;
504 grid.openLicense = openLicense;
505 grid.isAvailable = available;
506 grids.append( grid );
507 }
508 return grids;
509}
510
511#if 0
512QStringList QgsProjUtils::nonAvailableGrids( const QString &projDef )
513{
514 if ( projDef.isEmpty() )
515 return QStringList();
516
517 PJ_CONTEXT *context = QgsProjContext::get();
518 QgsProjUtils::proj_pj_unique_ptr op( proj_create( context, projDef.toUtf8().constData() ) ); < ---- - this always fails if grids are missing
519 if ( !op )
520 return QStringList();
521
522 QStringList res;
523 for ( int j = 0; j < proj_coordoperation_get_grid_used_count( context, op.get() ); ++j )
524 {
525 const char *shortName = nullptr;
526 int isAvailable = 0;
527 proj_coordoperation_get_grid_used( context, op.get(), j, &shortName, nullptr, nullptr, nullptr, nullptr, nullptr, &isAvailable );
528 if ( !isAvailable )
529 res << QString( shortName );
530 }
531 return res;
532}
533#endif
534
536{
537 return PROJ_VERSION_MAJOR;
538}
539
541{
542 return PROJ_VERSION_MINOR;
543}
544
546{
547 PJ_CONTEXT *context = QgsProjContext::get();
548 const char *version = proj_context_get_database_metadata( context, "EPSG.VERSION" );
549 return QString( version );
550}
551
553{
554 PJ_CONTEXT *context = QgsProjContext::get();
555 const char *date = proj_context_get_database_metadata( context, "EPSG.DATE" );
556 return QDate::fromString( date, Qt::DateFormat::ISODate );
557}
558
560{
561 PJ_CONTEXT *context = QgsProjContext::get();
562 const char *version = proj_context_get_database_metadata( context, "ESRI.VERSION" );
563 return QString( version );
564}
565
567{
568 PJ_CONTEXT *context = QgsProjContext::get();
569 const char *date = proj_context_get_database_metadata( context, "ESRI.DATE" );
570 return QDate::fromString( date, Qt::DateFormat::ISODate );
571}
572
574{
575 PJ_CONTEXT *context = QgsProjContext::get();
576 const char *version = proj_context_get_database_metadata( context, "IGNF.VERSION" );
577 return QString( version );
578}
579
581{
582 PJ_CONTEXT *context = QgsProjContext::get();
583 const char *date = proj_context_get_database_metadata( context, "IGNF.DATE" );
584 return QDate::fromString( date, Qt::DateFormat::ISODate );
585}
586
588{
589 const QString path( proj_info().searchpath );
590 QStringList paths;
591#ifdef Q_OS_WIN
592 paths = path.split( ';' );
593#else
594 paths = path.split( ':' );
595#endif
596
597 QSet<QString> existing;
598 // thin out duplicates from paths -- see https://github.com/OSGeo/proj.4/pull/1498
599 QStringList res;
600 res.reserve( paths.count() );
601 for ( const QString &p : std::as_const( paths ) )
602 {
603 if ( existing.contains( p ) )
604 continue;
605
606 existing.insert( p );
607 res << p;
608 }
609 return res;
610}
611
612//
613// QgsScopedProjCollectingLogger
614//
615
620
622{
623 // reset logger back to terminal output
624 proj_log_func( QgsProjContext::get(), nullptr, QgsProjUtils::proj_logger );
625}
Custom exception class which is raised when an operation is not supported.
Used to create and store a proj context object, correctly freeing the context upon destruction.
static PJ_CONTEXT * get()
Returns a thread local instance of a proj context, safe for use in the current thread.
static proj_pj_unique_ptr crsToHorizontalCrs(const PJ *crs)
Given a PROJ crs (which may be a compound or bound crs, or some other type), extract the horizontal c...
@ FlagMatchBoundCrsToUnderlyingSourceCrs
Allow matching a BoundCRS object to its underlying SourceCRS.
static QList< QgsDatumTransform::GridDetails > gridsUsed(const QString &proj)
Returns a list of grids used by the given proj string.
static proj_pj_unique_ptr createCompoundCrs(const PJ *horizontalCrs, const PJ *verticalCrs, QStringList *errors=nullptr)
Given a PROJ horizontal and vertical CRS, attempt to create a compound CRS from them.
static bool isDynamic(const PJ *crs)
Returns true if the given proj coordinate system is a dynamic CRS.
static QDate epsgRegistryDate()
Returns the EPSG registry database release date used by the proj library.
static proj_pj_unique_ptr unboundCrs(const PJ *crs)
Given a PROJ crs (which may be a compound or bound crs, or some other type), ensure that it is not a ...
static bool identifyCrs(const PJ *crs, QString &authName, QString &authCode, IdentifyFlags flags=IdentifyFlags())
Attempts to identify a crs, matching it to a known authority and code within an acceptable level of t...
static QStringList searchPaths()
Returns the current list of Proj file search paths.
static bool hasVerticalAxis(const PJ *crs)
Returns true if a PROJ crs has a vertical axis.
static proj_pj_unique_ptr crsToVerticalCrs(const PJ *crs)
Given a PROJ crs (which may be a compound crs, or some other type), extract the vertical crs from it.
static QString ignfDatabaseVersion()
Returns the IGNF database version used by the proj library (e.g.
static proj_pj_unique_ptr crsToDatumEnsemble(const PJ *crs)
Given a PROJ crs, attempt to retrieve the datum ensemble from it.
static void proj_collecting_logger(void *user_data, int level, const char *message)
QGIS proj log function which collects errors to a QStringList.
QFlags< IdentifyFlag > IdentifyFlags
static void proj_logger(void *user_data, int level, const char *message)
Default QGIS proj log function.
static bool coordinateOperationIsAvailable(const QString &projDef)
Returns true if a coordinate operation (specified via proj string) is available.
static QString epsgRegistryVersion()
Returns the EPSG registry database version used by the proj library (e.g.
static QDate esriDatabaseDate()
Returns the ESRI projection engine database release date used by the proj library.
std::unique_ptr< PJ, ProjPJDeleter > proj_pj_unique_ptr
Scoped Proj PJ object.
static bool usesAngularUnit(const QString &projDef)
Returns true if the given proj coordinate system uses angular units.
static bool axisOrderIsSwapped(const PJ *crs)
Returns true if the given proj coordinate system uses requires y/x coordinate order instead of x/y.
static QString esriDatabaseVersion()
Returns the ESRI projection engine database version used by the proj library (e.g.
static int projVersionMajor()
Returns the proj library major version number.
static QDate ignfDatabaseDate()
Returns the IGNF database release date used by the proj library.
static int projVersionMinor()
Returns the proj library minor version number.
Scoped object for temporary swapping to an error-collecting PROJ log function.
QStringList errors() const
Returns the (possibly empty) list of collected errors.
QgsScopedProjCollectingLogger()
Constructor for QgsScopedProjCollectingLogger.
~QgsScopedProjCollectingLogger()
Returns the PROJ logger back to the default QGIS PROJ logger.
#define BUILTIN_UNREACHABLE
Definition qgis.h:6612
struct projCtx_t PJ_CONTEXT
#define QgsDebugMsgLevel(str, level)
Definition qgslogger.h:39
#define QgsDebugError(str)
Definition qgslogger.h:38
struct PJconsts PJ
struct projCtx_t PJ_CONTEXT
const QgsCoordinateReferenceSystem & crs
Contains information about a projection transformation grid file.
QString shortName
Short name of transform grid.
bool isAvailable
true if grid is currently available for use
QString fullName
Full name of transform grid.
bool directDownload
true if direct download of grid is possible
QString packageName
Name of package the grid is included within.
QString url
Url to download grid from.
bool openLicense
true if grid is available under an open license
void CORE_EXPORT operator()(PJ *object) const
Destroys an PJ object, using the correct proj calls.