QGIS API Documentation  3.22.4-Białowieża (ce8e65e95e)
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"
19 #include "qgscoordinatetransform.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)
29 thread_local QgsProjContext QgsProjContext::sProjContext;
30 #else
31 QThreadStorage< 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 
72 bool QgsProjUtils::usesAngularUnit( const QString &projDef )
73 {
74  const QString crsDef = QStringLiteral( "%1 +type=crs" ).arg( projDef );
75  PJ_CONTEXT *context = QgsProjContext::get();
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 #if PROJ_VERSION_MAJOR > 7 || (PROJ_VERSION_MAJOR == 7 && PROJ_VERSION_MINOR >= 2)
167  else
168  {
169  proj_pj_unique_ptr ensemble( horiz ? proj_crs_get_datum_ensemble( context, horiz.get() ) : nullptr );
170  if ( ensemble )
171  {
172  proj_pj_unique_ptr member( proj_datum_ensemble_get_member( context, ensemble.get(), 0 ) );
173  if ( member )
174  {
175  const PJ_TYPE type = proj_get_type( member.get() );
176  isDynamic = type == PJ_TYPE_DYNAMIC_GEODETIC_REFERENCE_FRAME ||
177  type == PJ_TYPE_DYNAMIC_VERTICAL_REFERENCE_FRAME;
178  }
179  }
180  }
181 #endif
182  return isDynamic;
183 }
184 
186 {
187  if ( !crs )
188  return nullptr;
189 
190  PJ_CONTEXT *context = QgsProjContext::get();
191  switch ( proj_get_type( crs ) )
192  {
193  case PJ_TYPE_BOUND_CRS:
194  return QgsProjUtils::proj_pj_unique_ptr( proj_get_source_crs( context, 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  // maybe other types to handle??
209 
210  default:
211  return QgsProjUtils::proj_pj_unique_ptr( proj_clone( context, crs ) );
212  }
213 
214 #ifndef _MSC_VER // unreachable
215  return nullptr;
216 #endif
217 }
218 
220 {
221  if ( !crs )
222  return nullptr;
223 
224 #if PROJ_VERSION_MAJOR>=8
225  PJ_CONTEXT *context = QgsProjContext::get();
227  if ( !singleCrs )
228  return nullptr;
229 
230  return QgsProjUtils::proj_pj_unique_ptr( proj_crs_get_datum_ensemble( context, singleCrs.get() ) );
231 #else
232  throw QgsNotSupportedException( QStringLiteral( "Calculating datum ensembles requires a QGIS build based on PROJ 8.0 or later" ) );
233 #endif
234 }
235 
236 bool QgsProjUtils::identifyCrs( const PJ *crs, QString &authName, QString &authCode, IdentifyFlags flags )
237 {
238  authName.clear();
239  authCode.clear();
240 
241  if ( !crs )
242  return false;
243 
244  int *confidence = nullptr;
245  if ( PJ_OBJ_LIST *crsList = proj_identify( QgsProjContext::get(), crs, nullptr, nullptr, &confidence ) )
246  {
247  const int count = proj_list_get_count( crsList );
248  int bestConfidence = 0;
250  for ( int i = 0; i < count; ++i )
251  {
252  if ( confidence[i] >= bestConfidence )
253  {
254  QgsProjUtils::proj_pj_unique_ptr candidateCrs( proj_list_get( QgsProjContext::get(), crsList, i ) );
255  switch ( proj_get_type( candidateCrs.get() ) )
256  {
257  case PJ_TYPE_BOUND_CRS:
258  // proj_identify also matches bound CRSes to the source CRS. But they are not the same as the source CRS, so we don't
259  // consider them a candidate for a match here (depending on the identify flags, that is!)
261  break;
262  else
263  continue;
264 
265  default:
266  break;
267  }
268 
269  candidateCrs = QgsProjUtils::crsToSingleCrs( candidateCrs.get() );
270  const QString authName( proj_get_id_auth_name( candidateCrs.get(), 0 ) );
271  // if a match is identical confidence, we prefer EPSG codes for compatibility with earlier qgis conversions
272  if ( confidence[i] > bestConfidence || ( confidence[i] == bestConfidence && authName == QLatin1String( "EPSG" ) ) )
273  {
274  bestConfidence = confidence[i];
275  matchedCrs = std::move( candidateCrs );
276  }
277  }
278  }
279  proj_list_destroy( crsList );
280  proj_int_list_destroy( confidence );
281  if ( matchedCrs && bestConfidence >= 70 )
282  {
283  authName = QString( proj_get_id_auth_name( matchedCrs.get(), 0 ) );
284  authCode = QString( proj_get_id_code( matchedCrs.get(), 0 ) );
285  }
286  }
287  return !authName.isEmpty() && !authCode.isEmpty();
288 }
289 
290 bool QgsProjUtils::coordinateOperationIsAvailable( const QString &projDef )
291 {
292  if ( projDef.isEmpty() )
293  return true;
294 
295  PJ_CONTEXT *context = QgsProjContext::get();
296  QgsProjUtils::proj_pj_unique_ptr coordinateOperation( proj_create( context, projDef.toUtf8().constData() ) );
297  if ( !coordinateOperation )
298  return false;
299 
300  return static_cast< bool >( proj_coordoperation_is_instantiable( context, coordinateOperation.get() ) );
301 }
302 
303 QList<QgsDatumTransform::GridDetails> QgsProjUtils::gridsUsed( const QString &proj )
304 {
305  const thread_local QRegularExpression regex( QStringLiteral( "\\+(?:nad)?grids=(.*?)\\s" ) );
306 
307  QList< QgsDatumTransform::GridDetails > grids;
308  QRegularExpressionMatchIterator matches = regex.globalMatch( proj );
309  while ( matches.hasNext() )
310  {
311  const QRegularExpressionMatch match = matches.next();
312  const QString gridName = match.captured( 1 );
314  grid.shortName = gridName;
315  const char *fullName = nullptr;
316  const char *packageName = nullptr;
317  const char *url = nullptr;
318  int directDownload = 0;
319  int openLicense = 0;
320  int available = 0;
321  proj_grid_get_info_from_database( QgsProjContext::get(), gridName.toUtf8().constData(), &fullName, &packageName, &url, &directDownload, &openLicense, &available );
322  grid.fullName = QString( fullName );
323  grid.packageName = QString( packageName );
324  grid.url = QString( url );
325  grid.directDownload = directDownload;
326  grid.openLicense = openLicense;
327  grid.isAvailable = available;
328  grids.append( grid );
329  }
330  return grids;
331 }
332 
333 #if 0
334 QStringList QgsProjUtils::nonAvailableGrids( const QString &projDef )
335 {
336  if ( projDef.isEmpty() )
337  return QStringList();
338 
339  PJ_CONTEXT *context = QgsProjContext::get();
340  QgsProjUtils::proj_pj_unique_ptr op( proj_create( context, projDef.toUtf8().constData() ) ); < ---- - this always fails if grids are missing
341  if ( !op )
342  return QStringList();
343 
344  QStringList res;
345  for ( int j = 0; j < proj_coordoperation_get_grid_used_count( context, op.get() ); ++j )
346  {
347  const char *shortName = nullptr;
348  int isAvailable = 0;
349  proj_coordoperation_get_grid_used( context, op.get(), j, &shortName, nullptr, nullptr, nullptr, nullptr, nullptr, &isAvailable );
350  if ( !isAvailable )
351  res << QString( shortName );
352  }
353  return res;
354 }
355 #endif
356 
358 {
359  return PROJ_VERSION_MAJOR;
360 }
361 
363 {
364  return PROJ_VERSION_MINOR;
365 }
366 
368 {
369  PJ_CONTEXT *context = QgsProjContext::get();
370  const char *version = proj_context_get_database_metadata( context, "EPSG.VERSION" );
371  return QString( version );
372 }
373 
375 {
376  PJ_CONTEXT *context = QgsProjContext::get();
377  const char *date = proj_context_get_database_metadata( context, "EPSG.DATE" );
378  return QDate::fromString( date, Qt::DateFormat::ISODate );
379 }
380 
382 {
383  PJ_CONTEXT *context = QgsProjContext::get();
384  const char *version = proj_context_get_database_metadata( context, "ESRI.VERSION" );
385  return QString( version );
386 }
387 
389 {
390  PJ_CONTEXT *context = QgsProjContext::get();
391  const char *date = proj_context_get_database_metadata( context, "ESRI.DATE" );
392  return QDate::fromString( date, Qt::DateFormat::ISODate );
393 }
394 
396 {
397  PJ_CONTEXT *context = QgsProjContext::get();
398  const char *version = proj_context_get_database_metadata( context, "IGNF.VERSION" );
399  return QString( version );
400 }
401 
403 {
404  PJ_CONTEXT *context = QgsProjContext::get();
405  const char *date = proj_context_get_database_metadata( context, "IGNF.DATE" );
406  return QDate::fromString( date, Qt::DateFormat::ISODate );
407 }
408 
410 {
411  const QString path( proj_info().searchpath );
412  QStringList paths;
413 #ifdef Q_OS_WIN
414  paths = path.split( ';' );
415 #else
416  paths = path.split( ':' );
417 #endif
418 
419  QSet<QString> existing;
420  // thin out duplicates from paths -- see https://github.com/OSGeo/proj.4/pull/1498
421  QStringList res;
422  res.reserve( paths.count() );
423  for ( const QString &p : std::as_const( paths ) )
424  {
425  if ( existing.contains( p ) )
426  continue;
427 
428  existing.insert( p );
429  res << p;
430  }
431  return res;
432 }
433 
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)
Destroys an PJ object, using the correct proj calls.