Isis 3 Programmer Reference
UpturnedEllipsoidTransverseAzimuthal.cpp
1
6/* SPDX-License-Identifier: CC0-1.0 */
7#include "UpturnedEllipsoidTransverseAzimuthal.h"
8
9#include <cmath>
10#include <cfloat>
11#include <iostream>
12#include <iomanip>
13
14#include <QDebug>
15
16#include "Constants.h"
17#include "IException.h"
18#include "IString.h"
19#include "TProjection.h"
20#include "Pvl.h"
21#include "PvlGroup.h"
22#include "PvlKeyword.h"
23#include "SpecialPixel.h"
24
25using namespace std;
26namespace Isis {
47 UpturnedEllipsoidTransverseAzimuthal::UpturnedEllipsoidTransverseAzimuthal(Pvl &label,
48 bool allowDefaults) :
49 TProjection::TProjection(label) {
50 try {
51 // This algorithm can be found in the professional paper
52 // Cartographic Projections for Small Bodies of the Solar System
53 // by Maria E Fleis, Kira B Shingareva,
54 // Michael M Borisov, Michael V Alexandrovich, and Philip Stooke
55
56 // Try to read the mapping group
57 PvlGroup &mapGroup = label.findGroup("Mapping", Pvl::Traverse);
58
59 // Compute and write the default center longitude if allowed and
60 // necessary
61 double centerLongitude = 0.0;
62 if (!mapGroup.hasKeyword("CenterLongitude")) {
63 if (allowDefaults) {
64 // centerLongitude still 0.0 here
65 mapGroup += PvlKeyword("CenterLongitude", toString(centerLongitude), "Degrees");
66 }
67 else {
68 QString message = "Cannot project using upturned ellipsoid Transverse Azimuthal";
69 message += " without [CenterLongitude] value. Keyword does not exist";
70 message += " in labels and defaults are not allowed.";
71 throw IException(IException::Unknown, message, _FILEINFO_);
72 }
73 }
74 else {
75 centerLongitude = mapGroup["CenterLongitude"];
76 }
77
78 if (MinimumLongitude() < centerLongitude - 90.0) {
79 QString message = "MinimumLongitude ["
80 + toString(MinimumLongitude())
81 + "] is invalid. Must be within -90 degrees of the CenterLongitude ["
82 + toString(centerLongitude) + "].";
83 throw IException(IException::Unknown, message, _FILEINFO_);
84 }
85 if (MaximumLongitude() > centerLongitude + 90.0) {
86 QString message = "MaximumLongitude ["
87 + toString(MaximumLongitude())
88 + "] is invalid. Must be within +90 degrees of the CenterLongitude ["
89 + toString(centerLongitude) + "].";
90 throw IException(IException::Unknown, message, _FILEINFO_);
91 }
92
93 // Get the center longitude & latitude
94 // initialize member variables
95 init(centerLongitude);
96 }
97 catch(IException &e) {
98 QString message = "Invalid label group [Mapping]";
99 throw IException(e, IException::Unknown, message, _FILEINFO_);
100 }
101 }
102
103
109 UpturnedEllipsoidTransverseAzimuthal::~UpturnedEllipsoidTransverseAzimuthal() {
110 }
111
112
123 bool UpturnedEllipsoidTransverseAzimuthal::operator== (const Projection &proj) {
124 // don't do the below it is a recursive plunge
125 // if (Projection::operator!=(proj)) return false;
126
127 // all other data members calculated using m_lambda0, m_a and m_b.
128 UpturnedEllipsoidTransverseAzimuthal *tcyl = (UpturnedEllipsoidTransverseAzimuthal *) &proj;
129 if ((tcyl->m_lambda0 != m_lambda0) ||
130 (tcyl->m_a != m_a) ||
131 (tcyl->m_b != m_b)) return false;
132
133 return true;
134 }
135
136
144 QString UpturnedEllipsoidTransverseAzimuthal::Name() const {
145 return "UpturnedEllipsoidUpturnedEllipsoidTransverseAzimuthal";
146 }
147
148
154 QString UpturnedEllipsoidTransverseAzimuthal::Version() const {
155 return "1.0";
156 }
157
158
170 void UpturnedEllipsoidTransverseAzimuthal::init(double centerLongitude) {
171
172 // adjust for positive east longitude direction and convert to radians
173 if (IsPositiveEast()) {
174 m_lambda0 = centerLongitude * DEG2RAD;
175 }
176 else {
177 m_lambda0 = ToPositiveEast(centerLongitude, 360) * DEG2RAD;
178 }
179 if (Has180Domain()) {
180 m_lambda0 = To180Domain(m_lambda0);
181 }
182 else {
183 m_lambda0 = To360Domain(m_lambda0);
184 }
185
186 // Initialize miscellaneous protected data elements
187 m_good = false;
188
189 m_minimumX = DBL_MAX;
190 m_maximumX = -DBL_MAX;
191 m_minimumY = DBL_MAX;
192 m_maximumY = -DBL_MAX;
193
194 // Descriptions of the variables a, e
195 double axis1 = EquatorialRadius();
196 double axis2 = PolarRadius();
197 m_a = qMax(axis1, axis2); // major axis. generally, this is the larger of the equatorial axes
198 m_b = qMin(axis1, axis2); // minor axis. generally, this is the polar axis
199 m_e = Eccentricity(); // e = sqrt(1 - (b/a)^2)
200 // must be 0 <= e < 1
201 if (qFuzzyCompare(0.0, m_e)) {
202 m_e = 0.0;
203 }
204
205 // to reduce calculations, calculate some scalar constants:
206 double t0 = m_b / m_a; // equals sqrt( 1 - m_e * m_e );
207 m_t = t0 * t0; // equals 1 - m_e * m_e ;
208 m_t1 = m_e * t0; // equals e/sqrt( 1 - m_e * m_e );
209 double k1 = 2 * m_a * exp(m_t1 * atan(m_t1));
210 // k = radius of the Equator of transverse graticule on Azimuthal projection
211 // under condition of no distortion in the center of projection
212 m_k = k1 * m_t;
213 }
214
215
233 bool UpturnedEllipsoidTransverseAzimuthal::SetGround(const double lat,
234 const double lon) {
235 // when lat/lon is Null we can't set ground
236 if (lat == Null || lon == Null) {
237 m_good = false;
238 return m_good;
239 }
240
241 // convert given lat to radians, planetocentric
242 // phiNorm = The planetocentric latitude, in normal aspect.
243 double phiNorm;
244 // when lat just barely beyond pole, set equal to pole
245 if (qFuzzyCompare(90.0, qAbs(lat)) && qAbs(lat) > 90.0) {
246 phiNorm = copysign(HALFPI, lat); // returns sign(lat) * PI/2
247 }
248 // if well beyond pole, it's bad
249 else if (qAbs(lat) > 90.0) {
250 m_good = false;
251 return m_good;
252 }
253 else if (IsPlanetocentric()) {
254 phiNorm = lat * DEG2RAD;
255 }
256 else {
257 // equations expect ocentric latitudes.
258 phiNorm = ToPlanetocentric(lat) * DEG2RAD;
259 }
260
261 // convert given lon to positive east, then subtract center lon to get
262 // lambdaNorm = longitude east of the center of projection
263 double lambdaNorm;
264 if (IsPositiveEast()) {
265 lambdaNorm = lon * DEG2RAD - m_lambda0;
266 }
267 else {
268 lambdaNorm = ToPositiveEast(lon, 360) * DEG2RAD - m_lambda0;
269 }
270
271 // Compute the rectangular x,y coordinate using the appropriate set of equations below
272 double x,y;
273
274 // NOTE: z = the angular distance from the center of projection.
275 double cosz = cos(phiNorm) * cos(lambdaNorm);
276
277 // First, we take care of the edge cases where
278 // 1. a rounding error causes cosz to be outside of the range of cosine
279 // 2. z == 0 (this could be handled by the next set of equations,
280 // but taking care of it here reduces calculations.)
281 if (cosz >= 1) {
282 // This is the origin,
283 // at lat = equator, lon = centerlon
284 x = 0.0;
285 y = 0.0;
286 }
287
288 // This condition is used for the set of equations that are
289 // written to exclude the singularities where z == 0 or z == PI
290 // (i.e. the given longitude is equal to the center longitude
291 // or 180 degrees from it).
292 // Use these equations for 0.5 < cosz < 1.0, i.e. 0 < z < PI/3
293 else if (cosz > 0.5) { // && cosz < 1 is implied by bounds on cosine
294 // use pythagorean identity to get sine
295 double sinz = sqrt( 1 - cosz*cosz ); // due to restrictions above sinz != 0
296 // phi = the latitude at "upturned" ellipsoid of revolution
297 // NOTE: Since cos(z) > 0.5 or cos(z) < -0.5,
298 // there is no risk of zero denominator when dividing by cosz
299 double phi = HALFPI - atan2( sinz, m_t * cosz );
300 double sinPhi = sin(phi);
301 // rhoOverTanZ = rho/tanz
302 // where rho = the radius of latitude circle (transverse graticule)
303 // NOTE: We know sin(phi) = -1 only if
304 // phi = PI/2 - arctan(angle) = -PI/2 or 3PI/2
305 // but the range of arctangent is (-PI/2, PI/2),
306 // so there is no risk of zero denominator dividing by (1+sin(phi))
307 double rhoOverTanZ = m_k * sinPhi / ( ( 1 + sinPhi )
308 * m_t
309 * exp( m_t1 * atan( m_t1 * sinPhi ) ) );
310 x = rhoOverTanZ * tan(lambdaNorm);
311 y = rhoOverTanZ * (sin(phiNorm) / cosz ); // Note: cos(z) > 0.5, so no dividing by zero
312 }
313
314 // This condition is used for the set of equations that are
315 // written to exclude the singularity where z == PI/2.
316 // Use these equations for -1 <= cosz <= 0.5, i.e. PI/3 <= z < PI.
317 else {
318 // For this case, we will restrict z near multiples of PI using a
319 // tolerance value in order to avoid singularities at 0 and PI. We
320 // define zmin and zmax by:
321 //
322 // zmin = minimal value of angular distance from the center of projection
323 // = 0 + angular tolerance
324 // zmax = maximal value of angular distance from the center of projection
325 // in the neighborhood of point opposite to the center of projection
326 // = PI - angular tolerance
327 double tolerance = 0.0016;//???
328 double coszmax = cos(PI - tolerance);
329 double lambdaModulus = fmod(lambdaNorm, TWOPI);
330 if (cosz < coszmax) {
331 cosz = coszmax; // prevents cosz from equalling -1
332 // The following prevent lambdaNorm from being too close to +/-PI
333
334 // if the given longitude is within tolerance of -PI and less than -PI,
335 // set it to -PI - tolerance)
336 //if (qFuzzyCompare(lambdaNorm, -PI) && lambdaNorm <= -PI) {
337 if (-PI - tolerance < lambdaModulus && lambdaModulus <= -PI) {
338 lambdaNorm = -PI - tolerance;
339 }
340 // if the given longitude is within tolerance of -PI and greater than -PI,
341 // set it to -zmax (i.e. -PI + tolerance)
342 //
343 //if (qFuzzyCompare(lambdaNorm, -PI) && lambdaNorm > -PI) {
344 else if (-PI < lambdaModulus && lambdaModulus <= -PI + tolerance) {
345 lambdaNorm = -PI + tolerance;
346 }
347 // if the given longitude is within tolerance of PI and less than PI,
348 // set it to zmax (i.e. PI - tolerance)
349 //
350 //if (qFuzzyCompare(lambdaNorm, PI) && lambdaNorm <= PI) {
351 else if (PI - tolerance < lambdaModulus && lambdaModulus <= PI) {
352 lambdaNorm = PI - tolerance;
353 }
354 // if the given longitude is within tolerance of PI and greater than PI,
355 // set it to PI + tolerance
356 //
357 //if (qFuzzyCompare(lambdaNorm, PI) && lambdaNorm > PI) {
358 else if (PI < lambdaModulus && lambdaModulus < PI + tolerance) {
359 lambdaNorm = PI + tolerance;
360 }
361 }
362 // use pythagorean identity to get sine
363 double sinz = sqrt( 1 - cosz*cosz ); // due to restrictions above, we know sinz != 0
364
365 // phi = latitude at "upturned" ellipsoid of revolution
366 // NOTE: On the interval, PI/3 <= z < PI,
367 // we know 0 < sin(z) <= 1,
368 // so there is no risk of zero denom when dividing by sinz
369 double phi = atan2( m_t * cosz, sinz );
370 double sinPhi = sin(phi);
371 // Note: We know sin(phi) = -1 only if
372 // phi = arctan(angle) = -PI/2
373 // but the range of arctangent is the open interval (-PI/2, PI/2),
374 // so there is no risk of zero denominator dividing by (1+sin(phi))
375 double rhoOverSinZ = m_k * cos(phi) / ( ( 1 + sinPhi )
376 * sinz
377 * exp( m_t1 * atan(m_t1 * sinPhi ) ) );
378 x = rhoOverSinZ * cos(phiNorm) * sin(lambdaNorm);
379 y = rhoOverSinZ * sin(phiNorm);
380 }
381
382 // set member data and return status
383 m_good = true;
384 m_latitude = lat;
385 m_longitude = lon;
386 SetComputedXY(x, y); // sets m_x = x and m_y = y and handles rotation
387 // Note: if x or y are Null, sets m_good back to false
388 return m_good;
389 }
390
391
409 bool UpturnedEllipsoidTransverseAzimuthal::SetCoordinate(const double x,
410 const double y) {
411 if (x == Null || y == Null) {
412 m_good = false;
413 return m_good;
414 }
415 // Save the coordinate
416 SetXY(x, y);
417
418 if (qFuzzyCompare(x + 1.0, 1.0) && qFuzzyCompare(y + 1.0, 1.0)) {
419 // origin
420 m_latitude = 0.0;
421 m_longitude = m_lambda0 * RAD2DEG;
422 }
423 else {
424
425 // We are given the following forward equations:
426 //
427 // x = (rho/sin(z)) * cos(phiNorm) * sin(lambdaNorm)
428 // y = (rho/sin(z)) * sin(phiNorm)
429 // rho/sin(z) = ( k*cos(phi) / [(1+sin(phi))*sin(z)*e^{t1*arctan(t1*sin(phi)}] )
430 // phi = arctan( (1-e^2)*cos(z) / sin(z) )
431 //
432 // So, after simplifying, we can verify x^2 + y^2 = rho^2
433 // Thus, we have two solutions for rho:
434 //
435 // rho = +/- sqrt(x^2 + y^2)
436 // rho = ( k*cos(phi) / [(1+sin(phi))*e^{t1*arctan(t1*sin(phi)}] )
437 //
438 // Now, we can use Newton's root finding method for the function
439 // f(phi) = g(phi) - h(phi) on [-pi/2, pi/2]
440 // where
441 // g(phi) = k*cos(phi) / [(1+sin(phi))*e^{t1*arctan(t1*sin(phi)}]
442 // and
443 // h(phi) = sqrt(x^2 + y^2).
444 //
445 // After simplifying, the derivative with respect to phi is
446 // f'(phi) = -k * (1 + t1^2) /
447 // [(1 + sin(phi)) * e^{t1 * arctan(t1 * sin(phi)} * (1 + t1^2 * sin^2(phi))]
448
449 double phi0, fphi0, fprimephi0, phi1;
450 bool converged = false;
451 int iterations = 0;
452 double tolerance = 10e-10;
453 phi0 = 0.0; // start at the equator???
454 while (!converged && iterations < 1000) {
455 fphi0 = m_k * cos(phi0) / ((1 + sin(phi0)) * exp(m_t1 * atan( m_t1 * sin(phi0) )))
456 - sqrt(x*x + y*y);
457
458 fprimephi0 = -m_k * (1 + m_t1 * m_t1)
459 / ((1 + sin(phi0))
460 * exp(m_t1 * atan( m_t1 * sin(phi0) ))
461 * (1 + m_t1 * m_t1 * sin(phi0) * sin(phi0)));
462 phi1 = phi0 - fphi0 / fprimephi0;
463 // if phi wrapped at the poles, make sure phi is on [-pi/2, pi/2]
464 if (qAbs(phi1) > HALFPI) {
465 double phiDegrees = To180Domain(phi1*RAD2DEG);
466 if (phiDegrees > 90.0) {
467 phiDegrees -= 90.0;
468 }
469 if (phiDegrees < -180.0) {
470 phiDegrees += 90.0;
471 }
472 phi1 = phiDegrees * DEG2RAD;
473 }
474 if (qAbs(phi0 - phi1) < tolerance) {
475 converged = true;
476 }
477 else {
478 phi0 = phi1;
479 iterations++;
480 }
481 }
482
483 if (!converged) {
484 m_good = false;
485 return m_good;
486 }
487
488 // Now we have phi, the latitude at "upturned" ellipsoid of revolution.
489 double phi = phi1;
490
491 // Now use the forward equation for phi to solve for z, the angular distance from the center
492 // of projection:
493 //
494 // phi = arctan( (1-e^2) * cos(z) / sin(z))
495 //
496 double z = atan2((1 - m_e * m_e), tan(phi)); // see handling of cylind???
497
498 // Get phiNorm,the planetocentric latitude, in normal aspect. The range of arcsine is
499 // [-pi/2, pi/2] so we are guaranteed to get an angle between the poles. Use the forward
500 // equation:
501 //
502 // y = (rho / sinz ) * sin(phiNorm)
503 //double rho = ( m_k * cos(phi) / ((1+sin(phi))*exp(m_e, m_t1*arctan(m_t1*sin(phi))) );
504 double rho = sqrt(x*x + y*y);
505 double phiNorm = asin( y * sin(z) / rho );
506
507 // Get lambdaNorm, the longitude east of lambda0, using the forward equation
508 // cos(z) = cos(phiNorm) * cos(lambdaNorm)
509 double lambdaNorm;
510
511 // make sure we are in the correct quadrant...
512 double cosLambdaNorm = cos(z) / cos(phiNorm);
513 if (cosLambdaNorm > 1.0) {
514 lambdaNorm = 0.0;
515 }
516 else if (cosLambdaNorm < -1.0) {
517 lambdaNorm = PI; // +/- PI doesn't matter here... same answer either way
518 }
519 else if (x >= 0 && y >= 0) {
520 lambdaNorm = acos(cosLambdaNorm);
521 }
522 else if (x < 0 && y >= 0) {// fix conditional???
523 lambdaNorm = -acos(cosLambdaNorm);
524 }
525 else if (y < 0 && x >= 0) {// fix conditional???
526 lambdaNorm = acos(cosLambdaNorm);
527 }
528 else { // y < 0 && x < 0
529 lambdaNorm = -acos(cosLambdaNorm);
530 }
531
532 // calculations give positive east longitude
533 m_longitude = (lambdaNorm + m_lambda0) * RAD2DEG;
534 m_latitude = phiNorm * RAD2DEG;
535 }
536
537 // Cleanup the latitude
538 if (IsPlanetocentric()) {
539 m_latitude = ToPlanetocentric(m_latitude);
540 }
541
542 // Cleanup the longitude
543 if (IsPositiveWest()) {
544 m_longitude = ToPositiveWest(m_longitude, m_longitudeDomain);
545 }
546
547 if (Has180Domain()) {
548 m_longitude = To180Domain(m_longitude);
549 }
550 else {
551 // Do this because longitudeDirection could cause (-360,0)
552 m_longitude = To360Domain(m_longitude);
553 }
554
555 m_good = true;
556 return m_good;
557 }
558
559
596 bool UpturnedEllipsoidTransverseAzimuthal::XYRange(double &minX, double &maxX,
597 double &minY, double &maxY) {
598
599 // First check combinations of the lat/lon range boundaries
600 XYRangeCheck(m_minimumLatitude, m_minimumLongitude);
601 XYRangeCheck(m_maximumLatitude, m_minimumLongitude);
602 XYRangeCheck(m_minimumLatitude, m_maximumLongitude);
603 XYRangeCheck(m_maximumLatitude, m_maximumLongitude);
604
605 double centerLongitude = m_lambda0 * RAD2DEG;
606
607 bool centerLongitudeInRange = TProjection::inLongitudeRange(centerLongitude);
608 bool centerLongitude90InRange = TProjection::inLongitudeRange(centerLongitude + 90.0);
609 bool centerLongitude180InRange = TProjection::inLongitudeRange(centerLongitude + 180.0);
610 bool centerLongitude270InRange = TProjection::inLongitudeRange(centerLongitude + 270.0);
611
612 if (centerLongitudeInRange) {
613 XYRangeCheck(m_minimumLatitude, centerLongitude);
614 XYRangeCheck(m_maximumLatitude, centerLongitude);
615 }
616 if (centerLongitude90InRange) {
617 XYRangeCheck(m_minimumLatitude, centerLongitude + 90.0);
618 XYRangeCheck(m_maximumLatitude, centerLongitude + 90.0);
619 }
620 if (centerLongitude180InRange) {
621 XYRangeCheck(m_minimumLatitude, centerLongitude + 180.0);
622 XYRangeCheck(m_maximumLatitude, centerLongitude + 180.0);
623 }
624 if (centerLongitude270InRange) {
625 XYRangeCheck(m_minimumLatitude, centerLongitude + 270.0);
626 XYRangeCheck(m_maximumLatitude, centerLongitude + 270.0);
627 }
628
629 if (TProjection::inLatitudeRange(0.0)) {
630 XYRangeCheck(0.0, m_minimumLongitude);
631 XYRangeCheck(0.0, m_maximumLongitude);
632 if (centerLongitudeInRange) {
633 XYRangeCheck(0.0, centerLongitude);
634 }
635 if (centerLongitude90InRange) {
636 XYRangeCheck(0.0, centerLongitude + 90.0);
637 }
638 if (centerLongitude180InRange) {
639 XYRangeCheck(0.0, centerLongitude + 180.0);
640 }
641 if (centerLongitude270InRange) {
642 XYRangeCheck(0.0, centerLongitude + 270.0);
643 }
644 }
645
646 // Make sure everything is ordered
647 if (m_minimumX >= m_maximumX) return false;
648 if (m_minimumY >= m_maximumY) return false;
649
650 // Return X/Y min/maxs
651 minX = m_minimumX;
652 maxX = m_maximumX;
653 minY = m_minimumY;
654 maxY = m_maximumY;
655 return true;
656 }
657
658
683 PvlGroup UpturnedEllipsoidTransverseAzimuthal::Mapping() {
684 PvlGroup mapping = TProjection::Mapping();
685 mapping += PvlKeyword("CenterLongitude", toString(m_lambda0 * RAD2DEG));
686 return mapping;
687 }
688
689
704 PvlGroup UpturnedEllipsoidTransverseAzimuthal::MappingLatitudes() {
705 return TProjection::MappingLatitudes();
706 }
707
708
724 PvlGroup UpturnedEllipsoidTransverseAzimuthal::MappingLongitudes() {
725 PvlGroup mapping = TProjection::MappingLongitudes();
726 mapping += PvlKeyword("CenterLongitude", toString(m_lambda0 * RAD2DEG));
727 return mapping;
728 }
729
730} // end namespace isis
731
747extern "C" Isis::Projection *UpturnedEllipsoidTransverseAzimuthalPlugin(Isis::Pvl &lab,
748 bool allowDefaults) {
749 return new Isis::UpturnedEllipsoidTransverseAzimuthal(lab, allowDefaults);
750}
751
Base class for Map Projections.
Definition Projection.h:155
Container for cube-like labels.
Definition Pvl.h:119
Upturned Ellipsoid Transverse Azimuthal Map Projection.
This is free and unencumbered software released into the public domain.
Definition Apollo.h:16
QString toString(bool boolToConvert)
Global function to convert a boolean to a string.
Definition IString.cpp:211
const double DEG2RAD
Multiplier for converting from degrees to radians.
Definition Constants.h:43
const double HALFPI
The mathematical constant PI/2.
Definition Constants.h:41
const double RAD2DEG
Multiplier for converting from radians to degrees.
Definition Constants.h:44
const double PI
The mathematical constant PI.
Definition Constants.h:40
Namespace for the standard library.