QGIS API Documentation 3.37.0-Master (fdefdf9c27f)
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 <QString>
22#include <QSet>
23#include <QRegularExpression>
24#include <QDate>
25
26#include <proj.h>
27#include <proj_experimental.h>
28
29#if defined(USE_THREAD_LOCAL) && !defined(Q_OS_WIN)
30thread_local QgsProjContext QgsProjContext::sProjContext;
31#else
32QThreadStorage< QgsProjContext * > QgsProjContext::sProjContext;
33#endif
34
36{
37 mContext = proj_context_create();
38}
39
41{
42 // Call removeFromCacheObjectsBelongingToCurrentThread() before
43 // destroying the context
44 QgsCoordinateTransform::removeFromCacheObjectsBelongingToCurrentThread( mContext );
45 QgsCoordinateReferenceSystem::removeFromCacheObjectsBelongingToCurrentThread( mContext );
46 proj_context_destroy( mContext );
47}
48
50{
51#if defined(USE_THREAD_LOCAL) && !defined(Q_OS_WIN)
52 return sProjContext.mContext;
53#else
54 PJ_CONTEXT *pContext = nullptr;
55 if ( sProjContext.hasLocalData() )
56 {
57 pContext = sProjContext.localData()->mContext;
58 }
59 else
60 {
61 sProjContext.setLocalData( new QgsProjContext() );
62 pContext = sProjContext.localData()->mContext;
63 }
64 return pContext;
65#endif
66}
67
69{
70 proj_destroy( object );
71}
72
73bool QgsProjUtils::usesAngularUnit( const QString &projDef )
74{
75 const QString crsDef = QStringLiteral( "%1 +type=crs" ).arg( projDef );
77 QgsProjUtils::proj_pj_unique_ptr projSingleOperation( proj_create( context, crsDef.toUtf8().constData() ) );
78 if ( !projSingleOperation )
79 return false;
80
81 QgsProjUtils::proj_pj_unique_ptr coordinateSystem( proj_crs_get_coordinate_system( context, projSingleOperation.get() ) );
82 if ( !coordinateSystem )
83 return false;
84
85 const int axisCount = proj_cs_get_axis_count( context, coordinateSystem.get() );
86 if ( axisCount > 0 )
87 {
88 const char *outUnitAuthName = nullptr;
89 const char *outUnitAuthCode = nullptr;
90 // Read only first axis
91 proj_cs_get_axis_info( context, coordinateSystem.get(), 0,
92 nullptr,
93 nullptr,
94 nullptr,
95 nullptr,
96 nullptr,
97 &outUnitAuthName,
98 &outUnitAuthCode );
99
100 if ( outUnitAuthName && outUnitAuthCode )
101 {
102 const char *unitCategory = nullptr;
103 if ( proj_uom_get_info_from_database( context, outUnitAuthName, outUnitAuthCode, nullptr, nullptr, &unitCategory ) )
104 {
105 return QString( unitCategory ).compare( QLatin1String( "angular" ), Qt::CaseInsensitive ) == 0;
106 }
107 }
108 }
109 return false;
110}
111
113{
114 //ported from https://github.com/pramsey/postgis/blob/7ecf6839c57a838e2c8540001a3cd35b78a730db/liblwgeom/lwgeom_transform.c#L299
115 if ( !crs )
116 return false;
117
118 PJ_CONTEXT *context = QgsProjContext::get();
119 QgsProjUtils::proj_pj_unique_ptr pjCs( proj_crs_get_coordinate_system( context, crs ) );
120 if ( !pjCs )
121 return false;
122
123 const int axisCount = proj_cs_get_axis_count( context, pjCs.get() );
124 if ( axisCount > 0 )
125 {
126 const char *outDirection = nullptr;
127 // Read only first axis, see if it is degrees / north
128
129 proj_cs_get_axis_info( context, pjCs.get(), 0,
130 nullptr,
131 nullptr,
132 &outDirection,
133 nullptr,
134 nullptr,
135 nullptr,
136 nullptr
137 );
138 return QString( outDirection ).compare( QLatin1String( "north" ), Qt::CaseInsensitive ) == 0;
139 }
140 return false;
141}
142
144{
145 // ported from GDAL OGRSpatialReference::IsDynamic()
146 bool isDynamic = false;
147 PJ_CONTEXT *context = QgsProjContext::get();
148
149 // prefer horizontal crs if possible
151 if ( !crs )
152 candidate = unboundCrs( crs );
153
154 proj_pj_unique_ptr datum( candidate ? proj_crs_get_datum( context, candidate.get() ) : nullptr );
155 if ( datum )
156 {
157 const PJ_TYPE type = proj_get_type( datum.get() );
158 isDynamic = type == PJ_TYPE_DYNAMIC_GEODETIC_REFERENCE_FRAME ||
159 type == PJ_TYPE_DYNAMIC_VERTICAL_REFERENCE_FRAME;
160 if ( !isDynamic )
161 {
162 const QString authName( proj_get_id_auth_name( datum.get(), 0 ) );
163 const QString code( proj_get_id_code( datum.get(), 0 ) );
164 if ( authName == QLatin1String( "EPSG" ) && code == QLatin1String( "6326" ) )
165 {
166 isDynamic = true;
167 }
168 }
169 }
170 else
171 {
172 proj_pj_unique_ptr ensemble( candidate ? proj_crs_get_datum_ensemble( context, candidate.get() ) : nullptr );
173 if ( ensemble )
174 {
175 proj_pj_unique_ptr member( proj_datum_ensemble_get_member( context, ensemble.get(), 0 ) );
176 if ( member )
177 {
178 const PJ_TYPE type = proj_get_type( member.get() );
179 isDynamic = type == PJ_TYPE_DYNAMIC_GEODETIC_REFERENCE_FRAME ||
180 type == PJ_TYPE_DYNAMIC_VERTICAL_REFERENCE_FRAME;
181 }
182 }
183 }
184 return isDynamic;
185}
186
188{
189 if ( !crs )
190 return nullptr;
191
192 PJ_CONTEXT *context = QgsProjContext::get();
193 switch ( proj_get_type( crs ) )
194 {
195 case PJ_TYPE_COMPOUND_CRS:
196 {
197 int i = 0;
198 QgsProjUtils::proj_pj_unique_ptr res( proj_crs_get_sub_crs( context, crs, i ) );
199 while ( res && ( proj_get_type( res.get() ) == PJ_TYPE_VERTICAL_CRS || proj_get_type( res.get() ) == PJ_TYPE_TEMPORAL_CRS ) )
200 {
201 i++;
202 res.reset( proj_crs_get_sub_crs( context, crs, i ) );
203 }
204 return res;
205 }
206
207 case PJ_TYPE_VERTICAL_CRS:
208 return nullptr;
209
210 // maybe other types to handle??
211
212 default:
213 return unboundCrs( crs );
214 }
215
216#ifndef _MSC_VER // unreachable
217 return nullptr;
218#endif
219}
220
222{
223 if ( !crs )
224 return nullptr;
225
226 PJ_CONTEXT *context = QgsProjContext::get();
227 switch ( proj_get_type( crs ) )
228 {
229 case PJ_TYPE_COMPOUND_CRS:
230 {
231 int i = 0;
232 QgsProjUtils::proj_pj_unique_ptr res( proj_crs_get_sub_crs( context, crs, i ) );
233 while ( res && ( proj_get_type( res.get() ) != PJ_TYPE_VERTICAL_CRS ) )
234 {
235 i++;
236 res.reset( proj_crs_get_sub_crs( context, crs, i ) );
237 }
238 return res;
239 }
240
241 case PJ_TYPE_VERTICAL_CRS:
242 return QgsProjUtils::proj_pj_unique_ptr( proj_clone( context, crs ) );
243
244 // maybe other types to handle??
245
246 default:
247 return nullptr;
248 }
249
251}
252
254{
255 if ( !crs )
256 return nullptr;
257
258 PJ_CONTEXT *context = QgsProjContext::get();
259 switch ( proj_get_type( crs ) )
260 {
261 case PJ_TYPE_BOUND_CRS:
262 return QgsProjUtils::proj_pj_unique_ptr( proj_get_source_crs( context, crs ) );
263
264 // maybe other types to handle??
265
266 default:
267 return QgsProjUtils::proj_pj_unique_ptr( proj_clone( context, crs ) );
268 }
269
270#ifndef _MSC_VER // unreachable
271 return nullptr;
272#endif
273}
274
276{
277 if ( !crs )
278 return nullptr;
279
280#if PROJ_VERSION_MAJOR>=8
281 PJ_CONTEXT *context = QgsProjContext::get();
283 if ( !candidate ) // purely vertical CRS
284 candidate = unboundCrs( crs );
285
286 if ( !candidate )
287 return nullptr;
288
289 return QgsProjUtils::proj_pj_unique_ptr( proj_crs_get_datum_ensemble( context, candidate.get() ) );
290#else
291 throw QgsNotSupportedException( QObject::tr( "Calculating datum ensembles requires a QGIS build based on PROJ 8.0 or later" ) );
292#endif
293}
294
296{
297 if ( !horizontalCrs || !verticalCrs )
298 return nullptr;
299
300 // const cast here is for compatibility with proj < 9.5
301 return QgsProjUtils::proj_pj_unique_ptr( proj_create_compound_crs( QgsProjContext::get(),
302 nullptr,
303 const_cast< PJ *>( horizontalCrs ),
304 const_cast< PJ * >( verticalCrs ) ) );
305}
306
307bool QgsProjUtils::identifyCrs( const PJ *crs, QString &authName, QString &authCode, IdentifyFlags flags )
308{
309 authName.clear();
310 authCode.clear();
311
312 if ( !crs )
313 return false;
314
315 int *confidence = nullptr;
316 if ( PJ_OBJ_LIST *crsList = proj_identify( QgsProjContext::get(), crs, nullptr, nullptr, &confidence ) )
317 {
318 const int count = proj_list_get_count( crsList );
319 int bestConfidence = 0;
321 for ( int i = 0; i < count; ++i )
322 {
323 if ( confidence[i] >= bestConfidence )
324 {
325 QgsProjUtils::proj_pj_unique_ptr candidateCrs( proj_list_get( QgsProjContext::get(), crsList, i ) );
326 switch ( proj_get_type( candidateCrs.get() ) )
327 {
328 case PJ_TYPE_BOUND_CRS:
329 // proj_identify also matches bound CRSes to the source CRS. But they are not the same as the source CRS, so we don't
330 // consider them a candidate for a match here (depending on the identify flags, that is!)
332 break;
333 else
334 continue;
335
336 default:
337 break;
338 }
339
340 candidateCrs = QgsProjUtils::unboundCrs( candidateCrs.get() );
341 const QString authName( proj_get_id_auth_name( candidateCrs.get(), 0 ) );
342 // if a match is identical confidence, we prefer EPSG codes for compatibility with earlier qgis conversions
343 if ( confidence[i] > bestConfidence || ( confidence[i] == bestConfidence && authName == QLatin1String( "EPSG" ) ) )
344 {
345 bestConfidence = confidence[i];
346 matchedCrs = std::move( candidateCrs );
347 }
348 }
349 }
350 proj_list_destroy( crsList );
351 proj_int_list_destroy( confidence );
352 if ( matchedCrs && bestConfidence >= 70 )
353 {
354 authName = QString( proj_get_id_auth_name( matchedCrs.get(), 0 ) );
355 authCode = QString( proj_get_id_code( matchedCrs.get(), 0 ) );
356 }
357 }
358 return !authName.isEmpty() && !authCode.isEmpty();
359}
360
362{
363 if ( projDef.isEmpty() )
364 return true;
365
366 PJ_CONTEXT *context = QgsProjContext::get();
367 QgsProjUtils::proj_pj_unique_ptr coordinateOperation( proj_create( context, projDef.toUtf8().constData() ) );
368 if ( !coordinateOperation )
369 return false;
370
371 return static_cast< bool >( proj_coordoperation_is_instantiable( context, coordinateOperation.get() ) );
372}
373
374QList<QgsDatumTransform::GridDetails> QgsProjUtils::gridsUsed( const QString &proj )
375{
376 const thread_local QRegularExpression regex( QStringLiteral( "\\+(?:nad)?grids=(.*?)\\s" ) );
377
378 QList< QgsDatumTransform::GridDetails > grids;
379 QRegularExpressionMatchIterator matches = regex.globalMatch( proj );
380 while ( matches.hasNext() )
381 {
382 const QRegularExpressionMatch match = matches.next();
383 const QString gridName = match.captured( 1 );
385 grid.shortName = gridName;
386 const char *fullName = nullptr;
387 const char *packageName = nullptr;
388 const char *url = nullptr;
389 int directDownload = 0;
390 int openLicense = 0;
391 int available = 0;
392 proj_grid_get_info_from_database( QgsProjContext::get(), gridName.toUtf8().constData(), &fullName, &packageName, &url, &directDownload, &openLicense, &available );
393 grid.fullName = QString( fullName );
394 grid.packageName = QString( packageName );
395 grid.url = QString( url );
396 grid.directDownload = directDownload;
397 grid.openLicense = openLicense;
398 grid.isAvailable = available;
399 grids.append( grid );
400 }
401 return grids;
402}
403
404#if 0
405QStringList QgsProjUtils::nonAvailableGrids( const QString &projDef )
406{
407 if ( projDef.isEmpty() )
408 return QStringList();
409
410 PJ_CONTEXT *context = QgsProjContext::get();
411 QgsProjUtils::proj_pj_unique_ptr op( proj_create( context, projDef.toUtf8().constData() ) ); < ---- - this always fails if grids are missing
412 if ( !op )
413 return QStringList();
414
415 QStringList res;
416 for ( int j = 0; j < proj_coordoperation_get_grid_used_count( context, op.get() ); ++j )
417 {
418 const char *shortName = nullptr;
419 int isAvailable = 0;
420 proj_coordoperation_get_grid_used( context, op.get(), j, &shortName, nullptr, nullptr, nullptr, nullptr, nullptr, &isAvailable );
421 if ( !isAvailable )
422 res << QString( shortName );
423 }
424 return res;
425}
426#endif
427
429{
430 return PROJ_VERSION_MAJOR;
431}
432
434{
435 return PROJ_VERSION_MINOR;
436}
437
439{
440 PJ_CONTEXT *context = QgsProjContext::get();
441 const char *version = proj_context_get_database_metadata( context, "EPSG.VERSION" );
442 return QString( version );
443}
444
446{
447 PJ_CONTEXT *context = QgsProjContext::get();
448 const char *date = proj_context_get_database_metadata( context, "EPSG.DATE" );
449 return QDate::fromString( date, Qt::DateFormat::ISODate );
450}
451
453{
454 PJ_CONTEXT *context = QgsProjContext::get();
455 const char *version = proj_context_get_database_metadata( context, "ESRI.VERSION" );
456 return QString( version );
457}
458
460{
461 PJ_CONTEXT *context = QgsProjContext::get();
462 const char *date = proj_context_get_database_metadata( context, "ESRI.DATE" );
463 return QDate::fromString( date, Qt::DateFormat::ISODate );
464}
465
467{
468 PJ_CONTEXT *context = QgsProjContext::get();
469 const char *version = proj_context_get_database_metadata( context, "IGNF.VERSION" );
470 return QString( version );
471}
472
474{
475 PJ_CONTEXT *context = QgsProjContext::get();
476 const char *date = proj_context_get_database_metadata( context, "IGNF.DATE" );
477 return QDate::fromString( date, Qt::DateFormat::ISODate );
478}
479
481{
482 const QString path( proj_info().searchpath );
483 QStringList paths;
484#ifdef Q_OS_WIN
485 paths = path.split( ';' );
486#else
487 paths = path.split( ':' );
488#endif
489
490 QSet<QString> existing;
491 // thin out duplicates from paths -- see https://github.com/OSGeo/proj.4/pull/1498
492 QStringList res;
493 res.reserve( paths.count() );
494 for ( const QString &p : std::as_const( paths ) )
495 {
496 if ( existing.contains( p ) )
497 continue;
498
499 existing.insert( p );
500 res << p;
501 }
502 return res;
503}
Custom exception class which is raised when an operation is not supported.
Definition: qgsexception.h:118
Used to create and store a proj context object, correctly freeing the context upon destruction.
Definition: qgsprojutils.h:264
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.
Definition: qgsprojutils.h:121
static QList< QgsDatumTransform::GridDetails > gridsUsed(const QString &proj)
Returns a list of grids used by the given proj string.
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 proj_pj_unique_ptr createCompoundCrs(const PJ *horizontalCrs, const PJ *verticalCrs)
Given a PROJ horizontal and vertical CRS, attempt to create a compound CRS from them.
static QStringList searchPaths()
Returns the current list of Proj file search paths.
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
Definition: qgsprojutils.h:123
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.
Definition: qgsprojutils.h:141
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:5853
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.