QGIS API Documentation 3.39.0-Master (67e056379ed)
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 QgsProjUtils::proj_pj_unique_ptr pjCs( proj_crs_get_coordinate_system( context, crs ) );
121 if ( !pjCs )
122 return false;
123
124 const int axisCount = proj_cs_get_axis_count( context, pjCs.get() );
125 if ( axisCount > 0 )
126 {
127 const char *outDirection = nullptr;
128 // Read only first axis, see if it is degrees / north
129
130 proj_cs_get_axis_info( context, pjCs.get(), 0,
131 nullptr,
132 nullptr,
133 &outDirection,
134 nullptr,
135 nullptr,
136 nullptr,
137 nullptr
138 );
139 return QString( outDirection ).compare( QLatin1String( "north" ), Qt::CaseInsensitive ) == 0;
140 }
141 return false;
142}
143
145{
146 // ported from GDAL OGRSpatialReference::IsDynamic()
147 bool isDynamic = false;
148 PJ_CONTEXT *context = QgsProjContext::get();
149
150 // prefer horizontal crs if possible
152 if ( !crs )
153 candidate = unboundCrs( crs );
154
155 proj_pj_unique_ptr datum( candidate ? proj_crs_get_datum( context, candidate.get() ) : nullptr );
156 if ( datum )
157 {
158 const PJ_TYPE type = proj_get_type( datum.get() );
159 isDynamic = type == PJ_TYPE_DYNAMIC_GEODETIC_REFERENCE_FRAME ||
160 type == PJ_TYPE_DYNAMIC_VERTICAL_REFERENCE_FRAME;
161 if ( !isDynamic )
162 {
163 const QString authName( proj_get_id_auth_name( datum.get(), 0 ) );
164 const QString code( proj_get_id_code( datum.get(), 0 ) );
165 if ( authName == QLatin1String( "EPSG" ) && code == QLatin1String( "6326" ) )
166 {
167 isDynamic = true;
168 }
169 }
170 }
171 else
172 {
173 proj_pj_unique_ptr ensemble( candidate ? proj_crs_get_datum_ensemble( context, candidate.get() ) : nullptr );
174 if ( ensemble )
175 {
176 proj_pj_unique_ptr member( proj_datum_ensemble_get_member( context, ensemble.get(), 0 ) );
177 if ( member )
178 {
179 const PJ_TYPE type = proj_get_type( member.get() );
180 isDynamic = type == PJ_TYPE_DYNAMIC_GEODETIC_REFERENCE_FRAME ||
181 type == PJ_TYPE_DYNAMIC_VERTICAL_REFERENCE_FRAME;
182 }
183 }
184 }
185 return isDynamic;
186}
187
189{
190 if ( !crs )
191 return nullptr;
192
193 PJ_CONTEXT *context = QgsProjContext::get();
194 switch ( proj_get_type( crs ) )
195 {
196 case PJ_TYPE_COMPOUND_CRS:
197 {
198 int i = 0;
199 QgsProjUtils::proj_pj_unique_ptr res( proj_crs_get_sub_crs( context, crs, i ) );
200 while ( res && ( proj_get_type( res.get() ) == PJ_TYPE_VERTICAL_CRS || proj_get_type( res.get() ) == PJ_TYPE_TEMPORAL_CRS ) )
201 {
202 i++;
203 res.reset( proj_crs_get_sub_crs( context, crs, i ) );
204 }
205 return res;
206 }
207
208 case PJ_TYPE_VERTICAL_CRS:
209 return nullptr;
210
211 // maybe other types to handle??
212
213 default:
214 return unboundCrs( crs );
215 }
216
217#ifndef _MSC_VER // unreachable
218 return nullptr;
219#endif
220}
221
223{
224 if ( !crs )
225 return nullptr;
226
227 PJ_CONTEXT *context = QgsProjContext::get();
228 switch ( proj_get_type( crs ) )
229 {
230 case PJ_TYPE_COMPOUND_CRS:
231 {
232 int i = 0;
233 QgsProjUtils::proj_pj_unique_ptr res( proj_crs_get_sub_crs( context, crs, i ) );
234 while ( res && ( proj_get_type( res.get() ) != PJ_TYPE_VERTICAL_CRS ) )
235 {
236 i++;
237 res.reset( proj_crs_get_sub_crs( context, crs, i ) );
238 }
239 return res;
240 }
241
242 case PJ_TYPE_VERTICAL_CRS:
243 return QgsProjUtils::proj_pj_unique_ptr( proj_clone( context, crs ) );
244
245 // maybe other types to handle??
246
247 default:
248 return nullptr;
249 }
250
252}
253
255{
256 if ( !crs )
257 return false;
258
259 PJ_CONTEXT *context = QgsProjContext::get();
260
261 switch ( proj_get_type( crs ) )
262 {
263 case PJ_TYPE_COMPOUND_CRS:
264 {
265 int i = 0;
266 QgsProjUtils::proj_pj_unique_ptr res( proj_crs_get_sub_crs( context, crs, i ) );
267 while ( res )
268 {
269 if ( hasVerticalAxis( res.get() ) )
270 return true;
271 i++;
272 res.reset( proj_crs_get_sub_crs( context, crs, i ) );
273 }
274 return false;
275 }
276
277 // maybe other types to handle like this??
278
279 default:
280 break;
281 }
282
283 QgsProjUtils::proj_pj_unique_ptr pjCs( proj_crs_get_coordinate_system( context, crs ) );
284 if ( !pjCs )
285 return false;
286
287 const int axisCount = proj_cs_get_axis_count( context, pjCs.get() );
288 for ( int axisIndex = 0; axisIndex < axisCount; ++axisIndex )
289 {
290 const char *outDirection = nullptr;
291 proj_cs_get_axis_info( context, pjCs.get(), axisIndex,
292 nullptr,
293 nullptr,
294 &outDirection,
295 nullptr,
296 nullptr,
297 nullptr,
298 nullptr
299 );
300 const QString outDirectionString = QString( outDirection );
301 if ( outDirectionString.compare( QLatin1String( "geocentricZ" ), Qt::CaseInsensitive ) == 0
302 || outDirectionString.compare( QLatin1String( "up" ), Qt::CaseInsensitive ) == 0
303 || outDirectionString.compare( QLatin1String( "down" ), Qt::CaseInsensitive ) == 0 )
304 {
305 return true;
306 }
307 }
308 return false;
309}
310
312{
313 if ( !crs )
314 return nullptr;
315
316 PJ_CONTEXT *context = QgsProjContext::get();
317 switch ( proj_get_type( crs ) )
318 {
319 case PJ_TYPE_BOUND_CRS:
320 return QgsProjUtils::proj_pj_unique_ptr( proj_get_source_crs( context, crs ) );
321
322 // maybe other types to handle??
323
324 default:
325 return QgsProjUtils::proj_pj_unique_ptr( proj_clone( context, crs ) );
326 }
327
328#ifndef _MSC_VER // unreachable
329 return nullptr;
330#endif
331}
332
334{
335 if ( !crs )
336 return nullptr;
337
338#if PROJ_VERSION_MAJOR>=8
339 PJ_CONTEXT *context = QgsProjContext::get();
341 if ( !candidate ) // purely vertical CRS
342 candidate = unboundCrs( crs );
343
344 if ( !candidate )
345 return nullptr;
346
347 return QgsProjUtils::proj_pj_unique_ptr( proj_crs_get_datum_ensemble( context, candidate.get() ) );
348#else
349 throw QgsNotSupportedException( QObject::tr( "Calculating datum ensembles requires a QGIS build based on PROJ 8.0 or later" ) );
350#endif
351}
352
353static void proj_collecting_logger( void *user_data, int /*level*/, const char *message )
354{
355 QStringList *dest = reinterpret_cast< QStringList * >( user_data );
356 dest->append( QString( message ) );
357}
358
359static void proj_logger( void *, int level, const char *message )
360{
361#ifdef QGISDEBUG
362 if ( level == PJ_LOG_ERROR )
363 {
364 QgsDebugError( QString( message ) );
365 }
366 else if ( level == PJ_LOG_DEBUG )
367 {
368 QgsDebugMsgLevel( QString( message ), 3 );
369 }
370#else
371 ( void )level;
372 ( void )message;
373#endif
374}
375
376QgsProjUtils::proj_pj_unique_ptr QgsProjUtils::createCompoundCrs( const PJ *horizontalCrs, const PJ *verticalCrs, QStringList *errors )
377{
378 if ( !horizontalCrs || !verticalCrs )
379 return nullptr;
380
381 PJ_CONTEXT *context = QgsProjContext::get();
382 // collect errors instead of dumping them to terminal
383
384 QStringList tempErrors;
385 proj_log_func( context, &tempErrors, proj_collecting_logger );
386
387 // const cast here is for compatibility with proj < 9.5
388 QgsProjUtils::proj_pj_unique_ptr compoundCrs( proj_create_compound_crs( context,
389 nullptr,
390 const_cast< PJ *>( horizontalCrs ),
391 const_cast< PJ * >( verticalCrs ) ) );
392
393 // reset logging function
394 proj_log_func( context, nullptr, proj_logger );
395 if ( errors )
396 *errors = tempErrors;
397
398 return compoundCrs;
399}
400
401bool QgsProjUtils::identifyCrs( const PJ *crs, QString &authName, QString &authCode, IdentifyFlags flags )
402{
403 authName.clear();
404 authCode.clear();
405
406 if ( !crs )
407 return false;
408
409 int *confidence = nullptr;
410 if ( PJ_OBJ_LIST *crsList = proj_identify( QgsProjContext::get(), crs, nullptr, nullptr, &confidence ) )
411 {
412 const int count = proj_list_get_count( crsList );
413 int bestConfidence = 0;
415 for ( int i = 0; i < count; ++i )
416 {
417 if ( confidence[i] >= bestConfidence )
418 {
419 QgsProjUtils::proj_pj_unique_ptr candidateCrs( proj_list_get( QgsProjContext::get(), crsList, i ) );
420 switch ( proj_get_type( candidateCrs.get() ) )
421 {
422 case PJ_TYPE_BOUND_CRS:
423 // proj_identify also matches bound CRSes to the source CRS. But they are not the same as the source CRS, so we don't
424 // consider them a candidate for a match here (depending on the identify flags, that is!)
426 break;
427 else
428 continue;
429
430 default:
431 break;
432 }
433
434 candidateCrs = QgsProjUtils::unboundCrs( candidateCrs.get() );
435 const QString authName( proj_get_id_auth_name( candidateCrs.get(), 0 ) );
436 // if a match is identical confidence, we prefer EPSG codes for compatibility with earlier qgis conversions
437 if ( confidence[i] > bestConfidence || ( confidence[i] == bestConfidence && authName == QLatin1String( "EPSG" ) ) )
438 {
439 bestConfidence = confidence[i];
440 matchedCrs = std::move( candidateCrs );
441 }
442 }
443 }
444 proj_list_destroy( crsList );
445 proj_int_list_destroy( confidence );
446 if ( matchedCrs && bestConfidence >= 70 )
447 {
448 authName = QString( proj_get_id_auth_name( matchedCrs.get(), 0 ) );
449 authCode = QString( proj_get_id_code( matchedCrs.get(), 0 ) );
450 }
451 }
452 return !authName.isEmpty() && !authCode.isEmpty();
453}
454
456{
457 if ( projDef.isEmpty() )
458 return true;
459
460 PJ_CONTEXT *context = QgsProjContext::get();
461 QgsProjUtils::proj_pj_unique_ptr coordinateOperation( proj_create( context, projDef.toUtf8().constData() ) );
462 if ( !coordinateOperation )
463 return false;
464
465 return static_cast< bool >( proj_coordoperation_is_instantiable( context, coordinateOperation.get() ) );
466}
467
468QList<QgsDatumTransform::GridDetails> QgsProjUtils::gridsUsed( const QString &proj )
469{
470 const thread_local QRegularExpression regex( QStringLiteral( "\\+(?:nad)?grids=(.*?)\\s" ) );
471
472 QList< QgsDatumTransform::GridDetails > grids;
473 QRegularExpressionMatchIterator matches = regex.globalMatch( proj );
474 while ( matches.hasNext() )
475 {
476 const QRegularExpressionMatch match = matches.next();
477 const QString gridName = match.captured( 1 );
479 grid.shortName = gridName;
480 const char *fullName = nullptr;
481 const char *packageName = nullptr;
482 const char *url = nullptr;
483 int directDownload = 0;
484 int openLicense = 0;
485 int available = 0;
486 proj_grid_get_info_from_database( QgsProjContext::get(), gridName.toUtf8().constData(), &fullName, &packageName, &url, &directDownload, &openLicense, &available );
487 grid.fullName = QString( fullName );
488 grid.packageName = QString( packageName );
489 grid.url = QString( url );
490 grid.directDownload = directDownload;
491 grid.openLicense = openLicense;
492 grid.isAvailable = available;
493 grids.append( grid );
494 }
495 return grids;
496}
497
498#if 0
499QStringList QgsProjUtils::nonAvailableGrids( const QString &projDef )
500{
501 if ( projDef.isEmpty() )
502 return QStringList();
503
504 PJ_CONTEXT *context = QgsProjContext::get();
505 QgsProjUtils::proj_pj_unique_ptr op( proj_create( context, projDef.toUtf8().constData() ) ); < ---- - this always fails if grids are missing
506 if ( !op )
507 return QStringList();
508
509 QStringList res;
510 for ( int j = 0; j < proj_coordoperation_get_grid_used_count( context, op.get() ); ++j )
511 {
512 const char *shortName = nullptr;
513 int isAvailable = 0;
514 proj_coordoperation_get_grid_used( context, op.get(), j, &shortName, nullptr, nullptr, nullptr, nullptr, nullptr, &isAvailable );
515 if ( !isAvailable )
516 res << QString( shortName );
517 }
518 return res;
519}
520#endif
521
523{
524 return PROJ_VERSION_MAJOR;
525}
526
528{
529 return PROJ_VERSION_MINOR;
530}
531
533{
534 PJ_CONTEXT *context = QgsProjContext::get();
535 const char *version = proj_context_get_database_metadata( context, "EPSG.VERSION" );
536 return QString( version );
537}
538
540{
541 PJ_CONTEXT *context = QgsProjContext::get();
542 const char *date = proj_context_get_database_metadata( context, "EPSG.DATE" );
543 return QDate::fromString( date, Qt::DateFormat::ISODate );
544}
545
547{
548 PJ_CONTEXT *context = QgsProjContext::get();
549 const char *version = proj_context_get_database_metadata( context, "ESRI.VERSION" );
550 return QString( version );
551}
552
554{
555 PJ_CONTEXT *context = QgsProjContext::get();
556 const char *date = proj_context_get_database_metadata( context, "ESRI.DATE" );
557 return QDate::fromString( date, Qt::DateFormat::ISODate );
558}
559
561{
562 PJ_CONTEXT *context = QgsProjContext::get();
563 const char *version = proj_context_get_database_metadata( context, "IGNF.VERSION" );
564 return QString( version );
565}
566
568{
569 PJ_CONTEXT *context = QgsProjContext::get();
570 const char *date = proj_context_get_database_metadata( context, "IGNF.DATE" );
571 return QDate::fromString( date, Qt::DateFormat::ISODate );
572}
573
575{
576 const QString path( proj_info().searchpath );
577 QStringList paths;
578#ifdef Q_OS_WIN
579 paths = path.split( ';' );
580#else
581 paths = path.split( ':' );
582#endif
583
584 QSet<QString> existing;
585 // thin out duplicates from paths -- see https://github.com/OSGeo/proj.4/pull/1498
586 QStringList res;
587 res.reserve( paths.count() );
588 for ( const QString &p : std::as_const( paths ) )
589 {
590 if ( existing.contains( p ) )
591 continue;
592
593 existing.insert( p );
594 res << p;
595 }
596 return res;
597}
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.
QFlags< IdentifyFlag > IdentifyFlags
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.
#define BUILTIN_UNREACHABLE
Definition qgis.h:6119
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.