33#include <QApplication>
49QReadWriteLock QgsCoordinateTransform::sCacheLock;
51bool QgsCoordinateTransform::sDisableCache =
false;
55 const QString &desiredOperation )> QgsCoordinateTransform::sFallbackOperationOccurredHandler =
nullptr;
59 d =
new QgsCoordinateTransformPrivate();
65 d =
new QgsCoordinateTransformPrivate( source, destination, mContext );
68 mIgnoreImpossible =
true;
80 if ( !d->checkValidity() )
84 if ( !setFromCache( d->mSourceCRS, d->mDestCRS, d->mProjCoordinateOperation, d->mAllowFallbackTransforms ) )
92 mBallparkTransformsAreAppropriate =
true;
98 d =
new QgsCoordinateTransformPrivate( source, destination, mContext );
105 mIgnoreImpossible =
true;
113 if ( !d->checkValidity() )
117 if ( !setFromCache( d->mSourceCRS, d->mDestCRS, d->mProjCoordinateOperation, d->mAllowFallbackTransforms ) )
125 mBallparkTransformsAreAppropriate =
true;
130 d =
new QgsCoordinateTransformPrivate( source, destination, sourceDatumTransform, destinationDatumTransform );
135 if ( !d->checkValidity() )
139 if ( !setFromCache( d->mSourceCRS, d->mDestCRS, d->mProjCoordinateOperation, d->mAllowFallbackTransforms ) )
148 : mContext( o.mContext )
150 , mHasContext( o.mHasContext )
156 , mIgnoreImpossible( false )
157 , mBallparkTransformsAreAppropriate( false )
158 , mDisableFallbackHandler( false )
159 , mFallbackOperationOccurred( false )
168 mHasContext = o.mHasContext;
170 mContext = o.mContext;
171 mLastError = QString();
179 return d->mSourceCRS == other.d->mSourceCRS
180 && d->mDestCRS == other.d->mDestCRS
181 && mBallparkTransformsAreAppropriate == other.mBallparkTransformsAreAppropriate
182 && d->mProjCoordinateOperation == other.d->mProjCoordinateOperation
188 return !( *
this == other );
213 if ( !d->checkValidity() )
216 d->calculateTransforms( mContext );
218 if ( !setFromCache( d->mSourceCRS, d->mDestCRS, d->mProjCoordinateOperation, d->mAllowFallbackTransforms ) )
236 if ( !d->checkValidity() )
239 d->calculateTransforms( mContext );
241 if ( !setFromCache( d->mSourceCRS, d->mDestCRS, d->mProjCoordinateOperation, d->mAllowFallbackTransforms ) )
263 if ( !d->checkValidity() )
266 d->calculateTransforms( mContext );
268 if ( !setFromCache( d->mSourceCRS, d->mDestCRS, d->mProjCoordinateOperation, d->mAllowFallbackTransforms ) )
283 return d->mSourceCRS;
293 if ( !d->mIsValid || d->mShortCircuit )
297 double x = point.
x();
298 double y = point.
y();
331 if ( !d->mIsValid || d->mShortCircuit )
355#ifdef COORDINATE_TRANSFORM_VERBOSE
367 double x = point.
x();
368 double y = point.
y();
369 double z = point.
z();
386 if ( !d->mIsValid || d->mShortCircuit )
407 double xd =
static_cast< double >( x ), yd =
static_cast< double >( y );
416 if ( !d->mIsValid || d->mShortCircuit )
442 if ( !d->mIsValid || d->mShortCircuit )
448 const int nVertices = poly.size();
450 QVector<double> x( nVertices );
451 QVector<double> y( nVertices );
452 QVector<double> z( nVertices );
453 double *destX = x.data();
454 double *destY = y.data();
455 double *destZ = z.data();
457 const QPointF *polyData = poly.constData();
458 for (
int i = 0; i < nVertices; ++i )
460 *destX++ = polyData->x();
461 *destY++ = polyData->y();
477 QPointF *destPoint = poly.data();
478 const double *srcX = x.constData();
479 const double *srcY = y.constData();
480 for (
int i = 0; i < nVertices; ++i )
482 destPoint->rx() = *srcX++;
483 destPoint->ry() = *srcY++;
488 if ( !err.isEmpty() )
496 if ( !d->mIsValid || d->mShortCircuit )
499 Q_ASSERT( x.size() == y.size() );
522 if ( !d->mIsValid || d->mShortCircuit )
525 Q_ASSERT( x.size() == y.size() );
535 const int vectorSize = x.size();
536 QVector<double> xd( x.size() );
537 QVector<double> yd( y.size() );
538 QVector<double> zd( z.size() );
540 double *destX = xd.data();
541 double *destY = yd.data();
542 double *destZ = zd.data();
544 const float *srcX = x.constData();
545 const float *srcY = y.constData();
546 const float *srcZ = z.constData();
548 for (
int i = 0; i < vectorSize; ++i )
550 *destX++ =
static_cast< double >( *srcX++ );
551 *destY++ =
static_cast< double >( *srcY++ );
552 *destZ++ =
static_cast< double >( *srcZ++ );
558 float *destFX = x.data();
559 float *destFY = y.data();
560 float *destFZ = z.data();
561 const double *srcXD = xd.constData();
562 const double *srcYD = yd.constData();
563 const double *srcZD = zd.constData();
564 for (
int i = 0; i < vectorSize; ++i )
566 *destFX++ =
static_cast< float >( *srcXD++ );
567 *destFY++ =
static_cast< float >( *srcYD++ );
568 *destFZ++ =
static_cast< float >( *srcZD++ );
586 if ( !d->mIsValid || d->mShortCircuit )
598 QgsDebugMsgLevel( QStringLiteral(
"No QgsCoordinateTransformContext context set for transform" ), 4 );
606 throw QgsCsException( QObject::tr(
"Could not transform bounding box for geocentric CRS %1" ).arg( d->mSourceCRS.authid() ) );
610 throw QgsCsException( QObject::tr(
"Could not transform bounding box for geocentric CRS %1" ).arg( d->mDestCRS.authid() ) );
613 const double xMin = rect.
xMinimum();
614 const double xMax = rect.
xMaximum();
617 if ( d->mGeographicToWebMercator &&
626 constexpr double EPS = 1e-1;
627 if ( yMin < -90 + EPS )
629 if ( yMax < -90 + EPS )
630 throw QgsCsException( QObject::tr(
"Could not transform bounding box to target CRS" ) );
633 if ( yMax > 90 - EPS )
635 if ( yMin > 90 - EPS )
636 throw QgsCsException( QObject::tr(
"Could not transform bounding box to target CRS" ) );
643 QgsDebugMsgLevel( QStringLiteral(
"Entering transformBoundingBox..." ), 4 );
645#if PROJ_VERSION_MAJOR< 9 || (PROJ_VERSION_MAJOR==9 && PROJ_VERSION_MINOR<7)
646 const auto legacyImplementation = [
this, &rect, xMin, yMin, yMax, direction, handle180Crossover]()
654 const int nPoints = 1000;
655 const double dst = std::sqrt( ( rect.
width() * ( yMax - yMin ) ) / std::pow( std::sqrt(
static_cast< double >( nPoints ) ) - 1, 2.0 ) );
656 const int nXPoints =
static_cast<int>( std::clamp( std::ceil( rect.
width() / dst ) + 1, 3.0, 1000.0 ) );
657 const int nYPoints =
static_cast<int>( std::clamp( std::ceil( ( yMax - yMin ) / dst ) + 1, 3.0, 1000.0 ) );
662 std::vector<double> x( nXPoints *
static_cast< std::size_t
>( nYPoints ) );
663 std::vector<double> y( nXPoints *
static_cast< std::size_t
>( nYPoints ) );
664 std::vector<double> z( nXPoints *
static_cast< std::size_t
>( nYPoints ) );
668 const double dx = rect.
width() /
static_cast< double >( nXPoints - 1 );
669 const double dy = ( yMax - yMin ) /
static_cast< double >( nYPoints - 1 );
671 double pointY = yMin;
673 for (
int i = 0; i < nYPoints ; i++ )
677 double pointX = xMin;
679 for (
int j = 0; j < nXPoints; j++ )
681 x[( i * nXPoints ) + j] = pointX;
682 y[( i * nXPoints ) + j] = pointY;
684 z[( i * nXPoints ) + j] = 0.0;
695 transformCoords( nXPoints * nYPoints, x.data(), y.data(), z.data(), direction );
705 bool doHandle180Crossover =
false;
708 const double xMin = std::fmod( x[0], 180.0 );
709 const double xMax = std::fmod( x[nXPoints - 1], 180.0 );
710 if ( handle180Crossover
713 && xMin > 0.0 && xMin <= 180.0 && xMax < 0.0 && xMax >= -180.0 )
715 doHandle180Crossover =
true;
720 for (
int i = 0; i < nXPoints * nYPoints; i++ )
722 if ( !std::isfinite( x[i] ) || !std::isfinite( y[i] ) )
727 if ( doHandle180Crossover )
741 throw QgsCsException( QObject::tr(
"Could not transform bounding box to target CRS" ) );
744 if ( doHandle180Crossover )
765#if PROJ_VERSION_MAJOR>8 || (PROJ_VERSION_MAJOR==8 && PROJ_VERSION_MINOR>=2)
767#if PROJ_VERSION_MAJOR< 9 || (PROJ_VERSION_MAJOR==9 && PROJ_VERSION_MINOR<7)
775 return legacyImplementation();
779 ProjData projData = d->threadLocalProjData();
782#if PROJ_VERSION_MAJOR< 9 || (PROJ_VERSION_MAJOR==9 && PROJ_VERSION_MINOR<6)
797 transform2D.reset( proj_create_crs_to_crs_from_pj( projContext, srcCrsHorizontal.get(), destCrsHorizontal.get(),
nullptr,
nullptr ) );
800 const QString err = QStringLiteral(
"proj_create_crs_to_crs_from_pj(horizontalCrs(%1), horizontalCrs(%2)) failed" ).arg( d->mSourceCRS.authid(), d->mSourceCRS.authid() );
801 throw QgsCsException( QObject::tr(
"Could not transform bounding box to target CRS: %1" ).arg( err ) );
803 transform2D.reset( proj_normalize_for_visualization( projContext, transform2D.get() ) );
806 const QString err = QStringLiteral(
"Cannot normalize transform between horizontalCrs(%1) and horizontalCrs(%2)" ).arg( d->mSourceCRS.authid(), d->mDestCRS.authid() );
807 throw QgsCsException( QObject::tr(
"Could not transform bounding box to target CRS: %1" ).arg( err ) );
809 projData = transform2D.get();
813 double transXMin = 0;
814 double transYMin = 0;
815 double transXMax = 0;
816 double transYMax = 0;
818 proj_errno_reset( projData );
821 constexpr int DENSIFY_POINTS = 30;
823 xMin, yMin, xMax, yMax,
824 &transXMin, &transYMin, &transXMax, &transYMax, DENSIFY_POINTS );
826 if ( ( projResult != 1
827 || !std::isfinite( transXMin )
828 || !std::isfinite( transXMax )
829 || !std::isfinite( transYMin )
830 || !std::isfinite( transYMax ) )
831 && ( d->mAvailableOpCount > 1 || d->mAvailableOpCount == -1 )
835 if (
PJ *
transform = d->threadLocalFallbackProjData() )
838 xMin, yMin, xMax, yMax,
839 &transXMin, &transYMin, &transXMax, &transYMax, DENSIFY_POINTS );
844 || !std::isfinite( transXMin )
845 || !std::isfinite( transXMax )
846 || !std::isfinite( transYMin )
847 || !std::isfinite( transYMax ) )
849#if PROJ_VERSION_MAJOR< 9 || (PROJ_VERSION_MAJOR==9 && PROJ_VERSION_MINOR<7)
853 return legacyImplementation();
855 const QString projErr = QString::fromUtf8( proj_context_errno_string( projContext, proj_errno( projData ) ) );
857 const QString msg = QObject::tr(
"%1 (%2 to %3) of bounding box failed: %4" )
868 bool doHandle180Crossover =
false;
870 if ( handle180Crossover
873 && ( transXMax < transXMin ) )
876 std::swap( transXMax, transXMin );
881 doHandle180Crossover =
true;
884 QgsRectangle boundingBoxRect{ transXMin, transYMin, transXMax, transYMax };
885 if ( boundingBoxRect.isNull() )
888 throw QgsCsException( QObject::tr(
"Could not transform bounding box to target CRS" ) );
891 if ( doHandle180Crossover )
894 if ( boundingBoxRect.xMinimum() > 180.0 )
895 boundingBoxRect.setXMinimum( boundingBoxRect.xMinimum() - 360.0 );
896 if ( boundingBoxRect.xMaximum() > 180.0 )
897 boundingBoxRect.setXMaximum( boundingBoxRect.xMaximum() - 360.0 );
902 if ( boundingBoxRect.isEmpty() )
907 return boundingBoxRect;
909#if PROJ_VERSION_MAJOR< 9 || (PROJ_VERSION_MAJOR==9 && PROJ_VERSION_MINOR<7)
910 return legacyImplementation();
916 if ( !d->mIsValid || d->mShortCircuit )
919 if ( !d->mSourceCRS.isValid() )
922 "The coordinates can not be reprojected. The CRS is: %1" )
923 .arg( d->mSourceCRS.toProj() ), QObject::tr(
"CRS" ) );
926 if ( !d->mDestCRS.isValid() )
929 "The coordinates can not be reprojected. The CRS is: %1" ).arg( d->mDestCRS.toProj() ), QObject::tr(
"CRS" ) );
933 std::vector< int > zNanPositions;
934 for (
int i = 0; i < numPoints; i++ )
936 if ( std::isnan( z[i] ) )
938 zNanPositions.push_back( i );
943 std::vector< double > xprev( numPoints );
944 memcpy( xprev.data(), x,
sizeof(
double ) * numPoints );
945 std::vector< double > yprev( numPoints );
946 memcpy( yprev.data(), y,
sizeof(
double ) * numPoints );
947 std::vector< double > zprev( numPoints );
948 memcpy( zprev.data(), z,
sizeof(
double ) * numPoints );
950 const bool useTime = !std::isnan( d->mDefaultTime );
951 std::vector< double > t( useTime ? numPoints : 0, d->mDefaultTime );
953#ifdef COORDINATE_TRANSFORM_VERBOSE
956 QgsDebugMsgLevel( QStringLiteral(
"[[[[[[ Number of points to transform: %1 ]]]]]]" ).arg( numPoints ), 2 );
962 QgsDebugMsgLevel( QStringLiteral(
"No QgsCoordinateTransformContext context set for transform" ), 4 );
967 ProjData projData = d->threadLocalProjData();
971 proj_errno_reset( projData );
973 x,
sizeof( double ), numPoints,
974 y,
sizeof(
double ), numPoints,
975 z,
sizeof( double ), numPoints,
976 useTime ? t.data() :
nullptr,
sizeof( double ), useTime ? numPoints : 0 );
986 if ( numPoints == 1 )
988 projResult = proj_errno( projData );
989 actualRes = projResult;
993 actualRes = proj_errno( projData );
995 if ( actualRes == 0 )
999 if ( std::any_of( x, x + numPoints, [](
double v ) {
return std::isinf( v ); } )
1000 || std::any_of( y, y + numPoints, [](
double v ) {
return std::isinf( v ); } )
1001 || std::any_of( z, z + numPoints, [](
double v ) {
return std::isinf( v ); } ) )
1007 mFallbackOperationOccurred =
false;
1008 bool errorOccurredDuringFallbackOperation =
false;
1010 && ( d->mAvailableOpCount > 1 || d->mAvailableOpCount == -1 )
1011 && ( d->mAllowFallbackTransforms || mBallparkTransformsAreAppropriate ) )
1014 if (
PJ *
transform = d->threadLocalFallbackProjData() )
1018 memcpy( x, xprev.data(),
sizeof(
double ) * numPoints );
1019 memcpy( y, yprev.data(),
sizeof(
double ) * numPoints );
1020 memcpy( z, zprev.data(),
sizeof(
double ) * numPoints );
1022 x,
sizeof( double ), numPoints,
1023 y,
sizeof(
double ), numPoints,
1024 z,
sizeof( double ), numPoints,
1025 useTime ? t.data() :
nullptr,
sizeof( double ), useTime ? numPoints : 0 );
1034 if ( numPoints == 1 )
1040 errorOccurredDuringFallbackOperation = std::isinf( x[0] ) || std::isinf( y[0] ) || std::isinf( z[0] );
1043 if ( !errorOccurredDuringFallbackOperation )
1045 mFallbackOperationOccurred =
true;
1048 if ( !mBallparkTransformsAreAppropriate && !mDisableFallbackHandler && sFallbackOperationOccurredHandler )
1050 sFallbackOperationOccurredHandler( d->mSourceCRS, d->mDestCRS, d->mProjCoordinateOperation );
1052 const QString warning = QStringLiteral(
"A fallback coordinate operation was used between %1 and %2" ).arg( d->mSourceCRS.authid(),
1053 d->mDestCRS.authid() );
1054 qWarning(
"%s", warning.toLatin1().constData() );
1060 for (
const int &pos : zNanPositions )
1062 z[pos] = std::numeric_limits<double>::quiet_NaN();
1065 if ( projResult != 0 || errorOccurredDuringFallbackOperation )
1070 const QChar delim = numPoints > 1 ?
'\n' :
' ';
1071 for (
int i = 0; i < numPoints; ++i )
1073 points += QStringLiteral(
"(%1, %2)" ).arg( xprev[i], 0,
'f' ).arg( yprev[i], 0,
'f' ) + delim;
1079 const QString projError = !errorOccurredDuringFallbackOperation ? QString::fromUtf8( proj_context_errno_string( projContext, projResult ) ) : QObject::tr(
"Fallback transform failed" );
1081 const QString msg = QObject::tr(
"%1 (%2 to %3) of%4%5Error: %6" )
1090 if ( msg != mLastError )
1092 QgsDebugError(
"Projection failed emitting invalid transform signal: " + msg );
1100#ifdef COORDINATE_TRANSFORM_VERBOSE
1101 QgsDebugMsgLevel( QStringLiteral(
"[[[[[[ Projected %1, %2 to %3, %4 ]]]]]]" )
1102 .arg( xorg, 0,
'g', 15 ).arg( yorg, 0,
'g', 15 )
1103 .arg( *x, 0,
'g', 15 ).arg( *y, 0,
'g', 15 ), 2 );
1114 return !d->mIsValid || d->mShortCircuit;
1119 return d->mIsValid && d->mHasVerticalComponent;
1124 return d->mProjCoordinateOperation;
1129 ProjData projData = d->threadLocalProjData();
1136 d->mProjCoordinateOperation = operation;
1137 d->mShouldReverseCoordinateOperation =
false;
1143 d->mAllowFallbackTransforms = allowed;
1148 return d->mAllowFallbackTransforms;
1153 mBallparkTransformsAreAppropriate = appropriate;
1158 mDisableFallbackHandler = disabled;
1163 return mFallbackOperationOccurred;
1170 proj = QApplication::applicationDirPath()
1171 +
"/share/proj/" + QString( name );
1175 return proj.toUtf8();
1183 const QString sourceKey = src.
authid().isEmpty() ?
1185 const QString destKey = dest.
authid().isEmpty() ?
1188 if ( sourceKey.isEmpty() || destKey.isEmpty() )
1192 if ( sDisableCache )
1195 const QList< QgsCoordinateTransform > values = sTransforms.values( qMakePair( sourceKey, destKey ) );
1196 for (
auto valIt = values.constBegin(); valIt != values.constEnd(); ++valIt )
1198 if ( ( *valIt ).coordinateOperation() == coordinateOperationProj
1199 && ( *valIt ).allowFallbackTransforms() == allowFallback
1207 const bool hasContext = mHasContext;
1214 mHasContext = hasContext;
1223void QgsCoordinateTransform::addToCache()
1225 if ( !d->mSourceCRS.isValid() || !d->mDestCRS.isValid() )
1228 const QString sourceKey = d->mSourceCRS.authid().isEmpty() ?
1230 const QString destKey = d->mDestCRS.authid().isEmpty() ?
1233 if ( sourceKey.isEmpty() || destKey.isEmpty() )
1237 if ( sDisableCache )
1240 sTransforms.insert( qMakePair( sourceKey, destKey ), *
this );
1246 return d->mSourceDatumTransform;
1254 d->mSourceDatumTransform = dt;
1261 return d->mDestinationDatumTransform;
1269 d->mDestinationDatumTransform = dt;
1276 if ( sDisableCache )
1281 sDisableCache =
true;
1284 sTransforms.clear();
1287void QgsCoordinateTransform::removeFromCacheObjectsBelongingToCurrentThread(
void *pj_context )
1293 if ( sDisableCache )
1298 if ( sDisableCache )
1301 for (
auto it = sTransforms.begin(); it != sTransforms.end(); )
1303 auto &v = it.value();
1304 if ( v.d->removeObjectsBelongingToCurrentThread( pj_context ) )
1305 it = sTransforms.erase( it );
1315 const double distSourceUnits = std::sqrt( source1.
sqrDist( source2 ) );
1318 const double distDestUnits = std::sqrt( dest1.
sqrDist( dest2 ) );
1319 return distDestUnits / distSourceUnits;
1324 QgsCoordinateTransformPrivate::setCustomMissingRequiredGridHandler( handler );
1329 QgsCoordinateTransformPrivate::setCustomMissingPreferredGridHandler( handler );
1334 QgsCoordinateTransformPrivate::setCustomCoordinateOperationCreationErrorHandler( handler );
1339 QgsCoordinateTransformPrivate::setCustomMissingGridUsedByContextHandler( handler );
1344 sFallbackOperationOccurredHandler = handler;
1349 QgsCoordinateTransformPrivate::setDynamicCrsToDynamicCrsWarningHandler( handler );
@ Geocentric
Geocentric CRS.
QFlags< CoordinateTransformationFlag > CoordinateTransformationFlags
Coordinate transformation flags.
@ Preferred
Preferred format, matching the most recent WKT ISO standard. Currently an alias to WKT2_2019,...
@ BallparkTransformsAreAppropriate
Indicates that approximate "ballpark" results are appropriate for this coordinate transform....
@ IgnoreImpossibleTransformations
Indicates that impossible transformations (such as those which attempt to transform between two diffe...
TransformDirection
Indicates the direction (forward or inverse) of a transform.
@ Forward
Forward transform (from source to destination)
@ Reverse
Reverse/inverse transform (from destination to source)
Represents a coordinate reference system (CRS).
bool isValid() const
Returns whether this CRS is correctly initialized and usable.
QString toWkt(Qgis::CrsWktVariant variant=Qgis::CrsWktVariant::Wkt1Gdal, bool multiline=false, int indentationWidth=4) const
Returns a WKT representation of this CRS.
QString celestialBodyName() const
Attempts to retrieve the name of the celestial body associated with the CRS (e.g.
double coordinateEpoch() const
Returns the coordinate epoch, as a decimal year.
Contains information about the context in which a coordinate transform is executed.
Custom exception class for Coordinate Reference System related exceptions.
static void logMessage(const QString &message, const QString &tag=QString(), Qgis::MessageLevel level=Qgis::MessageLevel::Warning, bool notifyUser=true, const char *file=__builtin_FILE(), const char *function=__builtin_FUNCTION(), int line=__builtin_LINE())
Adds a message to the log instance (and creates it if necessary).
double sqrDist(double x, double y) const
Returns the squared distance between this point a specified x, y coordinate.
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...
static bool hasVerticalAxis(const PJ *crs)
Returns true if a PROJ crs has a vertical axis.
std::unique_ptr< PJ, ProjPJDeleter > proj_pj_unique_ptr
Scoped Proj PJ object.
Encapsulates a QGIS project, including sets of map layers and their styles, layouts,...
QgsCoordinateTransformContext transformContext
A convenience class that simplifies locking and unlocking QReadWriteLocks.
A rectangle specified with double values.
Q_INVOKABLE QString toString(int precision=16) const
Returns a string representation of form xmin,ymin : xmax,ymax Coordinates will be truncated to the sp...
void setXMinimum(double x)
Set the minimum x value.
void setXMaximum(double x)
Set the maximum x value.
void combineExtentWith(const QgsRectangle &rect)
Expands the rectangle so that it covers both the original rectangle and the given rectangle.
void setNull()
Mark a rectangle as being null (holding no spatial information).
Scoped object for temporary suppression of PROJ logging output.
A 3D vector (similar to QVector3D) with the difference that it uses double precision instead of singl...
double y() const
Returns Y coordinate.
double z() const
Returns Z coordinate.
double x() const
Returns X coordinate.
#define Q_NOWARN_DEPRECATED_POP
#define Q_NOWARN_DEPRECATED_PUSH
bool qgsNanCompatibleEquals(double a, double b)
Compare two doubles, treating nan values as equal.
#define QgsDebugMsgLevel(str, level)
#define QgsDebugError(str)
const QgsCoordinateReferenceSystem & crs