QGIS API Documentation 4.3.0-Master (0ff9723465c)
Loading...
Searching...
No Matches
qgscoordinatetransform.cpp
Go to the documentation of this file.
1/***************************************************************************
2 QgsCoordinateTransform.cpp - Coordinate Transforms
3 -------------------
4 begin : Dec 2004
5 copyright : (C) 2004 Tim Sutton
6 email : tim at linfiniti.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 ***************************************************************************/
18
19#include "qgis.h"
20#include "qgsapplication.h"
21#include "qgsbox3d.h"
23#include "qgsexception.h"
24#include "qgslogger.h"
25#include "qgsmessagelog.h"
26#include "qgspointxy.h"
27#include "qgsproject.h"
28#include "qgsreadwritelocker.h"
29#include "qgsrectangle.h"
30#include "qgsvector3d.h"
31
32#include <QString>
33
34using namespace Qt::StringLiterals;
35
36//qt includes
37#include <QDomNode>
38#include <QDomElement>
39#include <QApplication>
40#include <QPolygonF>
41#include <QStringList>
42#include <QVector>
43
44#include <proj.h>
45#include "qgsprojutils.h"
46
47#include <sqlite3.h>
48#include <qlogging.h>
49#include <vector>
50#include <algorithm>
51
52// if defined shows all information about transform to stdout
53// #define COORDINATE_TRANSFORM_VERBOSE
54
55QReadWriteLock QgsCoordinateTransform::sCacheLock;
56QMultiHash< QPair< QString, QString >, QgsCoordinateTransform > QgsCoordinateTransform::sTransforms; //same auth_id pairs might have different datum transformations
57bool QgsCoordinateTransform::sDisableCache = false;
58
59std::function< void( const QgsCoordinateReferenceSystem &sourceCrs, const QgsCoordinateReferenceSystem &destinationCrs, const QString &desiredOperation )>
60 QgsCoordinateTransform::sFallbackOperationOccurredHandler = nullptr;
61
63{
64 d = new QgsCoordinateTransformPrivate();
65}
66
69)
70{
71 mContext = context;
72 d = new QgsCoordinateTransformPrivate( source, destination, mContext );
73
75 mIgnoreImpossible = true;
76
77#ifdef QGISDEBUG
78 mHasContext = true;
79#endif
80
81 if ( mIgnoreImpossible && !isTransformationPossible( source, destination ) )
82 {
83 d->invalidate();
84 return;
85 }
86
87 if ( !d->checkValidity() )
88 return;
89
91 if ( !setFromCache( d->mSourceCRS, d->mDestCRS, d->mProjCoordinateOperation, d->mAllowFallbackTransforms ) )
92 {
93 d->initialize();
94 addToCache();
95 }
97
99 mBallparkTransformsAreAppropriate = true;
100}
101
104)
105{
106 mContext = project ? project->transformContext() : QgsCoordinateTransformContext();
107 d = new QgsCoordinateTransformPrivate( source, destination, mContext );
108#ifdef QGISDEBUG
109 if ( project )
110 mHasContext = true;
111#endif
112
114 mIgnoreImpossible = true;
115
116 if ( mIgnoreImpossible && !isTransformationPossible( source, destination ) )
117 {
118 d->invalidate();
119 return;
120 }
121
122 if ( !d->checkValidity() )
123 return;
124
126 if ( !setFromCache( d->mSourceCRS, d->mDestCRS, d->mProjCoordinateOperation, d->mAllowFallbackTransforms ) )
127 {
128 d->initialize();
129 addToCache();
130 }
132
134 mBallparkTransformsAreAppropriate = true;
135}
136
137QgsCoordinateTransform::QgsCoordinateTransform( const QgsCoordinateReferenceSystem &source, const QgsCoordinateReferenceSystem &destination, int sourceDatumTransform, int destinationDatumTransform )
138{
139 d = new QgsCoordinateTransformPrivate( source, destination, sourceDatumTransform, destinationDatumTransform );
140#ifdef QGISDEBUG
141 mHasContext = true; // not strictly true, but we don't need to worry if datums have been explicitly set
142#endif
143
144 if ( !d->checkValidity() )
145 return;
146
148 if ( !setFromCache( d->mSourceCRS, d->mDestCRS, d->mProjCoordinateOperation, d->mAllowFallbackTransforms ) )
149 {
150 d->initialize();
151 addToCache();
152 }
154}
155
157 : mContext( o.mContext )
158#ifdef QGISDEBUG
159 , mHasContext( o.mHasContext )
160#endif
161 , mLastError()
162 // none of these should be copied -- they must be set manually for every object instead, or
163 // we risk contaminating the cache and copies retrieved from cache with settings which should NOT
164 // be applied to all transforms
165 , mIgnoreImpossible( false )
166 , mBallparkTransformsAreAppropriate( false )
167 , mDisableFallbackHandler( false )
168 , mFallbackOperationOccurred( false )
169{
170 d = o.d;
171}
172
174{
175 if ( &o == this )
176 return *this;
177
178 d = o.d;
179#ifdef QGISDEBUG
180 mHasContext = o.mHasContext;
181#endif
182 mContext = o.mContext;
183 mLastError = QString();
184 return *this;
185}
186
189
191{
192 return d->mSourceCRS == other.d->mSourceCRS
193 && d->mDestCRS == other.d->mDestCRS
194 && mBallparkTransformsAreAppropriate == other.mBallparkTransformsAreAppropriate
195 && d->mProjCoordinateOperation == other.d->mProjCoordinateOperation
197}
198
200{
201 return !( *this == other );
202}
203
205{
206 if ( !source.isValid() || !destination.isValid() )
207 return false;
208
209 if ( source.celestialBodyName() != destination.celestialBodyName() )
210 return false;
211
212 return true;
213}
214
216{
217 d.detach();
218 d->mSourceCRS = crs;
219
220 if ( mIgnoreImpossible && !isTransformationPossible( d->mSourceCRS, d->mDestCRS ) )
221 {
222 d->invalidate();
223 return;
224 }
225
226 if ( !d->checkValidity() )
227 return;
228
229 d->calculateTransforms( mContext );
231 if ( !setFromCache( d->mSourceCRS, d->mDestCRS, d->mProjCoordinateOperation, d->mAllowFallbackTransforms ) )
232 {
233 d->initialize();
234 addToCache();
235 }
237}
239{
240 d.detach();
241 d->mDestCRS = crs;
242
243 if ( mIgnoreImpossible && !isTransformationPossible( d->mSourceCRS, d->mDestCRS ) )
244 {
245 d->invalidate();
246 return;
247 }
248
249 if ( !d->checkValidity() )
250 return;
251
252 d->calculateTransforms( mContext );
254 if ( !setFromCache( d->mSourceCRS, d->mDestCRS, d->mProjCoordinateOperation, d->mAllowFallbackTransforms ) )
255 {
256 d->initialize();
257 addToCache();
258 }
260}
261
263{
264 d.detach();
265 mContext = context;
266#ifdef QGISDEBUG
267 mHasContext = true;
268#endif
269
270 if ( mIgnoreImpossible && !isTransformationPossible( d->mSourceCRS, d->mDestCRS ) )
271 {
272 d->invalidate();
273 return;
274 }
275
276 if ( !d->checkValidity() )
277 return;
278
279 d->calculateTransforms( mContext );
281 if ( !setFromCache( d->mSourceCRS, d->mDestCRS, d->mProjCoordinateOperation, d->mAllowFallbackTransforms ) )
282 {
283 d->initialize();
284 addToCache();
285 }
287}
288
293
295{
296 return d->mSourceCRS;
297}
298
303
305{
306 if ( !d->mIsValid || d->mShortCircuit )
307 return point;
308
309 // transform x
310 double x = point.x();
311 double y = point.y();
312 double z = 0.0;
313 try
314 {
315 transformCoords( 1, &x, &y, &z, direction );
316 }
317 catch ( const QgsCsException & )
318 {
319 // rethrow the exception
320 QgsDebugMsgLevel( u"rethrowing exception"_s, 2 );
321 throw;
322 }
323
324 return QgsPointXY( x, y );
325}
326
327
328QgsPointXY QgsCoordinateTransform::transform( const double theX, const double theY = 0.0, Qgis::TransformDirection direction ) const
329{
330 try
331 {
332 return transform( QgsPointXY( theX, theY ), direction );
333 }
334 catch ( const QgsCsException & )
335 {
336 // rethrow the exception
337 QgsDebugMsgLevel( u"rethrowing exception"_s, 2 );
338 throw;
339 }
340}
341
343{
344 if ( !d->mIsValid || d->mShortCircuit )
345 return rect;
346 // transform x
347 double x1 = rect.xMinimum();
348 double y1 = rect.yMinimum();
349 double x2 = rect.xMaximum();
350 double y2 = rect.yMaximum();
351
352 // Number of points to reproject------+
353 // |
354 // V
355 try
356 {
357 double z = 0.0;
358 transformCoords( 1, &x1, &y1, &z, direction );
359 transformCoords( 1, &x2, &y2, &z, direction );
360 }
361 catch ( const QgsCsException & )
362 {
363 // rethrow the exception
364 QgsDebugMsgLevel( u"rethrowing exception"_s, 2 );
365 throw;
366 }
367
368#ifdef COORDINATE_TRANSFORM_VERBOSE
369 QgsDebugMsgLevel( u"Rect projection..."_s, 2 );
370 QgsDebugMsgLevel( u"Xmin : %1 --> %2"_s.arg( rect.xMinimum() ).arg( x1 ), 2 );
371 QgsDebugMsgLevel( u"Ymin : %1 --> %2"_s.arg( rect.yMinimum() ).arg( y1 ), 2 );
372 QgsDebugMsgLevel( u"Xmax : %1 --> %2"_s.arg( rect.xMaximum() ).arg( x2 ), 2 );
373 QgsDebugMsgLevel( u"Ymax : %1 --> %2"_s.arg( rect.yMaximum() ).arg( y2 ), 2 );
374#endif
375 return QgsRectangle( x1, y1, x2, y2 );
376}
377
379{
380 double x = point.x();
381 double y = point.y();
382 double z = point.z();
383 try
384 {
385 transformCoords( 1, &x, &y, &z, direction );
386 }
387 catch ( const QgsCsException & )
388 {
389 // rethrow the exception
390 QgsDebugMsgLevel( u"rethrowing exception"_s, 2 );
391 throw;
392 }
393 return QgsVector3D( x, y, z );
394}
395
396void QgsCoordinateTransform::transformInPlace( double &x, double &y, double &z, Qgis::TransformDirection direction ) const
397{
398 if ( !d->mIsValid || d->mShortCircuit )
399 return;
400#ifdef QGISDEBUG
401// QgsDebugMsgLevel(QString("Using transform in place %1 %2").arg(__FILE__).arg(__LINE__), 2);
402#endif
403 // transform x
404 try
405 {
406 transformCoords( 1, &x, &y, &z, direction );
407 }
408 catch ( const QgsCsException & )
409 {
410 // rethrow the exception
411 QgsDebugMsgLevel( u"rethrowing exception"_s, 2 );
412 throw;
413 }
414}
415
416void QgsCoordinateTransform::transformInPlace( float &x, float &y, double &z, Qgis::TransformDirection direction ) const
417{
418 double xd = static_cast< double >( x ), yd = static_cast< double >( y );
419 transformInPlace( xd, yd, z, direction );
420 x = xd;
421 y = yd;
422}
423
424void QgsCoordinateTransform::transformInPlace( float &x, float &y, float &z, Qgis::TransformDirection direction ) const
425{
426 if ( !d->mIsValid || d->mShortCircuit )
427 return;
428#ifdef QGISDEBUG
429 // QgsDebugMsgLevel(QString("Using transform in place %1 %2").arg(__FILE__).arg(__LINE__), 2);
430#endif
431 // transform x
432 try
433 {
434 double xd = x;
435 double yd = y;
436 double zd = z;
437 transformCoords( 1, &xd, &yd, &zd, direction );
438 x = xd;
439 y = yd;
440 z = zd;
441 }
442 catch ( QgsCsException & )
443 {
444 // rethrow the exception
445 QgsDebugMsgLevel( u"rethrowing exception"_s, 2 );
446 throw;
447 }
448}
449
451{
452 if ( !d->mIsValid || d->mShortCircuit )
453 {
454 return;
455 }
456
457 //create x, y arrays
458 const int nVertices = poly.size();
459
460 QVector<double> x( nVertices );
461 QVector<double> y( nVertices );
462 QVector<double> z( nVertices );
463 double *destX = x.data();
464 double *destY = y.data();
465 double *destZ = z.data();
466
467 const QPointF *polyData = poly.constData();
468 for ( int i = 0; i < nVertices; ++i )
469 {
470 *destX++ = polyData->x();
471 *destY++ = polyData->y();
472 *destZ++ = 0;
473 polyData++;
474 }
475
476 QString err;
477 try
478 {
479 transformCoords( nVertices, x.data(), y.data(), z.data(), direction );
480 }
481 catch ( const QgsCsException &e )
482 {
483 // record the exception, but don't rethrow it until we've recorded the coordinates we *could* transform
484 err = e.what();
485 }
486
487 QPointF *destPoint = poly.data();
488 const double *srcX = x.constData();
489 const double *srcY = y.constData();
490 for ( int i = 0; i < nVertices; ++i )
491 {
492 destPoint->rx() = *srcX++;
493 destPoint->ry() = *srcY++;
494 destPoint++;
495 }
496
497 // rethrow the exception
498 if ( !err.isEmpty() )
499 throw QgsCsException( err );
500}
501
502void QgsCoordinateTransform::transformInPlace( QVector<double> &x, QVector<double> &y, QVector<double> &z, Qgis::TransformDirection direction ) const
503{
504 if ( !d->mIsValid || d->mShortCircuit )
505 return;
506
507 Q_ASSERT( x.size() == y.size() );
508
509 // Apparently, if one has a std::vector, it is valid to use the
510 // address of the first element in the vector as a pointer to an
511 // array of the vectors data, and hence easily interface with code
512 // that wants C-style arrays.
513
514 try
515 {
516 transformCoords( x.size(), &x[0], &y[0], &z[0], direction );
517 }
518 catch ( const QgsCsException & )
519 {
520 // rethrow the exception
521 QgsDebugMsgLevel( u"rethrowing exception"_s, 2 );
522 throw;
523 }
524}
525
526
527void QgsCoordinateTransform::transformInPlace( QVector<float> &x, QVector<float> &y, QVector<float> &z, Qgis::TransformDirection direction ) const
528{
529 if ( !d->mIsValid || d->mShortCircuit )
530 return;
531
532 Q_ASSERT( x.size() == y.size() );
533
534 // Apparently, if one has a std::vector, it is valid to use the
535 // address of the first element in the vector as a pointer to an
536 // array of the vectors data, and hence easily interface with code
537 // that wants C-style arrays.
538
539 try
540 {
541 //copy everything to double vectors since proj needs double
542 const int vectorSize = x.size();
543 QVector<double> xd( x.size() );
544 QVector<double> yd( y.size() );
545 QVector<double> zd( z.size() );
546
547 double *destX = xd.data();
548 double *destY = yd.data();
549 double *destZ = zd.data();
550
551 const float *srcX = x.constData();
552 const float *srcY = y.constData();
553 const float *srcZ = z.constData();
554
555 for ( int i = 0; i < vectorSize; ++i )
556 {
557 *destX++ = static_cast< double >( *srcX++ );
558 *destY++ = static_cast< double >( *srcY++ );
559 *destZ++ = static_cast< double >( *srcZ++ );
560 }
561
562 transformCoords( x.size(), &xd[0], &yd[0], &zd[0], direction );
563
564 //copy back
565 float *destFX = x.data();
566 float *destFY = y.data();
567 float *destFZ = z.data();
568 const double *srcXD = xd.constData();
569 const double *srcYD = yd.constData();
570 const double *srcZD = zd.constData();
571 for ( int i = 0; i < vectorSize; ++i )
572 {
573 *destFX++ = static_cast< float >( *srcXD++ );
574 *destFY++ = static_cast< float >( *srcYD++ );
575 *destFZ++ = static_cast< float >( *srcZD++ );
576 }
577 }
578 catch ( QgsCsException & )
579 {
580 // rethrow the exception
581 QgsDebugMsgLevel( u"rethrowing exception"_s, 2 );
582 throw;
583 }
584}
585
586QgsRectangle QgsCoordinateTransform::transformBoundingBox( const QgsRectangle &rect, Qgis::TransformDirection direction, const bool handle180Crossover ) const
587{
588 // Calculate the bounding box of a QgsRectangle in the source CRS
589 // when projected to the destination CRS (or the inverse).
590 // This is done by looking at a number of points spread evenly
591 // across the rectangle
592
593 if ( !d->mIsValid || d->mShortCircuit )
594 return rect;
595
596 if ( rect.isEmpty() )
597 {
598 const QgsPointXY p = transform( rect.xMinimum(), rect.yMinimum(), direction );
599 return QgsRectangle( p, p );
600 }
601
602#ifdef QGISDEBUG
603 if ( !mHasContext )
604 {
605 QgsDebugMsgLevel( u"No QgsCoordinateTransformContext context set for transform"_s, 4 );
606 }
607#endif
608
609 // we can't calculate if transform involves a geocentric CRS. This is silly anyway,
610 // as transformation of a 2d bounding box makes no sense when a geocentric CRS is involved!
611 if ( d->mSourceCRS.type() == Qgis::CrsType::Geocentric )
612 {
613 throw QgsCsException( QObject::tr( "Could not transform bounding box for geocentric CRS %1" ).arg( d->mSourceCRS.authid() ) );
614 }
615 if ( d->mDestCRS.type() == Qgis::CrsType::Geocentric )
616 {
617 throw QgsCsException( QObject::tr( "Could not transform bounding box for geocentric CRS %1" ).arg( d->mDestCRS.authid() ) );
618 }
619
620 const double xMin = rect.xMinimum();
621 const double xMax = rect.xMaximum();
622 double yMin = rect.yMinimum();
623 double yMax = rect.yMaximum();
624 if ( d->mGeographicToWebMercator && ( direction == Qgis::TransformDirection::Forward ) )
625 {
626 // Latitudes close to 90 degree project to infinite northing in theory.
627 // We limit to 90 - 1e-1 which reproject to northing of ~ 44e6 m (about twice
628 // the maximum easting of ~20e6 m).
629 // For reference, GoogleMercator tiles are limited to a northing ~85 deg / ~20e6 m
630 // so limiting to 90 - 1e-1 is reasonable.
631 constexpr double EPS = 1e-1;
632 if ( yMin < -90 + EPS )
633 {
634 if ( yMax < -90 + EPS )
635 throw QgsCsException( QObject::tr( "Could not transform bounding box to target CRS" ) );
636 yMin = -90 + EPS;
637 }
638 if ( yMax > 90 - EPS )
639 {
640 if ( yMin > 90 - EPS )
641 throw QgsCsException( QObject::tr( "Could not transform bounding box to target CRS" ) );
642 yMax = 90 - EPS;
643 }
644 }
645
646 QgsScopedProjSilentLogger errorLogger;
647
648 QgsDebugMsgLevel( u"Entering transformBoundingBox..."_s, 4 );
649
650#if PROJ_VERSION_MAJOR < 9 || ( PROJ_VERSION_MAJOR == 9 && PROJ_VERSION_MINOR < 7 )
651 const auto legacyImplementation = [this, &rect, xMin, yMin, yMax, direction, handle180Crossover]() {
652 // this logic is buggy! See https://github.com/qgis/QGIS/issues/59821
653
654 // 64 points (<=2.12) is not enough, see #13665, for EPSG:4326 -> EPSG:3574 (say that it is a hard one),
655 // are decent result from about 500 points and more. This method is called quite often, but
656 // even with 1000 points it takes < 1ms.
657 // TODO: how to effectively and precisely reproject bounding box?
658 const int nPoints = 1000;
659 const double dst = std::sqrt( ( rect.width() * ( yMax - yMin ) ) / std::pow( std::sqrt( static_cast< double >( nPoints ) ) - 1, 2.0 ) );
660 const int nXPoints = static_cast<int>( std::clamp( std::ceil( rect.width() / dst ) + 1, 3.0, 1000.0 ) );
661 const int nYPoints = static_cast<int>( std::clamp( std::ceil( ( yMax - yMin ) / dst ) + 1, 3.0, 1000.0 ) );
662
663 QgsRectangle bb_rect;
664 bb_rect.setNull();
665
666 std::vector<double> x( nXPoints * static_cast< std::size_t >( nYPoints ) );
667 std::vector<double> y( nXPoints * static_cast< std::size_t >( nYPoints ) );
668 std::vector<double> z( nXPoints * static_cast< std::size_t >( nYPoints ) );
669
670 // Populate the vectors
671
672 const double dx = rect.width() / static_cast< double >( nXPoints - 1 );
673 const double dy = ( yMax - yMin ) / static_cast< double >( nYPoints - 1 );
674
675 double pointY = yMin;
676
677 for ( int i = 0; i < nYPoints; i++ )
678 {
679 // Start at right edge
680 double pointX = xMin;
681
682 for ( int j = 0; j < nXPoints; j++ )
683 {
684 x[( i * nXPoints ) + j] = pointX;
685 y[( i * nXPoints ) + j] = pointY;
686 // and the height...
687 z[( i * nXPoints ) + j] = 0.0;
688 // QgsDebugMsgLevel(QString("BBox coord: (%1, %2)").arg(x[(i*numP) + j]).arg(y[(i*numP) + j]), 2);
689 pointX += dx;
690 }
691 pointY += dy;
692 }
693
694 // Do transformation. Any exception generated must
695 // be handled in above layers.
696 try
697 {
698 transformCoords( nXPoints * nYPoints, x.data(), y.data(), z.data(), direction );
699 }
700 catch ( const QgsCsException & )
701 {
702 // rethrow the exception
703 QgsDebugMsgLevel( u"rethrowing exception"_s, 2 );
704 throw;
705 }
706
707 // check if result bbox is geographic and is crossing 180/-180 line: ie. min X is before the 180° and max X is after the -180°
708 bool doHandle180Crossover = false;
709 if ( nXPoints > 0 )
710 {
711 const double xMin = std::fmod( x[0], 180.0 );
712 const double xMax = std::fmod( x[nXPoints - 1], 180.0 );
713 if ( handle180Crossover
714 && ( ( direction == Qgis::TransformDirection::Forward && d->mDestCRS.isGeographic() ) || ( direction == Qgis::TransformDirection::Reverse && d->mSourceCRS.isGeographic() ) )
715 && xMin > 0.0
716 && xMin <= 180.0
717 && xMax < 0.0
718 && xMax >= -180.0 )
719 {
720 doHandle180Crossover = true;
721 }
722 }
723
724 // Calculate the bounding box and use that for the extent
725 for ( int i = 0; i < nXPoints * nYPoints; i++ )
726 {
727 if ( !std::isfinite( x[i] ) || !std::isfinite( y[i] ) )
728 {
729 continue;
730 }
731
732 if ( doHandle180Crossover )
733 {
734 //if crossing the date line, temporarily add 360 degrees to -ve longitudes
735 bb_rect.combineExtentWith( x[i] >= 0.0 ? x[i] : x[i] + 360.0, y[i] );
736 }
737 else
738 {
739 bb_rect.combineExtentWith( x[i], y[i] );
740 }
741 }
742
743 if ( bb_rect.isNull() )
744 {
745 // something bad happened when reprojecting the filter rect... no finite points were left!
746 throw QgsCsException( QObject::tr( "Could not transform bounding box to target CRS" ) );
747 }
748
749 if ( doHandle180Crossover )
750 {
751 //subtract temporary addition of 360 degrees from longitudes
752 if ( bb_rect.xMinimum() > 180.0 )
753 bb_rect.setXMinimum( bb_rect.xMinimum() - 360.0 );
754 if ( bb_rect.xMaximum() > 180.0 )
755 bb_rect.setXMaximum( bb_rect.xMaximum() - 360.0 );
756 }
757
758 QgsDebugMsgLevel( "Projected extent: " + bb_rect.toString(), 4 );
759
760 if ( bb_rect.isEmpty() )
761 {
762 QgsDebugMsgLevel( "Original extent: " + rect.toString(), 4 );
763 }
764
765 return bb_rect;
766 };
767#endif
768
769 // delegate logic to proj if version >= 8.2 available
770#if PROJ_VERSION_MAJOR > 8 || ( PROJ_VERSION_MAJOR == 8 && PROJ_VERSION_MINOR >= 2 )
771
772#if PROJ_VERSION_MAJOR < 9 || ( PROJ_VERSION_MAJOR == 9 && PROJ_VERSION_MINOR < 7 )
773 if ( !( ( direction == Qgis::TransformDirection::Forward && d->mDestCRS.isGeographic() ) || ( direction == Qgis::TransformDirection::Reverse && d->mSourceCRS.isGeographic() ) ) )
774 {
775 // PROJ < 9.7 has for example issues from world coverage in EPSG:4326
776 // to Spilhaus. But do not use the legacy implementation when going through
777 // geographic as the proj_trans_bounds() heuristics to detect anti-meridian
778 // crossing is better
779 return legacyImplementation();
780 }
781#endif
782
783 ProjData projData = d->threadLocalProjData();
784 PJ_CONTEXT *projContext = QgsProjContext::get();
785
786#if PROJ_VERSION_MAJOR < 9 || ( PROJ_VERSION_MAJOR == 9 && PROJ_VERSION_MINOR < 6 )
787 // if source or destination crs include vertical components, we need to demote them to
788 // 2d crs first, otherwise proj_trans_bounds fails on proj < 9.6 (see https://github.com/OSGeo/PROJ/pull/4333)
789
790 QgsProjUtils::proj_pj_unique_ptr srcCrs( proj_get_source_crs( projContext, projData ) );
791 QgsProjUtils::proj_pj_unique_ptr destCrs( proj_get_target_crs( projContext, projData ) );
792
793 QgsProjUtils::proj_pj_unique_ptr srcCrsHorizontal;
794 QgsProjUtils::proj_pj_unique_ptr destCrsHorizontal;
796 if ( QgsProjUtils::hasVerticalAxis( srcCrs.get() ) || QgsProjUtils::hasVerticalAxis( destCrs.get() ) )
797 {
798 srcCrsHorizontal = QgsProjUtils::crsToHorizontalCrs( srcCrs.get() );
799 destCrsHorizontal = QgsProjUtils::crsToHorizontalCrs( destCrs.get() );
800 transform2D.reset( proj_create_crs_to_crs_from_pj( projContext, srcCrsHorizontal.get(), destCrsHorizontal.get(), nullptr, nullptr ) );
801 if ( !transform2D )
802 {
803 const QString err = u"proj_create_crs_to_crs_from_pj(horizontalCrs(%1), horizontalCrs(%2)) failed"_s.arg( d->mSourceCRS.authid(), d->mSourceCRS.authid() );
804 throw QgsCsException( QObject::tr( "Could not transform bounding box to target CRS: %1" ).arg( err ) );
805 }
806 transform2D.reset( proj_normalize_for_visualization( projContext, transform2D.get() ) );
807 if ( !transform2D )
808 {
809 const QString err = u"Cannot normalize transform between horizontalCrs(%1) and horizontalCrs(%2)"_s.arg( d->mSourceCRS.authid(), d->mDestCRS.authid() );
810 throw QgsCsException( QObject::tr( "Could not transform bounding box to target CRS: %1" ).arg( err ) );
811 }
812 projData = transform2D.get();
813 }
814#endif
815
816 double transXMin = 0;
817 double transYMin = 0;
818 double transXMax = 0;
819 double transYMax = 0;
820
821 proj_errno_reset( projData );
822 // proj documentation recommends 21 points for densification, bump to
823 // 30 for higher accuracy in some situations
824 constexpr int DENSIFY_POINTS = 30;
825 int projResult = proj_trans_bounds(
826 projContext,
827 projData,
828 ( direction == Qgis::TransformDirection::Forward && !d->mIsReversed ) || ( direction == Qgis::TransformDirection::Reverse && d->mIsReversed ) ? PJ_FWD : PJ_INV,
829 xMin,
830 yMin,
831 xMax,
832 yMax,
833 &transXMin,
834 &transYMin,
835 &transXMax,
836 &transYMax,
837 DENSIFY_POINTS
838 );
839
840 if (
841 ( projResult != 1 || !std::isfinite( transXMin ) || !std::isfinite( transXMax ) || !std::isfinite( transYMin ) || !std::isfinite( transYMax ) )
842 && ( d->mAvailableOpCount > 1 || d->mAvailableOpCount == -1 ) // only use fallbacks if more than one operation is possible -- otherwise we've already tried it and it failed
843 )
844 {
845 // fail #1 -- try with getting proj to auto-pick an appropriate coordinate operation for the points
846 if ( PJ *transform = d->threadLocalFallbackProjData() )
847 {
848 projResult = proj_trans_bounds(
849 projContext,
850 transform,
851 ( direction == Qgis::TransformDirection::Forward && !d->mIsReversed ) || ( direction == Qgis::TransformDirection::Reverse && d->mIsReversed ) ? PJ_FWD : PJ_INV,
852 xMin,
853 yMin,
854 xMax,
855 yMax,
856 &transXMin,
857 &transYMin,
858 &transXMax,
859 &transYMax,
860 DENSIFY_POINTS
861 );
862 }
863 }
864
865 if ( projResult != 1 || !std::isfinite( transXMin ) || !std::isfinite( transXMax ) || !std::isfinite( transYMin ) || !std::isfinite( transYMax ) )
866 {
867#if PROJ_VERSION_MAJOR < 9 || ( PROJ_VERSION_MAJOR == 9 && PROJ_VERSION_MINOR < 7 )
868 // proj_trans_bounds() of PROJ < 9.7 may fail if the transform passed to it
869 // has been directly instantiated from a PROJ pipeline string.
870 // Cf https://github.com/OSGeo/PROJ/pull/4512
871 return legacyImplementation();
872#else
873 const QString projErr = QString::fromUtf8( proj_context_errno_string( projContext, proj_errno( projData ) ) );
874 const QString dir = ( direction == Qgis::TransformDirection::Forward ) ? QObject::tr( "Forward transform" ) : QObject::tr( "Inverse transform" );
875 const QString msg = QObject::tr( "%1 (%2 to %3) of bounding box failed: %4" )
876 .arg(
877 dir,
878 ( direction == Qgis::TransformDirection::Forward ) ? d->mSourceCRS.authid() : d->mDestCRS.authid(),
879 ( direction == Qgis::TransformDirection::Forward ) ? d->mDestCRS.authid() : d->mSourceCRS.authid(),
880 projErr
881 );
882 QgsDebugError( msg );
883
884 throw QgsCsException( msg );
885#endif
886 }
887
888 bool doHandle180Crossover = false;
889 // check if result bbox is geographic and is crossing 180/-180 line: ie. min X is before the 180° and max X is after the -180°
890 if ( handle180Crossover
891 && ( ( direction == Qgis::TransformDirection::Forward && d->mDestCRS.isGeographic() ) || ( direction == Qgis::TransformDirection::Reverse && d->mSourceCRS.isGeographic() ) )
892 && ( transXMax < transXMin ) )
893 {
894 //if crossing the date line, temporarily add 360 degrees to -ve longitudes
895 std::swap( transXMax, transXMin );
896 if ( transXMin < 0 )
897 transXMin += 360;
898 if ( transXMax < 0 )
899 transXMax += 360;
900 doHandle180Crossover = true;
901 }
902
903 QgsRectangle boundingBoxRect { transXMin, transYMin, transXMax, transYMax };
904 if ( boundingBoxRect.isNull() )
905 {
906 // something bad happened when reprojecting the filter rect... no finite points were left!
907 throw QgsCsException( QObject::tr( "Could not transform bounding box to target CRS" ) );
908 }
909
910 if ( doHandle180Crossover )
911 {
912 //subtract temporary addition of 360 degrees from longitudes
913 if ( boundingBoxRect.xMinimum() > 180.0 )
914 boundingBoxRect.setXMinimum( boundingBoxRect.xMinimum() - 360.0 );
915 if ( boundingBoxRect.xMaximum() > 180.0 )
916 boundingBoxRect.setXMaximum( boundingBoxRect.xMaximum() - 360.0 );
917 }
918
919 QgsDebugMsgLevel( "Projected extent: " + boundingBoxRect.toString(), 4 );
920
921 if ( boundingBoxRect.isEmpty() )
922 {
923 QgsDebugMsgLevel( "Original extent: " + rect.toString(), 4 );
924 }
925
926 return boundingBoxRect;
927#endif
928#if PROJ_VERSION_MAJOR < 9 || ( PROJ_VERSION_MAJOR == 9 && PROJ_VERSION_MINOR < 7 )
929 return legacyImplementation();
930#endif
931}
932
934{
935 if ( !d->mIsValid || d->mShortCircuit || box.isNull() )
936 return box;
937
938#ifdef QGISDEBUG
939 if ( !mHasContext )
940 {
941 QgsDebugMsgLevel( u"No QgsCoordinateTransformContext context set for transform"_s, 4 );
942 }
943#endif
944
945 const double zMin = std::isnan( box.zMinimum() ) ? 0.0 : box.zMinimum();
946 const double zMax = std::isnan( box.zMaximum() ) ? 0.0 : box.zMaximum();
947
948 QgsScopedProjSilentLogger errorLogger;
949
950#if PROJ_VERSION_MAJOR < 9 || ( PROJ_VERSION_MAJOR == 9 && PROJ_VERSION_MINOR < 6 )
951 // our implementation that should go away after PROJ version upgrade to at least 9.6
952 const auto legacy = [this, &box, zMin, zMax, direction]() {
953 constexpr int POINTS_PER_AXIS = 31;
954 const int nX = box.width() > 0 ? POINTS_PER_AXIS : 1;
955 const int nY = box.height() > 0 ? POINTS_PER_AXIS : 1;
956 const int nZ = zMax > zMin ? POINTS_PER_AXIS : 1;
957
958 std::vector< double > x;
959 std::vector< double > y;
960 std::vector< double > z;
961 const std::size_t pointCount = static_cast< std::size_t >( nX ) * static_cast< std::size_t >( nY ) * static_cast< std::size_t >( nZ );
962 x.reserve( pointCount );
963 y.reserve( pointCount );
964 z.reserve( pointCount );
965
966 for ( int i = 0; i < nX; ++i )
967 {
968 const double pointX = nX == 1 ? box.xMinimum() : box.xMinimum() + box.width() * i / ( nX - 1 );
969 for ( int j = 0; j < nY; ++j )
970 {
971 const double pointY = nY == 1 ? box.yMinimum() : box.yMinimum() + box.height() * j / ( nY - 1 );
972 for ( int k = 0; k < nZ; ++k )
973 {
974 const double pointZ = nZ == 1 ? zMin : zMin + ( zMax - zMin ) * k / ( nZ - 1 );
975 x.push_back( pointX );
976 y.push_back( pointY );
977 z.push_back( pointZ );
978 }
979 }
980 }
981
982 transformCoords( static_cast< int >( pointCount ), x.data(), y.data(), z.data(), direction );
983
984 QgsBox3D result;
985 result.setNull();
986 for ( std::size_t i = 0; i < pointCount; ++i )
987 {
988 if ( !std::isfinite( x[i] ) || !std::isfinite( y[i] ) || !std::isfinite( z[i] ) )
989 continue;
990
991 result.combineWith( x[i], y[i], z[i] );
992 }
993
994 if ( result.isNull() )
995 {
996 throw QgsCsException( QObject::tr( "Could not transform box to target CRS" ) );
997 }
998
999 QgsDebugMsgLevel( "Projected box: " + result.toString(), 4 );
1000
1001 return result;
1002 };
1003
1004 return legacy();
1005#else
1006
1007 ProjData projData = d->threadLocalProjData();
1008 PJ_CONTEXT *projContext = QgsProjContext::get();
1009
1010 double outXMin = 0;
1011 double outYMin = 0;
1012 double outZMin = 0;
1013 double outXMax = 0;
1014 double outYMax = 0;
1015 double outZMax = 0;
1016
1017 constexpr int DENSIFY_POINTS = 30;
1018
1019 proj_errno_reset( projData );
1020 int projResult = proj_trans_bounds_3D(
1021 projContext,
1022 projData,
1023 ( direction == Qgis::TransformDirection::Forward && !d->mIsReversed ) || ( direction == Qgis::TransformDirection::Reverse && d->mIsReversed ) ? PJ_FWD : PJ_INV,
1024 box.xMinimum(),
1025 box.yMinimum(),
1026 zMin,
1027 box.xMaximum(),
1028 box.yMaximum(),
1029 zMax,
1030 &outXMin,
1031 &outYMin,
1032 &outZMin,
1033 &outXMax,
1034 &outYMax,
1035 &outZMax,
1036 DENSIFY_POINTS
1037 );
1038
1039 if (
1040 ( projResult != 1 || !std::isfinite( outXMin ) || !std::isfinite( outXMax ) || !std::isfinite( outYMin ) || !std::isfinite( outYMax ) || !std::isfinite( outZMin ) || !std::isfinite( outZMax ) )
1041 && ( d->mAvailableOpCount > 1 || d->mAvailableOpCount == -1 ) // only use fallbacks if more than one operation is possible -- otherwise we've already tried it and it failed
1042 )
1043 {
1044 // fail #1 -- try with getting proj to auto-pick an appropriate coordinate operation for the points
1045 if ( PJ *transform = d->threadLocalFallbackProjData() )
1046 {
1047 projResult = proj_trans_bounds_3D(
1048 projContext,
1049 transform,
1050 ( direction == Qgis::TransformDirection::Forward && !d->mIsReversed ) || ( direction == Qgis::TransformDirection::Reverse && d->mIsReversed ) ? PJ_FWD : PJ_INV,
1051 box.xMinimum(),
1052 box.yMinimum(),
1053 zMin,
1054 box.xMaximum(),
1055 box.yMaximum(),
1056 zMax,
1057 &outXMin,
1058 &outYMin,
1059 &outZMin,
1060 &outXMax,
1061 &outYMax,
1062 &outZMax,
1063 DENSIFY_POINTS
1064 );
1065 }
1066 }
1067
1068 if ( projResult != 1 || !std::isfinite( outXMin ) || !std::isfinite( outXMax ) || !std::isfinite( outYMin ) || !std::isfinite( outYMax ) || !std::isfinite( outZMin ) || !std::isfinite( outZMax ) )
1069 {
1070 throw QgsCsException( QObject::tr( "Could not transform box to target CRS" ) );
1071 }
1072
1073 const QgsBox3D result( outXMin, outYMin, outZMin, outXMax, outYMax, outZMax );
1074 QgsDebugMsgLevel( "Projected box: " + result.toString(), 4 );
1075 return result;
1076#endif
1077}
1078
1079void QgsCoordinateTransform::transformCoords( int numPoints, double *x, double *y, double *z, Qgis::TransformDirection direction ) const
1080{
1081 if ( !d->mIsValid || d->mShortCircuit )
1082 return;
1083 // Refuse to transform the points if the srs's are invalid
1084 if ( !d->mSourceCRS.isValid() )
1085 {
1087 QObject::tr(
1088 "The source spatial reference system (CRS) is not valid. "
1089 "The coordinates can not be reprojected. The CRS is: %1"
1090 )
1091 .arg( d->mSourceCRS.toProj() ),
1092 QObject::tr( "CRS" )
1093 );
1094 return;
1095 }
1096 if ( !d->mDestCRS.isValid() )
1097 {
1099 QObject::tr(
1100 "The destination spatial reference system (CRS) is not valid. "
1101 "The coordinates can not be reprojected. The CRS is: %1"
1102 )
1103 .arg( d->mDestCRS.toProj() ),
1104 QObject::tr( "CRS" )
1105 );
1106 return;
1107 }
1108
1109 std::vector< int > zNanPositions;
1110 for ( int i = 0; i < numPoints; i++ )
1111 {
1112 if ( std::isnan( z[i] ) )
1113 {
1114 zNanPositions.push_back( i );
1115 z[i] = 0.0;
1116 }
1117 }
1118
1119 std::vector< double > xprev( numPoints );
1120 memcpy( xprev.data(), x, sizeof( double ) * numPoints );
1121 std::vector< double > yprev( numPoints );
1122 memcpy( yprev.data(), y, sizeof( double ) * numPoints );
1123 std::vector< double > zprev( numPoints );
1124 memcpy( zprev.data(), z, sizeof( double ) * numPoints );
1125
1126 const bool useTime = !std::isnan( d->mDefaultTime );
1127 std::vector< double > t( useTime ? numPoints : 0, d->mDefaultTime );
1128
1129#ifdef COORDINATE_TRANSFORM_VERBOSE
1130 double xorg = *x;
1131 double yorg = *y;
1132 QgsDebugMsgLevel( u"[[[[[[ Number of points to transform: %1 ]]]]]]"_s.arg( numPoints ), 2 );
1133#endif
1134
1135#ifdef QGISDEBUG
1136 if ( !mHasContext )
1137 {
1138 QgsDebugMsgLevel( u"No QgsCoordinateTransformContext context set for transform"_s, 4 );
1139 }
1140#endif
1141
1142 // use proj4 to do the transform
1143 ProjData projData = d->threadLocalProjData();
1144
1145 int projResult = 0;
1146
1147 proj_errno_reset( projData );
1148 proj_trans_generic(
1149 projData,
1150 ( direction == Qgis::TransformDirection::Forward && !d->mIsReversed ) || ( direction == Qgis::TransformDirection::Reverse && d->mIsReversed ) ? PJ_FWD : PJ_INV,
1151 x,
1152 sizeof( double ),
1153 numPoints,
1154 y,
1155 sizeof( double ),
1156 numPoints,
1157 z,
1158 sizeof( double ),
1159 numPoints,
1160 useTime ? t.data() : nullptr,
1161 sizeof( double ),
1162 useTime ? numPoints : 0
1163 );
1164 // Try to - approximately - emulate the behavior of pj_transform()...
1165 // In the case of a single point transform, and a transformation error occurs,
1166 // pj_transform() would return the errno. In cases of multiple point transform,
1167 // it would continue (for non-transient errors, that is pipeline definition
1168 // errors) and just set the resulting x,y to infinity. This is in fact a
1169 // bit more subtle than that, and I'm not completely sure the logic in
1170 // pj_transform() was really sane & fully bullet proof
1171 // So here just check proj_errno() for single point transform
1172 int actualRes = 0;
1173 if ( numPoints == 1 )
1174 {
1175 projResult = proj_errno( projData );
1176 actualRes = projResult;
1177 }
1178 else
1179 {
1180 actualRes = proj_errno( projData );
1181 }
1182 if ( actualRes == 0 )
1183 {
1184 // proj_errno is sometimes not an accurate method to test for transform failures - so we need to
1185 // manually scan for nan values
1186 if ( std::any_of( x, x + numPoints, []( double v ) { return std::isinf( v ); } )
1187 || std::any_of( y, y + numPoints, []( double v ) { return std::isinf( v ); } )
1188 || std::any_of( z, z + numPoints, []( double v ) { return std::isinf( v ); } ) )
1189 {
1190 actualRes = 1;
1191 }
1192 }
1193
1194 mFallbackOperationOccurred = false;
1195 bool errorOccurredDuringFallbackOperation = false;
1196 if ( actualRes != 0
1197 && ( d->mAvailableOpCount > 1 || d->mAvailableOpCount == -1 ) // only use fallbacks if more than one operation is possible -- otherwise we've already tried it and it failed
1198 && ( d->mAllowFallbackTransforms || mBallparkTransformsAreAppropriate ) )
1199 {
1200 // fail #1 -- try with getting proj to auto-pick an appropriate coordinate operation for the points
1201 if ( PJ *transform = d->threadLocalFallbackProjData() )
1202 {
1203 projResult = 0;
1204 proj_errno_reset( transform );
1205 memcpy( x, xprev.data(), sizeof( double ) * numPoints );
1206 memcpy( y, yprev.data(), sizeof( double ) * numPoints );
1207 memcpy( z, zprev.data(), sizeof( double ) * numPoints );
1208 proj_trans_generic(
1209 transform,
1210 direction == Qgis::TransformDirection::Forward ? PJ_FWD : PJ_INV,
1211 x,
1212 sizeof( double ),
1213 numPoints,
1214 y,
1215 sizeof( double ),
1216 numPoints,
1217 z,
1218 sizeof( double ),
1219 numPoints,
1220 useTime ? t.data() : nullptr,
1221 sizeof( double ),
1222 useTime ? numPoints : 0
1223 );
1224 // Try to - approximately - emulate the behavior of pj_transform()...
1225 // In the case of a single point transform, and a transformation error occurs,
1226 // pj_transform() would return the errno. In cases of multiple point transform,
1227 // it would continue (for non-transient errors, that is pipeline definition
1228 // errors) and just set the resulting x,y to infinity. This is in fact a
1229 // bit more subtle than that, and I'm not completely sure the logic in
1230 // pj_transform() was really sane & fully bullet proof
1231 // So here just check proj_errno() for single point transform
1232 if ( numPoints == 1 )
1233 {
1234 projResult = proj_errno( transform );
1235 // hmm - something very odd here. We can't trust proj_errno( transform ), as that's giving us incorrect error numbers
1236 // (such as "failed to load datum shift file", which is definitely incorrect for a default proj created operation!)
1237 // so we resort to testing values ourselves...
1238 errorOccurredDuringFallbackOperation = std::isinf( x[0] ) || std::isinf( y[0] ) || std::isinf( z[0] );
1239 }
1240
1241 if ( !errorOccurredDuringFallbackOperation )
1242 {
1243 mFallbackOperationOccurred = true;
1244 }
1245
1246 if ( !mBallparkTransformsAreAppropriate && !mDisableFallbackHandler && sFallbackOperationOccurredHandler )
1247 {
1248 sFallbackOperationOccurredHandler( d->mSourceCRS, d->mDestCRS, d->mProjCoordinateOperation );
1249#if 0
1250 const QString warning = u"A fallback coordinate operation was used between %1 and %2"_s.arg( d->mSourceCRS.authid(),
1251 d->mDestCRS.authid() );
1252 qWarning( "%s", warning.toLatin1().constData() );
1253#endif
1254 }
1255 }
1256 }
1257
1258 for ( const int &pos : zNanPositions )
1259 {
1260 z[pos] = std::numeric_limits<double>::quiet_NaN();
1261 }
1262
1263 if ( projResult != 0 || errorOccurredDuringFallbackOperation )
1264 {
1265 //something bad happened....
1266 QString points;
1267
1268 const QChar delim = numPoints > 1 ? '\n' : ' ';
1269 for ( int i = 0; i < numPoints; ++i )
1270 {
1271 points += u"(%1, %2)"_s.arg( xprev[i], 0, 'f' ).arg( yprev[i], 0, 'f' ) + delim;
1272 }
1273
1274 const QString dir = ( direction == Qgis::TransformDirection::Forward ) ? QObject::tr( "Forward transform" ) : QObject::tr( "Inverse transform" );
1275
1276 PJ_CONTEXT *projContext = QgsProjContext::get();
1277 const QString projError = !errorOccurredDuringFallbackOperation ? QString::fromUtf8( proj_context_errno_string( projContext, projResult ) ) : QObject::tr( "Fallback transform failed" );
1278
1279 const QString msg = QObject::tr( "%1 (%2 to %3) of%4%5Error: %6" )
1280 .arg(
1281 dir,
1282 ( direction == Qgis::TransformDirection::Forward ) ? d->mSourceCRS.authid() : d->mDestCRS.authid(),
1283 ( direction == Qgis::TransformDirection::Forward ) ? d->mDestCRS.authid() : d->mSourceCRS.authid(),
1284 QString( delim ),
1285 points,
1286 projError
1287 );
1288
1289 // don't flood console with thousands of duplicate transform error messages
1290 if ( msg != mLastError )
1291 {
1292 QgsDebugError( "Projection failed emitting invalid transform signal: " + msg );
1293 mLastError = msg;
1294 }
1295 QgsDebugMsgLevel( u"rethrowing exception"_s, 2 );
1296
1297 throw QgsCsException( msg );
1298 }
1299
1300#ifdef COORDINATE_TRANSFORM_VERBOSE
1301 QgsDebugMsgLevel( u"[[[[[[ Projected %1, %2 to %3, %4 ]]]]]]"_s.arg( xorg, 0, 'g', 15 ).arg( yorg, 0, 'g', 15 ).arg( *x, 0, 'g', 15 ).arg( *y, 0, 'g', 15 ), 2 );
1302#endif
1303}
1304
1306{
1307 return d->mIsValid;
1308}
1309
1311{
1312 return !d->mIsValid || d->mShortCircuit;
1313}
1314
1316{
1317 return d->mIsValid && d->mHasVerticalComponent;
1318}
1319
1321{
1322 return d->mProjCoordinateOperation;
1323}
1324
1326{
1327 ProjData projData = d->threadLocalProjData();
1329}
1330
1331void QgsCoordinateTransform::setCoordinateOperation( const QString &operation ) const
1332{
1333 d.detach();
1334 d->mProjCoordinateOperation = operation;
1335 d->mShouldReverseCoordinateOperation = false;
1336}
1337
1339{
1340 d.detach();
1341 d->mAllowFallbackTransforms = allowed;
1342}
1343
1345{
1346 return d->mAllowFallbackTransforms;
1347}
1348
1350{
1351 mBallparkTransformsAreAppropriate = appropriate;
1352}
1353
1355{
1356 mDisableFallbackHandler = disabled;
1357}
1358
1360{
1361 return mFallbackOperationOccurred;
1362}
1363
1364const char *finder( const char *name )
1365{
1366 QString proj;
1367#ifdef Q_OS_WIN
1368 proj = QApplication::applicationDirPath() + "/share/proj/" + QString( name );
1369#else
1370 Q_UNUSED( name )
1371#endif
1372 return proj.toUtf8();
1373}
1374
1375bool QgsCoordinateTransform::setFromCache( const QgsCoordinateReferenceSystem &src, const QgsCoordinateReferenceSystem &dest, const QString &coordinateOperationProj, bool allowFallback )
1376{
1377 if ( !src.isValid() || !dest.isValid() )
1378 return false;
1379
1380 const QString sourceKey = src.authid().isEmpty() ? src.toWkt( Qgis::CrsWktVariant::Preferred ) : src.authid();
1381 const QString destKey = dest.authid().isEmpty() ? dest.toWkt( Qgis::CrsWktVariant::Preferred ) : dest.authid();
1382
1383 if ( sourceKey.isEmpty() || destKey.isEmpty() )
1384 return false;
1385
1386 QgsReadWriteLocker locker( sCacheLock, QgsReadWriteLocker::Read );
1387 if ( sDisableCache )
1388 return false;
1389
1390 const QList< QgsCoordinateTransform > values = sTransforms.values( qMakePair( sourceKey, destKey ) );
1391 for ( auto valIt = values.constBegin(); valIt != values.constEnd(); ++valIt )
1392 {
1393 if ( ( *valIt ).coordinateOperation() == coordinateOperationProj
1394 && ( *valIt ).allowFallbackTransforms() == allowFallback
1395 && qgsNanCompatibleEquals( src.coordinateEpoch(), ( *valIt ).sourceCrs().coordinateEpoch() )
1396 && qgsNanCompatibleEquals( dest.coordinateEpoch(), ( *valIt ).destinationCrs().coordinateEpoch() ) )
1397 {
1398 // need to save, and then restore the context... we don't want this to be cached or to use the values from the cache
1399 const QgsCoordinateTransformContext context = mContext;
1400#ifdef QGISDEBUG
1401 const bool hasContext = mHasContext;
1402#endif
1403 *this = *valIt;
1404 locker.unlock();
1405
1406 mContext = context;
1407#ifdef QGISDEBUG
1408 mHasContext = hasContext;
1409#endif
1410
1411 return true;
1412 }
1413 }
1414 return false;
1415}
1416
1417void QgsCoordinateTransform::addToCache()
1418{
1419 if ( !d->mSourceCRS.isValid() || !d->mDestCRS.isValid() )
1420 return;
1421
1422 const QString sourceKey = d->mSourceCRS.authid().isEmpty() ? d->mSourceCRS.toWkt( Qgis::CrsWktVariant::Preferred ) : d->mSourceCRS.authid();
1423 const QString destKey = d->mDestCRS.authid().isEmpty() ? d->mDestCRS.toWkt( Qgis::CrsWktVariant::Preferred ) : d->mDestCRS.authid();
1424
1425 if ( sourceKey.isEmpty() || destKey.isEmpty() )
1426 return;
1427
1428 const QgsReadWriteLocker locker( sCacheLock, QgsReadWriteLocker::Write );
1429 if ( sDisableCache )
1430 return;
1431
1432 sTransforms.insert( qMakePair( sourceKey, destKey ), *this );
1433}
1434
1436{
1438 return d->mSourceDatumTransform;
1440}
1441
1443{
1444 d.detach();
1446 d->mSourceDatumTransform = dt;
1448}
1449
1451{
1453 return d->mDestinationDatumTransform;
1455}
1456
1458{
1459 d.detach();
1461 d->mDestinationDatumTransform = dt;
1463}
1464
1466{
1467 const QgsReadWriteLocker locker( sCacheLock, QgsReadWriteLocker::Write );
1468 if ( sDisableCache )
1469 return;
1470
1471 if ( disableCache )
1472 {
1473 sDisableCache = true;
1474 }
1475
1476 sTransforms.clear();
1477}
1478
1479void QgsCoordinateTransform::removeFromCacheObjectsBelongingToCurrentThread( void *pj_context )
1480{
1481 // Not completely sure about object order destruction after main() has
1482 // exited. So it is safer to check sDisableCache before using sCacheLock
1483 // in case sCacheLock would have been destroyed before the current TLS
1484 // QgsProjContext object that has called us...
1485 if ( sDisableCache )
1486 return;
1487
1488 const QgsReadWriteLocker locker( sCacheLock, QgsReadWriteLocker::Write );
1489 // cppcheck-suppress identicalConditionAfterEarlyExit
1490 if ( sDisableCache )
1491 return;
1492
1493 for ( auto it = sTransforms.begin(); it != sTransforms.end(); )
1494 {
1495 auto &v = it.value();
1496 if ( v.d->removeObjectsBelongingToCurrentThread( pj_context ) )
1497 it = sTransforms.erase( it );
1498 else
1499 ++it;
1500 }
1501}
1502
1503double QgsCoordinateTransform::scaleFactor( const QgsRectangle &ReferenceExtent ) const
1504{
1505 const QgsPointXY source1( ReferenceExtent.xMinimum(), ReferenceExtent.yMinimum() );
1506 const QgsPointXY source2( ReferenceExtent.xMaximum(), ReferenceExtent.yMaximum() );
1507 const double distSourceUnits = std::sqrt( source1.sqrDist( source2 ) );
1508 const QgsPointXY dest1 = transform( source1 );
1509 const QgsPointXY dest2 = transform( source2 );
1510 const double distDestUnits = std::sqrt( dest1.sqrDist( dest2 ) );
1511 return distDestUnits / distSourceUnits;
1512}
1513
1515 const std::function<void( const QgsCoordinateReferenceSystem &, const QgsCoordinateReferenceSystem &, const QgsDatumTransform::GridDetails & )> &handler
1516)
1517{
1518 QgsCoordinateTransformPrivate::setCustomMissingRequiredGridHandler( handler );
1519}
1520
1523)
1524{
1525 QgsCoordinateTransformPrivate::setCustomMissingPreferredGridHandler( handler );
1526}
1527
1529{
1530 QgsCoordinateTransformPrivate::setCustomCoordinateOperationCreationErrorHandler( handler );
1531}
1532
1534 const std::function<void( const QgsCoordinateReferenceSystem &, const QgsCoordinateReferenceSystem &, const QgsDatumTransform::TransformDetails & )> &handler
1535)
1536{
1537 QgsCoordinateTransformPrivate::setCustomMissingGridUsedByContextHandler( handler );
1538}
1539
1540void QgsCoordinateTransform::setFallbackOperationOccurredHandler( const std::function<void( const QgsCoordinateReferenceSystem &, const QgsCoordinateReferenceSystem &, const QString & )> &handler )
1541{
1542 sFallbackOperationOccurredHandler = handler;
1543}
1544
1546{
1547 QgsCoordinateTransformPrivate::setDynamicCrsToDynamicCrsWarningHandler( handler );
1548}
@ Geocentric
Geocentric CRS.
Definition qgis.h:2496
QFlags< CoordinateTransformationFlag > CoordinateTransformationFlags
Coordinate transformation flags.
Definition qgis.h:2887
@ Preferred
Preferred format, matching the most recent WKT ISO standard. Currently an alias to WKT2_2019,...
Definition qgis.h:2611
@ BallparkTransformsAreAppropriate
Indicates that approximate "ballpark" results are appropriate for this coordinate transform....
Definition qgis.h:2875
@ IgnoreImpossibleTransformations
Indicates that impossible transformations (such as those which attempt to transform between two diffe...
Definition qgis.h:2877
TransformDirection
Indicates the direction (forward or inverse) of a transform.
Definition qgis.h:2862
@ Forward
Forward transform (from source to destination).
Definition qgis.h:2863
@ Reverse
Reverse/inverse transform (from destination to source).
Definition qgis.h:2864
A 3-dimensional box composed of x, y, z coordinates.
Definition qgsbox3d.h:45
double yMaximum() const
Returns the maximum y value.
Definition qgsbox3d.h:240
QString toString(int precision=16) const
Returns a string representation of form xmin,ymin,zmin : xmax,ymax,zmax Coordinates will be truncated...
Definition qgsbox3d.cpp:326
double xMinimum() const
Returns the minimum x value.
Definition qgsbox3d.h:205
double zMaximum() const
Returns the maximum z value.
Definition qgsbox3d.h:268
double xMaximum() const
Returns the maximum x value.
Definition qgsbox3d.h:212
void combineWith(const QgsBox3D &box)
Expands the bbox so that it covers both the original rectangle and the given rectangle.
Definition qgsbox3d.cpp:211
double width() const
Returns the width of the box.
Definition qgsbox3d.h:287
void setNull()
Mark a box as being null (holding no spatial information).
Definition qgsbox3d.cpp:108
double zMinimum() const
Returns the minimum z value.
Definition qgsbox3d.h:261
double yMinimum() const
Returns the minimum y value.
Definition qgsbox3d.h:233
double height() const
Returns the height of the box.
Definition qgsbox3d.h:294
bool isNull() const
Test if the box is null (holding no spatial information).
Definition qgsbox3d.cpp:311
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.
Handles coordinate transforms between two coordinate systems.
QgsCoordinateTransformContext context() const
Returns the context in which the coordinate transform will be calculated.
QgsCoordinateTransform()
Default constructor, creates an invalid QgsCoordinateTransform.
static bool isTransformationPossible(const QgsCoordinateReferenceSystem &source, const QgsCoordinateReferenceSystem &destination)
Returns true if it is theoretically possible to transform between source and destination CRSes.
QgsCoordinateReferenceSystem sourceCrs() const
Returns the source coordinate reference system, which the transform will transform coordinates from.
bool allowFallbackTransforms() const
Returns whether "ballpark" fallback transformations will be used in the case that the specified coord...
static void setCustomMissingRequiredGridHandler(const std::function< void(const QgsCoordinateReferenceSystem &sourceCrs, const QgsCoordinateReferenceSystem &destinationCrs, const QgsDatumTransform::GridDetails &grid)> &handler)
Sets a custom handler to use when a coordinate transform is created between sourceCrs and destination...
QgsDatumTransform::TransformDetails instantiatedCoordinateOperationDetails() const
Returns the transform details representing the coordinate operation which is being used to transform ...
Q_DECL_DEPRECATED void setDestinationDatumTransformId(int datumId)
Sets the datumId ID of the datum transform to use when projecting to the destination CRS.
void setContext(const QgsCoordinateTransformContext &context)
Sets the context in which the coordinate transform should be calculated.
QgsBox3D transformBox3D(const QgsBox3D &box, Qgis::TransformDirection direction=Qgis::TransformDirection::Forward) const
Transforms a 3D box from the source CRS to the destination CRS.
QString coordinateOperation() const
Returns a Proj string representing the coordinate operation which will be used to transform coordinat...
void setSourceCrs(const QgsCoordinateReferenceSystem &crs)
Sets the source coordinate reference system.
void setBallparkTransformsAreAppropriate(bool appropriate)
Sets whether approximate "ballpark" results are appropriate for this coordinate transform.
double scaleFactor(const QgsRectangle &referenceExtent) const
Computes an estimated conversion factor between source and destination units:
static void setCustomCoordinateOperationCreationErrorHandler(const std::function< void(const QgsCoordinateReferenceSystem &sourceCrs, const QgsCoordinateReferenceSystem &destinationCrs, const QString &error)> &handler)
Sets a custom handler to use when a coordinate transform was required between sourceCrs and destinati...
void transformCoords(int numPoint, double *x, double *y, double *z, Qgis::TransformDirection direction=Qgis::TransformDirection::Forward) const
Transform an array of coordinates to the destination CRS.
void setDestinationCrs(const QgsCoordinateReferenceSystem &crs)
Sets the destination coordinate reference system.
QgsPointXY transform(const QgsPointXY &point, Qgis::TransformDirection direction=Qgis::TransformDirection::Forward) const
Transform the point from the source CRS to the destination CRS.
void transformInPlace(double &x, double &y, double &z, Qgis::TransformDirection direction=Qgis::TransformDirection::Forward) const
Transforms an array of x, y and z double coordinates in place, from the source CRS to the destination...
bool isShortCircuited() const
Returns true if the transform short circuits because the source and destination are equivalent.
Q_DECL_DEPRECATED void setSourceDatumTransformId(int datumId)
Sets the datumId ID of the datum transform to use when projecting from the source CRS.
bool fallbackOperationOccurred() const
Returns true if a fallback operation occurred for the most recent transform.
bool operator==(const QgsCoordinateTransform &other) const
void transformPolygon(QPolygonF &polygon, Qgis::TransformDirection direction=Qgis::TransformDirection::Forward) const
Transforms a polygon to the destination coordinate system.
QgsCoordinateTransform & operator=(const QgsCoordinateTransform &o)
QgsRectangle transformBoundingBox(const QgsRectangle &rectangle, Qgis::TransformDirection direction=Qgis::TransformDirection::Forward, bool handle180Crossover=false) const
Transforms a rectangle from the source CRS to the destination CRS.
bool hasVerticalComponent() const
Returns true if the transform includes a vertical component, i.e.
void setAllowFallbackTransforms(bool allowed)
Sets whether "ballpark" fallback transformations can be used in the case that the specified coordinat...
bool isValid() const
Returns true if the coordinate transform is valid, ie both the source and destination CRS have been s...
static void invalidateCache(bool disableCache=false)
Clears the internal cache used to initialize QgsCoordinateTransform objects.
static void setCustomMissingPreferredGridHandler(const std::function< void(const QgsCoordinateReferenceSystem &sourceCrs, const QgsCoordinateReferenceSystem &destinationCrs, const QgsDatumTransform::TransformDetails &preferredOperation, const QgsDatumTransform::TransformDetails &availableOperation)> &handler)
Sets a custom handler to use when a coordinate transform is created between sourceCrs and destination...
QgsCoordinateReferenceSystem destinationCrs() const
Returns the destination coordinate reference system, which the transform will transform coordinates t...
bool operator!=(const QgsCoordinateTransform &other) const
Q_DECL_DEPRECATED int destinationDatumTransformId() const
Returns the ID of the datum transform to use when projecting to the destination CRS.
void disableFallbackOperationHandler(bool disabled)
Sets whether the default fallback operation handler is disabled for this transform instance.
void setCoordinateOperation(const QString &operation) const
Sets a Proj string representing the coordinate operation which will be used to transform coordinates.
static void setDynamicCrsToDynamicCrsWarningHandler(const std::function< void(const QgsCoordinateReferenceSystem &sourceCrs, const QgsCoordinateReferenceSystem &destinationCrs)> &handler)
Sets a custom handler to use when the desired coordinate operation for use between sourceCrs and dest...
Q_DECL_DEPRECATED int sourceDatumTransformId() const
Returns the ID of the datum transform to use when projecting from the source CRS.
static void setCustomMissingGridUsedByContextHandler(const std::function< void(const QgsCoordinateReferenceSystem &sourceCrs, const QgsCoordinateReferenceSystem &destinationCrs, const QgsDatumTransform::TransformDetails &desiredOperation)> &handler)
Sets a custom handler to use when a coordinate operation was specified for use between sourceCrs and ...
static void setFallbackOperationOccurredHandler(const std::function< void(const QgsCoordinateReferenceSystem &sourceCrs, const QgsCoordinateReferenceSystem &destinationCrs, const QString &desiredOperation)> &handler)
Sets a custom handler to use when the desired coordinate operation for use between sourceCrs and dest...
Custom exception class for Coordinate Reference System related exceptions.
static QgsDatumTransform::TransformDetails transformDetailsFromPj(PJ *op)
Returns the transform details for a Proj coordinate operation op.
QString what() const
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(), Qgis::StringFormat format=Qgis::StringFormat::PlainText)
Adds a message to the log instance (and creates it if necessary).
Represents a 2D point.
Definition qgspointxy.h:62
double sqrDist(double x, double y) const
Returns the squared distance between this point a specified x, y coordinate.
Definition qgspointxy.h:189
double y
Definition qgspointxy.h:66
double x
Definition qgspointxy.h:65
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,...
Definition qgsproject.h:114
QgsCoordinateTransformContext transformContext
Definition qgsproject.h:121
A convenience class that simplifies locking and unlocking QReadWriteLocks.
@ Write
Lock for write.
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 rounded to the spec...
double xMinimum
double yMinimum
double xMaximum
void setXMinimum(double x)
Set the minimum x value.
void setXMaximum(double x)
Set the maximum x value.
double yMaximum
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...
Definition qgsvector3d.h:33
double y() const
Returns Y coordinate.
Definition qgsvector3d.h:60
double z() const
Returns Z coordinate.
Definition qgsvector3d.h:62
double x() const
Returns X coordinate.
Definition qgsvector3d.h:58
#define Q_NOWARN_DEPRECATED_POP
Definition qgis.h:8329
#define Q_NOWARN_DEPRECATED_PUSH
Definition qgis.h:8328
bool qgsNanCompatibleEquals(double a, double b)
Compare two doubles, treating nan values as equal.
Definition qgis.h:7645
struct pj_ctx PJ_CONTEXT
struct PJconsts PJ
const char * finder(const char *name)
#define QgsDebugMsgLevel(str, level)
Definition qgslogger.h:80
#define QgsDebugError(str)
Definition qgslogger.h:71
Contains information about a projection transformation grid file.
Contains information about a coordinate transformation operation.
QString proj
Proj representation of transform operation.