QGIS API Documentation 3.28.0-Firenze (ed3ad0430f)
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
28#if defined(USE_THREAD_LOCAL) && !defined(Q_OS_WIN)
29thread_local QgsProjContext QgsProjContext::sProjContext;
30#else
31QThreadStorage< QgsProjContext * > QgsProjContext::sProjContext;
32#endif
33
35{
36 mContext = proj_context_create();
37}
38
40{
41 // Call removeFromCacheObjectsBelongingToCurrentThread() before
42 // destroying the context
43 QgsCoordinateTransform::removeFromCacheObjectsBelongingToCurrentThread( mContext );
44 QgsCoordinateReferenceSystem::removeFromCacheObjectsBelongingToCurrentThread( mContext );
45 proj_context_destroy( mContext );
46}
47
49{
50#if defined(USE_THREAD_LOCAL) && !defined(Q_OS_WIN)
51 return sProjContext.mContext;
52#else
53 PJ_CONTEXT *pContext = nullptr;
54 if ( sProjContext.hasLocalData() )
55 {
56 pContext = sProjContext.localData()->mContext;
57 }
58 else
59 {
60 sProjContext.setLocalData( new QgsProjContext() );
61 pContext = sProjContext.localData()->mContext;
62 }
63 return pContext;
64#endif
65}
66
68{
69 proj_destroy( object );
70}
71
72bool QgsProjUtils::usesAngularUnit( const QString &projDef )
73{
74 const QString crsDef = QStringLiteral( "%1 +type=crs" ).arg( projDef );
76 QgsProjUtils::proj_pj_unique_ptr projSingleOperation( proj_create( context, crsDef.toUtf8().constData() ) );
77 if ( !projSingleOperation )
78 return false;
79
80 QgsProjUtils::proj_pj_unique_ptr coordinateSystem( proj_crs_get_coordinate_system( context, projSingleOperation.get() ) );
81 if ( !coordinateSystem )
82 return false;
83
84 const int axisCount = proj_cs_get_axis_count( context, coordinateSystem.get() );
85 if ( axisCount > 0 )
86 {
87 const char *outUnitAuthName = nullptr;
88 const char *outUnitAuthCode = nullptr;
89 // Read only first axis
90 proj_cs_get_axis_info( context, coordinateSystem.get(), 0,
91 nullptr,
92 nullptr,
93 nullptr,
94 nullptr,
95 nullptr,
96 &outUnitAuthName,
97 &outUnitAuthCode );
98
99 if ( outUnitAuthName && outUnitAuthCode )
100 {
101 const char *unitCategory = nullptr;
102 if ( proj_uom_get_info_from_database( context, outUnitAuthName, outUnitAuthCode, nullptr, nullptr, &unitCategory ) )
103 {
104 return QString( unitCategory ).compare( QLatin1String( "angular" ), Qt::CaseInsensitive ) == 0;
105 }
106 }
107 }
108 return false;
109}
110
112{
113 //ported from https://github.com/pramsey/postgis/blob/7ecf6839c57a838e2c8540001a3cd35b78a730db/liblwgeom/lwgeom_transform.c#L299
114 if ( !crs )
115 return false;
116
117 PJ_CONTEXT *context = QgsProjContext::get();
118 QgsProjUtils::proj_pj_unique_ptr pjCs( proj_crs_get_coordinate_system( context, crs ) );
119 if ( !pjCs )
120 return false;
121
122 const int axisCount = proj_cs_get_axis_count( context, pjCs.get() );
123 if ( axisCount > 0 )
124 {
125 const char *outDirection = nullptr;
126 // Read only first axis, see if it is degrees / north
127
128 proj_cs_get_axis_info( context, pjCs.get(), 0,
129 nullptr,
130 nullptr,
131 &outDirection,
132 nullptr,
133 nullptr,
134 nullptr,
135 nullptr
136 );
137 return QString( outDirection ).compare( QLatin1String( "north" ), Qt::CaseInsensitive ) == 0;
138 }
139 return false;
140}
141
143{
144 // ported from GDAL OGRSpatialReference::IsDynamic()
145 bool isDynamic = false;
146 PJ_CONTEXT *context = QgsProjContext::get();
147
149
150 proj_pj_unique_ptr datum( horiz ? proj_crs_get_datum( context, horiz.get() ) : nullptr );
151 if ( datum )
152 {
153 const PJ_TYPE type = proj_get_type( datum.get() );
154 isDynamic = type == PJ_TYPE_DYNAMIC_GEODETIC_REFERENCE_FRAME ||
155 type == PJ_TYPE_DYNAMIC_VERTICAL_REFERENCE_FRAME;
156 if ( !isDynamic )
157 {
158 const QString authName( proj_get_id_auth_name( datum.get(), 0 ) );
159 const QString code( proj_get_id_code( datum.get(), 0 ) );
160 if ( authName == QLatin1String( "EPSG" ) && code == QLatin1String( "6326" ) )
161 {
162 isDynamic = true;
163 }
164 }
165 }
166 else
167 {
168 proj_pj_unique_ptr ensemble( horiz ? proj_crs_get_datum_ensemble( context, horiz.get() ) : nullptr );
169 if ( ensemble )
170 {
171 proj_pj_unique_ptr member( proj_datum_ensemble_get_member( context, ensemble.get(), 0 ) );
172 if ( member )
173 {
174 const PJ_TYPE type = proj_get_type( member.get() );
175 isDynamic = type == PJ_TYPE_DYNAMIC_GEODETIC_REFERENCE_FRAME ||
176 type == PJ_TYPE_DYNAMIC_VERTICAL_REFERENCE_FRAME;
177 }
178 }
179 }
180 return isDynamic;
181}
182
184{
185 if ( !crs )
186 return nullptr;
187
188 PJ_CONTEXT *context = QgsProjContext::get();
189 switch ( proj_get_type( crs ) )
190 {
191 case PJ_TYPE_BOUND_CRS:
192 return QgsProjUtils::proj_pj_unique_ptr( proj_get_source_crs( context, crs ) );
193
194 case PJ_TYPE_COMPOUND_CRS:
195 {
196 int i = 0;
197 QgsProjUtils::proj_pj_unique_ptr res( proj_crs_get_sub_crs( context, crs, i ) );
198 while ( res && ( proj_get_type( res.get() ) == PJ_TYPE_VERTICAL_CRS || proj_get_type( res.get() ) == PJ_TYPE_TEMPORAL_CRS ) )
199 {
200 i++;
201 res.reset( proj_crs_get_sub_crs( context, crs, i ) );
202 }
203 return res;
204 }
205
206 // maybe other types to handle??
207
208 default:
209 return QgsProjUtils::proj_pj_unique_ptr( proj_clone( context, crs ) );
210 }
211
212#ifndef _MSC_VER // unreachable
213 return nullptr;
214#endif
215}
216
218{
219 if ( !crs )
220 return nullptr;
221
222#if PROJ_VERSION_MAJOR>=8
223 PJ_CONTEXT *context = QgsProjContext::get();
225 if ( !singleCrs )
226 return nullptr;
227
228 return QgsProjUtils::proj_pj_unique_ptr( proj_crs_get_datum_ensemble( context, singleCrs.get() ) );
229#else
230 throw QgsNotSupportedException( QObject::tr( "Calculating datum ensembles requires a QGIS build based on PROJ 8.0 or later" ) );
231#endif
232}
233
234bool QgsProjUtils::identifyCrs( const PJ *crs, QString &authName, QString &authCode, IdentifyFlags flags )
235{
236 authName.clear();
237 authCode.clear();
238
239 if ( !crs )
240 return false;
241
242 int *confidence = nullptr;
243 if ( PJ_OBJ_LIST *crsList = proj_identify( QgsProjContext::get(), crs, nullptr, nullptr, &confidence ) )
244 {
245 const int count = proj_list_get_count( crsList );
246 int bestConfidence = 0;
248 for ( int i = 0; i < count; ++i )
249 {
250 if ( confidence[i] >= bestConfidence )
251 {
252 QgsProjUtils::proj_pj_unique_ptr candidateCrs( proj_list_get( QgsProjContext::get(), crsList, i ) );
253 switch ( proj_get_type( candidateCrs.get() ) )
254 {
255 case PJ_TYPE_BOUND_CRS:
256 // proj_identify also matches bound CRSes to the source CRS. But they are not the same as the source CRS, so we don't
257 // consider them a candidate for a match here (depending on the identify flags, that is!)
259 break;
260 else
261 continue;
262
263 default:
264 break;
265 }
266
267 candidateCrs = QgsProjUtils::crsToSingleCrs( candidateCrs.get() );
268 const QString authName( proj_get_id_auth_name( candidateCrs.get(), 0 ) );
269 // if a match is identical confidence, we prefer EPSG codes for compatibility with earlier qgis conversions
270 if ( confidence[i] > bestConfidence || ( confidence[i] == bestConfidence && authName == QLatin1String( "EPSG" ) ) )
271 {
272 bestConfidence = confidence[i];
273 matchedCrs = std::move( candidateCrs );
274 }
275 }
276 }
277 proj_list_destroy( crsList );
278 proj_int_list_destroy( confidence );
279 if ( matchedCrs && bestConfidence >= 70 )
280 {
281 authName = QString( proj_get_id_auth_name( matchedCrs.get(), 0 ) );
282 authCode = QString( proj_get_id_code( matchedCrs.get(), 0 ) );
283 }
284 }
285 return !authName.isEmpty() && !authCode.isEmpty();
286}
287
289{
290 if ( projDef.isEmpty() )
291 return true;
292
293 PJ_CONTEXT *context = QgsProjContext::get();
294 QgsProjUtils::proj_pj_unique_ptr coordinateOperation( proj_create( context, projDef.toUtf8().constData() ) );
295 if ( !coordinateOperation )
296 return false;
297
298 return static_cast< bool >( proj_coordoperation_is_instantiable( context, coordinateOperation.get() ) );
299}
300
301QList<QgsDatumTransform::GridDetails> QgsProjUtils::gridsUsed( const QString &proj )
302{
303 const thread_local QRegularExpression regex( QStringLiteral( "\\+(?:nad)?grids=(.*?)\\s" ) );
304
305 QList< QgsDatumTransform::GridDetails > grids;
306 QRegularExpressionMatchIterator matches = regex.globalMatch( proj );
307 while ( matches.hasNext() )
308 {
309 const QRegularExpressionMatch match = matches.next();
310 const QString gridName = match.captured( 1 );
312 grid.shortName = gridName;
313 const char *fullName = nullptr;
314 const char *packageName = nullptr;
315 const char *url = nullptr;
316 int directDownload = 0;
317 int openLicense = 0;
318 int available = 0;
319 proj_grid_get_info_from_database( QgsProjContext::get(), gridName.toUtf8().constData(), &fullName, &packageName, &url, &directDownload, &openLicense, &available );
320 grid.fullName = QString( fullName );
321 grid.packageName = QString( packageName );
322 grid.url = QString( url );
323 grid.directDownload = directDownload;
324 grid.openLicense = openLicense;
325 grid.isAvailable = available;
326 grids.append( grid );
327 }
328 return grids;
329}
330
331#if 0
332QStringList QgsProjUtils::nonAvailableGrids( const QString &projDef )
333{
334 if ( projDef.isEmpty() )
335 return QStringList();
336
337 PJ_CONTEXT *context = QgsProjContext::get();
338 QgsProjUtils::proj_pj_unique_ptr op( proj_create( context, projDef.toUtf8().constData() ) ); < ---- - this always fails if grids are missing
339 if ( !op )
340 return QStringList();
341
342 QStringList res;
343 for ( int j = 0; j < proj_coordoperation_get_grid_used_count( context, op.get() ); ++j )
344 {
345 const char *shortName = nullptr;
346 int isAvailable = 0;
347 proj_coordoperation_get_grid_used( context, op.get(), j, &shortName, nullptr, nullptr, nullptr, nullptr, nullptr, &isAvailable );
348 if ( !isAvailable )
349 res << QString( shortName );
350 }
351 return res;
352}
353#endif
354
356{
357 return PROJ_VERSION_MAJOR;
358}
359
361{
362 return PROJ_VERSION_MINOR;
363}
364
366{
367 PJ_CONTEXT *context = QgsProjContext::get();
368 const char *version = proj_context_get_database_metadata( context, "EPSG.VERSION" );
369 return QString( version );
370}
371
373{
374 PJ_CONTEXT *context = QgsProjContext::get();
375 const char *date = proj_context_get_database_metadata( context, "EPSG.DATE" );
376 return QDate::fromString( date, Qt::DateFormat::ISODate );
377}
378
380{
381 PJ_CONTEXT *context = QgsProjContext::get();
382 const char *version = proj_context_get_database_metadata( context, "ESRI.VERSION" );
383 return QString( version );
384}
385
387{
388 PJ_CONTEXT *context = QgsProjContext::get();
389 const char *date = proj_context_get_database_metadata( context, "ESRI.DATE" );
390 return QDate::fromString( date, Qt::DateFormat::ISODate );
391}
392
394{
395 PJ_CONTEXT *context = QgsProjContext::get();
396 const char *version = proj_context_get_database_metadata( context, "IGNF.VERSION" );
397 return QString( version );
398}
399
401{
402 PJ_CONTEXT *context = QgsProjContext::get();
403 const char *date = proj_context_get_database_metadata( context, "IGNF.DATE" );
404 return QDate::fromString( date, Qt::DateFormat::ISODate );
405}
406
408{
409 const QString path( proj_info().searchpath );
410 QStringList paths;
411#ifdef Q_OS_WIN
412 paths = path.split( ';' );
413#else
414 paths = path.split( ':' );
415#endif
416
417 QSet<QString> existing;
418 // thin out duplicates from paths -- see https://github.com/OSGeo/proj.4/pull/1498
419 QStringList res;
420 res.reserve( paths.count() );
421 for ( const QString &p : std::as_const( paths ) )
422 {
423 if ( existing.contains( p ) )
424 continue;
425
426 existing.insert( p );
427 res << p;
428 }
429 return res;
430}
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:231
static PJ_CONTEXT * get()
Returns a thread local instance of a proj context, safe for use in the current thread.
@ 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 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 QString ignfDatabaseVersion()
Returns the IGNF database version used by the proj library (e.g.
static proj_pj_unique_ptr crsToSingleCrs(const PJ *crs)
Given a PROJ crs (which may be a compound or bound crs, or some other type), extract a single crs fro...
static proj_pj_unique_ptr crsToDatumEnsemble(const PJ *crs)
Given a PROJ crs, attempt to retrieve the datum ensemble from it.
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.
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.