Isis 3 Programmer Reference
LambertAzimuthalEqualArea.cpp
1
6/* SPDX-License-Identifier: CC0-1.0 */
7#include "LambertAzimuthalEqualArea.h"
8
9#include <cmath>
10#include <cfloat>
11
12#include <iostream>
13#include <iomanip>
14
15#include "Constants.h"
16#include "IException.h"
17#include "IString.h"
18#include "TProjection.h"
19#include "Pvl.h"
20#include "PvlGroup.h"
21#include "PvlKeyword.h"
22#include "SpecialPixel.h"
23
24using namespace std;
25namespace Isis {
62 LambertAzimuthalEqualArea::LambertAzimuthalEqualArea(
63 Pvl &label,
64 bool allowDefaults) :
65 TProjection::TProjection(label) {
66 try {
67 // This algorithm can be found in the USGS Professional Paper 1395
68 // Map Projections--A Working Manual by John P. Snyder
69
70 // Try to read the mapping group
71 PvlGroup &mapGroup = label.findGroup("Mapping", Pvl::Traverse);
72
73 // Compute and write the default center longitude if allowed and
74 // necessary
75 if (!mapGroup.hasKeyword("CenterLongitude")) {
76 if (allowDefaults) {
77 double centerLon = (MinimumLongitude() + MaximumLongitude()) / 2.0;
78 mapGroup += PvlKeyword("CenterLongitude", toString(centerLon), "Degrees");
79 }
80 else {
81 QString message = "Cannot project using Lambert Azimuthal equal-area";
82 message += " without [CenterLongitude] value. Keyword does not exist";
83 message += " in labels and defaults are not allowed.";
84 throw IException(IException::Unknown, message, _FILEINFO_);
85 }
86 }
87
88 // Compute and write the default center latitude if allowed and
89 // necessary
90 if (!mapGroup.hasKeyword("CenterLatitude")) {
91 if (allowDefaults) {
92 double centerLat = (MinimumLatitude() + MaximumLatitude()) / 2.0;
93 mapGroup += PvlKeyword("CenterLatitude", toString(centerLat), "Degrees");
94 }
95 else {
96 QString message = "Cannot project using Lambert Azimuthal equal-area";
97 message += " without [CenterLatitude] value. Keyword does not exist";
98 message += " in labels and defaults are not allowed.";
99 throw IException(IException::Unknown, message, _FILEINFO_);
100 }
101 }
102
103 // Get the center longitude & latitude
104 double centerLongitude = mapGroup["CenterLongitude"];
105 double centerLatitude = mapGroup["CenterLatitude"];
106
107 if (fabs(MinimumLongitude() - centerLongitude) > 360.0 ||
108 fabs(MaximumLongitude() - centerLongitude) > 360.0) {
109 IString message = "[MinimumLongitude,MaximumLongitude] range of [";
110 message += IString(MinimumLongitude())+","+IString(MaximumLongitude());
111 message += "] is invalid. No more than 360 degrees from the "
112 "CenterLongitude [" + IString(centerLongitude) + "] is allowed.";
113 throw IException(IException::Unknown, message, _FILEINFO_);
114 }
115
116 if (MaximumLongitude() - MinimumLongitude() > 360.0) {
117 IString message = "[MinimumLongitude,MaximumLongitude] range of ["
118 + IString(MinimumLongitude()) + ","
119 + IString(MaximumLongitude()) + "] is invalid. "
120 "No more than 360 degree range width is allowed.";
121 throw IException(IException::Unknown, message, _FILEINFO_);
122 }
123
124 // initialize member variables
125 init(centerLatitude, centerLongitude);
126 }
127 catch(IException &e) {
128 IString message = "Invalid label group [Mapping]";
129 throw IException(e, IException::Unknown, message, _FILEINFO_);
130 }
131 }
132
140 LambertAzimuthalEqualArea::~LambertAzimuthalEqualArea() {
141 }
142
155 bool LambertAzimuthalEqualArea::operator== (const Projection &proj) {
156 if (!Projection::operator==(proj)) return false;
157 // don't do the below it is a recursive plunge
158 // if (Projection::operator!=(proj)) return false;
159 LambertAzimuthalEqualArea *lama = (LambertAzimuthalEqualArea *) &proj;
160 if ((lama->m_phi1 != m_phi1) ||
161 (lama->m_lambda0 != m_lambda0) ||
162 (lama->m_a != m_a) ||
163 (lama->m_e != m_e)) return false;
164 // all other data members calculated using m_lambda0, m_phi1, m_a and m_e.
165 return true;
166 }
167
177 QString LambertAzimuthalEqualArea::Name() const {
178 return "LambertAzimuthalEqualArea";
179 }
180
186 QString LambertAzimuthalEqualArea::Version() const {
187 return "1.0";
188 }
189
204 double LambertAzimuthalEqualArea::TrueScaleLatitude() const {
205 //Snyder pg.
206 // no distortion at center of projection (centerLatitude, centerLongitude)
207 return m_phi1 * 180.0 / PI;
208 }
209
210
232 void LambertAzimuthalEqualArea::init(double centerLatitude,
233 double centerLongitude) {
234 // Initialize miscellaneous protected data elements
235 m_good = false;
236
237 m_minimumX = DBL_MAX;
238 m_maximumX = -DBL_MAX;
239 m_minimumY = DBL_MAX;
240 m_maximumY = -DBL_MAX;
241
242 // Test to make sure center longitude is valid
243 if (fabs(centerLongitude) > 360.0) {
244 IString message = "CenterLongitude [" + IString(centerLongitude);
245 message += "] is outside the range of [-360, 360]";
246 throw IException(IException::Unknown, message, _FILEINFO_);
247 }
248
249 // Test to make sure center lat is valid
250 if (fabs(centerLatitude) > 90.0) {
251 IString message = "CenterLatitude [" + IString(centerLatitude);
252 message += "] is outside the range of [-90, 90]";
253 throw IException(IException::Unknown, message, _FILEINFO_);
254 }
255
256 // We need to convert to planetographic since phi is geographic latitude
257 // (see phi and phi1 - Snyder pg ix)
258 if (IsPlanetocentric()) {
259 centerLatitude = ToPlanetographic(centerLatitude);
260 }
261
262 // adjust for positive east longitude direction
263 // (see lambda and lambda0 - Snyder pg ix)
264 if (m_longitudeDirection == PositiveWest) {
265 centerLongitude = -1.0*centerLongitude;
266 }
267
268 // Descriptions of the variables a, e, lambda0, and phi1
269 // can be found in Snyder text on pgs viii-ix, pg 187
270 m_a = EquatorialRadius();
271 m_e = Eccentricity(); // e = sqrt(1 - (PolarRadius/EqRadius)^2)
272 // must be 0 <= e < 1
273
274 // Snyder variables used for spherical projection
275 m_lambda0 = centerLongitude*PI / 180.0; // convert to radians
276 m_phi1 = centerLatitude*PI / 180.0; // convert to radians
277 m_sinPhi1 = sin(m_phi1);
278 m_cosPhi1 = cos(m_phi1);
279
280 // flags for determinining which formulas to use
281 m_spherical = false;
282 m_northPolarAspect = false;
283 m_southPolarAspect = false;
284 m_equatorialAspect = false;
285
286 if (qFuzzyCompare(0.0, m_e)) {
287 m_e = 0.0;
288 m_spherical = true;
289 }
290 if (qFuzzyCompare(HALFPI, m_phi1)) {
291 m_phi1 = HALFPI;
292 m_northPolarAspect = true;
293 }
294 if (qFuzzyCompare(-HALFPI, m_phi1)) {
295 m_phi1 = -HALFPI;
296 m_southPolarAspect = true;
297 }
298 if (qFuzzyCompare(0.0, m_phi1)) {
299 m_phi1 = 0.0;
300 m_equatorialAspect = true;
301 }
302
303 // Snyder ellipsoid variables will not be used
304 m_qp = Null;
305 m_q1 = Null;
306 m_m1 = Null;
307 m_beta1 = Null;
308 m_sinBeta1 = Null;
309 m_cosBeta1 = Null;
310 m_Rq = Null;
311 m_D = Null;
312
313 // other Snyder variables
314 m_h = Null;
315 m_k = Null;
316
317 // if eccentricity = 0, we are projecting a sphere.
318 if (!m_spherical) {
319 // Calculate Snyder ellipsoid variables
320 initEllipsoid();
321 }
322
323 // Check if the antipodal point is in the lat/lon ranges.
324 //
325 // We can only allow this if we have a polar projection.
326 // Otherwise, we cannot SetGround() for the antipodal point since it is
327 // projected as a circle, not a single point with (x,y) value.
328 //
329 // The antipodal point is defined by the coordinates
330 // (-centerLat, centerLon-180) or (-centerLat, centerLon+180)
331 if (!m_northPolarAspect && !m_southPolarAspect) {
332 if (-centerLatitude >= MinimumLatitude()
333 && -centerLatitude <= MaximumLatitude()
334 && ( (MinimumLongitude() <= centerLongitude - 180 &&
335 MaximumLongitude() >= centerLongitude - 180)
336 || (MinimumLongitude() <= centerLongitude + 180 &&
337 MaximumLongitude() >= centerLongitude + 180) ) ) {
338 IString message = "Invalid latitude and/or longitude range. ";
339 message += "Non-polar Lambert Azimuthal equal-area "
340 "projections can not project the antipodal point on "
341 "the body.";
342 throw IException(IException::Unknown, message, _FILEINFO_);
343 }
344 }
345 }
346
354 void LambertAzimuthalEqualArea::initEllipsoid() {
355 m_spherical = false;
356
357 // Decided not to use qCompute method here to reduce chance of roundoff
358 // error since q for polar simplifies
359 // m_qp = qCompute(1.0); //q for polar, sin(phi) = sin(pi/2) = 1.0
360 m_qp = 1 - (1 - m_e * m_e) / (2 * m_e) * log( (1 - m_e) / (1 + m_e) );
361 // Snyder eqn (3-12) pg 187, use phi = pi/2 (polar)
362 // Note: We know that qp is well defined since 0 < e < 1, thus
363 // 1+e != 0 (no 0 denominator) and (1-e)/(1+e) > 0 (log domain)
364 if (!m_northPolarAspect && !m_southPolarAspect) {
365 // we only need to compute these variables for oblique projections
366
367 m_q1 = qCompute(m_sinPhi1); // Snyder eqn (3-12) pg 187, use phi = phi1
368 m_m1 = mCompute(m_sinPhi1, m_cosPhi1); // Snyder eqn (14-15) pg 187,
369 //use phi = phi1, thus m1 = cosPhi1/sqrt(1-eSinPhi1*eSinPhi1)
370 m_beta1 = asin(m_q1 / m_qp); // Snyder eqn (3-11) pg 187, use q = q1
371 // Note: We can show mathematically that beta1 is well defined
372 // First, since 0 < eccentricity < 1, we have (1-e)/(1+e) < 1
373 // Thus, ln((1-e)/(1+e)) < 0 and so qp > 1 (no 0 denominator)
374 // Next, verify the domain for arcsine, -1 <= q1/qp <= 1
375 // Since not polar, |phi1| < 90, and since ellipsoid 0 < e < 1
376 // Then |sinPhi1| < 1, so e|sinPhi1| < e
377 // So, 1 < (1+e|sinPhi1|)/(1-e|sinPhi1|) < (1+e)/(1-e).
378 // 0 < ln[(1+e|sinPhi1|)/(1-e|sinPhi1|)] < ln[(1+e)/(1-e)]
379 // Note that (1-e^2)/(2e) > 0 and recall -ln(x) = ln(1/x)
380 // So |q1| = |sinPhi1 - (1-e^2)/(2e)ln[(1-eSinPhi1)/(1+eSinPhi1)]|
381 // = |sinPhi1 + (1-e^2)/(2e)ln[(1+eSinPhi1)/(1-eSinPhi1)]|
382 // <= |sinPhi1| + |(1-e^2)/(2e)ln[(1+eSinPhi1)/(1-eSinPhi1)]|
383 // = |sinPhi1| +(1-e^2)/(2e) |ln[(1+eSinPhi1)/(1-eSinPhi1)]|
384 // < 1 + (1-e^2)/(2e) |ln[(1+eSinPhi1)/(1-eSinPhi1)]|
385 // <= 1 + (1-e^2)/(2e) ln[(1+e|SinPhi1|)/(1-e|SinPhi1|)]
386 // < 1 + (1-e^2)/(2e) ln[(1+e)/(1-e)]
387 // = 1 - (1-e^2)/(2e)ln[(1-e)/(1+e)]
388 // = qp
389 m_sinBeta1 = sin(m_beta1);
390 m_cosBeta1 = cos(m_beta1);
391 m_Rq = m_a * sqrt(m_qp / 2); // Snyder eqn (3-13) pg 16, 187
392 // Note: We know Rq is well defined since qp > 1 (see m_beta1)
393 m_D = m_a * m_m1 / (m_Rq * m_cosBeta1); // Snyder eqn (24-20) pg 187
394 // Note: We know D is well-defined since
395 // cosBeta1 = 0 implies beta1 = +/-pi/2,
396 // which in turn implies q1 = +/-qp
397 // This should only happen if polar aspect,
398 // Furthermore, Rq = 0 is impossible since a > 0 and q > 1
399 }
400 }
401
421 bool LambertAzimuthalEqualArea::SetGround(const double lat,
422 const double lon) {
423 // when lat/lon is Null or lat is "beyond" the poles, we can't set ground
424 if (fabs(lat) - 90.0 > DBL_EPSILON || lat == Null || lon == Null) {
425 if (!qFuzzyCompare(90.0, fabs(lat))) {
426 m_good = false;
427 return m_good;
428 }
429 }
430
431 m_longitude = lon;
432 m_latitude = lat;
433
434 // assign input values to Snyder's phi and lambda variables
435 // convert to radians, positive east longitude, planetographic
436 double phi = lat * PI / 180.0;
437 double lambda = lon * PI / 180.0;
438
439 // when lat just barely beyond pole, set equal to pole
440 if (lat > 90.0 && qFuzzyCompare(90.0, lat)) {
441 phi = HALFPI;
442 }
443 if (lat < -90.0 && qFuzzyCompare(-90.0, lat)) {
444 phi = -HALFPI;
445 }
446
447 if (m_longitudeDirection == PositiveWest) lambda *= -1.0;
448 if (IsPlanetocentric()) phi = ToPlanetographic(phi);
449
450 if (!m_spherical) return setGroundEllipsoid(phi, lambda);
451
452 // calculate the following once to reduce computations
453 double sinPhi = sin(phi);
454 double cosPhi = cos(phi);
455 double sinLambdaDiff = sin(lambda - m_lambda0);
456 double cosLambdaDiff = cos(lambda - m_lambda0);
457
458 // Use the variables defined above to compute the x,y coordinate
459 double x,y;
460
461 // In this case, Spherical Radius = Equitorial Radius,
462 // so Snyder's R = Snyder's a variable pg ix
463 double R = m_a;
464 if (m_northPolarAspect ) {
465 double sinQuarterPiMinusHalfPhi = sin(PI/4-phi/2);
466 x = 2*R*sinQuarterPiMinusHalfPhi*sinLambdaDiff; //Snyder eqn (24-3) pg 186
467 y = -2*R*sinQuarterPiMinusHalfPhi*cosLambdaDiff;//Snyder eqn (24-4) pg 186
468 m_h = cos(PI/4-phi/2); // Snyder eqn (24-6) pg 186
469 m_k = 1/m_h; // Snyder eqn (24-5) and (24-6) pg 186
470 // - recall sec(theta) = 1/cos(theta)
471 }
472 else if (m_southPolarAspect ) {
473 double cosQuarterPiMinusHalfPhi = cos(PI/4-phi/2);
474 x = 2*R*cosQuarterPiMinusHalfPhi*sinLambdaDiff; //Snyder eqn (24-8) pg 186
475 y = 2*R*cosQuarterPiMinusHalfPhi*cosLambdaDiff; //Snyder eqn (24-9) pg 186
476 m_h = sin(PI/4-phi/2); // Snyder eqn (24-11) pg 186
477 m_k = 1/m_h; // Snyder eqn (24-10) and (24-11) pg 186
478 }
479 else { // spherical oblique aspect (this includes equatorial)
480
481 // Check if (phi, lambda) is the antipodal point (point diametrically
482 // opposite the center point of projection)
483 if (qFuzzyCompare(-m_phi1, phi) // phi = -phi1
484 && fabs(fmod(lambda-m_lambda0, PI)) < DBL_EPSILON //lambda-lambda0=k*PI
485 && fabs(fmod(lambda-m_lambda0, 2*PI)) > DBL_EPSILON) { // k is odd
486 // We don't allow this for oblique aspect projections. This point,
487 // at the opposite side of the planet, is projected as a circle of
488 // radius 2R - we can't return a unique (x,y) value.
489 m_good = false;
490 return m_good;
491 }
492
493 // First, be sure that the denominator will not be zero
494 double trigTerms;
495 if (m_equatorialAspect) {
496 // If m_phi1 == 0, the general case below cancels alegbraically
497 // to the following equations, however, the simplified equations
498 // are used here to reduce chance of roundoff errors
499 // -- Snyder eq (24-14) pg 186
500 trigTerms = cosPhi*cosLambdaDiff;
501 }
502 else {
503 // general case for oblique projections -- Snyder eq (24-2) pg 185
504 double sinProduct = m_sinPhi1 * sinPhi;
505 double cosProduct = m_cosPhi1 * cosPhi * cosLambdaDiff;
506 trigTerms = sinProduct + cosProduct;
507 }
508 // make sure when we add 1 to trigTerms that we are not near zero
509 if (qFuzzyCompare(-1.0, trigTerms)) {
510 m_good = false;
511 return m_good;
512 }
513 double denominator = 1 + trigTerms;
514 double kprime = sqrt(2/denominator); // Snyder eqn (24-2) or (24-14) pg 185-186
515 x = R*kprime*cosPhi*sinLambdaDiff; // Snyder eqn (22-4) pg 185
516 if (m_equatorialAspect) {
517 // If m_phi1 == 0, the general case below cancels alegbraically
518 // to the following, however, use the simplified equations for y
519 // to reduce the likelihood of roundoff errors
520 // -- Snyder eq (24-13) pg 186
521 y = R*kprime*sinPhi;
522 }
523 else { // spherical general oblique aspect -- Snyder eqn (22-5) pg 185
524 y = R*kprime*(m_cosPhi1*sinPhi - m_sinPhi1*cosPhi*cosLambdaDiff);
525 }
526 // these are the relative scale factors for oblique aspect
527 m_k = kprime;
528 m_h = 1/m_k;
529 }
530
531 SetComputedXY(x, y); // sets m_x = x and m_y = y and handles rotation
532 m_good = true;
533 return m_good;
534 }
535
550 bool LambertAzimuthalEqualArea::setGroundEllipsoid(double phi,
551 double lambda) {
552 // calculate the following once to reduce computations
553 double sinPhi = sin(phi);
554 double cosPhi = cos(phi);
555 double sinLambdaDiff = sin(lambda - m_lambda0);
556 double cosLambdaDiff = cos(lambda - m_lambda0);
557
558 // Use the variables passed in to compute the x,y coordinate
559 double x,y;
560
561 // projecting an ellipsoid
562 double q = qCompute(sinPhi); //q for given lat
563 if (qFuzzyCompare(PI/2, phi)) {
564 q = m_qp; // reduce roundoff error
565 }
566 if (qFuzzyCompare(-PI/2, phi)) {
567 q = -m_qp; // reduce roundoff error
568 }
569 // q = (1-e^2)*(sinphi/(1-e^2sinphi^2))-1/(2e)*log((1-esinphi)/(1+esinphi)))
570
571 double m = mCompute(sinPhi, cosPhi); // m = cosPhi/sqrt(1-eSinPhi*eSinPhi)
572 if (m_northPolarAspect) {
573 double rho = m_a* sqrt(m_qp - q); // Snyder eqn (24-23) pg 188
574 // Note: Rho is well defined (qp >= q) as shown in initEllipsoid()
575 x = rho * sinLambdaDiff; // Snyder eqn (21-30) pg 188
576 y = -1.0 * rho * cosLambdaDiff; // Snyder eqn (21-31) pg 188
577 if (qFuzzyCompare(m_phi1, phi)) {
578 m_k = 1; // Since True scale at clat/clon - see Snyder page 190, table 29
579 }
580 else{
581 m_k = rho / (m_a * m); // Snyder eqn (21-32) pg 188
582 }
583 m_h = 1 / m_k; // Snyder paragraph after eqn (24-23) pg 188
584
585 }
586 else if (m_southPolarAspect) {
587 double rho = m_a* sqrt(m_qp + q); // Snyder eqn (24-25) pg 188
588 // Note: Rho is well defined (qp >= -q) as shown in initEllipsoid()
589 x = rho * sinLambdaDiff; // Snyder eqn (21-30) pg 188
590 y = rho * cosLambdaDiff; // Snyder eqn (24-24) pg 188
591 if (qFuzzyCompare(m_phi1, phi)) {
592 m_k = 1; // Since True scale at clat/clon - see Snyder page 190, table 29
593 }
594 else{
595 m_k = rho / (m_a * m); // Snyder eqn (21-32) pg 188
596 }
597 m_h = 1 / m_k;
598 }
599 else { // ellipsoidal oblique aspect
600 // following comment is impossible -- see initEllipsoid(), |q1| > |qp|
601 // if (fabs(q) > fabs(m_qp)) {
602 // m_good = false;
603 // return m_good;
604 // }
605 double beta = asin( q/m_qp); // Snyder eqn (3-11) pg 187
606 // Note: beta is well defined- see initEllipsoid() m_beta1
607
608 // calculate the following once to reduce computations when defining
609 // Snyder's variable B
610 double sinBeta = sin(beta);
611 double cosBeta = cos(beta);
612
613 if (m_equatorialAspect) {
614 // If phi1 == 0, the general case below cancels alegbraically to
615 // the following equations, however, the simplified equations are
616 // used here to reduce chance of roundoff errors
617 double trigTerm = cosBeta*cosLambdaDiff;
618 if (qFuzzyCompare(-1, trigTerm)) { // avoid divide by zero & possible roundoff errors
619 // This happens when trying to project the antipodal point.
620 // lambda = lambda0 +/- 180 and beta = 0 (that is, phi = 0)
621 // Thus denominator = 1 + (1)(-1) = 0
622 // We don't allow this for equatorial aspect projections. This point,
623 // at the opposite side of the planet, is projected as a circle of
624 // radius 2R - we can't return a unique (x,y) value.
625 m_good = false;
626 return m_good;
627 }
628 double denominator = 1 + trigTerm;
629 x = m_a * cosBeta * sinLambdaDiff * sqrt(2/denominator);
630 // Snyder eqn (24-21) pg 187
631 y = (m_Rq*m_Rq/m_a) * sinBeta * sqrt(2/denominator);
632 // Snyder eqn (24-22) pg 187
633 // Note: Clearly 1 + cosBeta*cosLambdaDiff >= 0
634 // 1 + cosBeta*cosLambdaDiff = 0 implies 2 cases
635 // 1) cosBeta = 1 and cosLambdaDiff = -1
636 // So, beta = 0 and lambdaDiff = 180
637 // Then, q = 0, which implies phi = 0 = phi1
638 // this only happens when projecting opposite side of equator
639 // 2) cosBeta = -1 and cosLambdaDiff = 1
640 // So, beta = 180 impossible since beta is defined by asin
641 // (range of asin is -90 to 90)
642 // Then, q = 0, which implies phi = 0 = phi1
643 // this only happens when projecting opposite side of equator
644 // Therefore x and y are well defined
645 }
646 else { // ellipsoidal General oblique aspect
647 double sinProduct = m_sinBeta1 * sinBeta;
648 double cosProduct = m_cosBeta1 * cosBeta * cosLambdaDiff;
649 double trigTerms = sinProduct + cosProduct;
650 if (qFuzzyCompare(-1, trigTerms)) { // avoid divide by zero & possible roundoff errors
651 // This happens when trying to project the antipodal point.
652 // lambda = lambda0 + 180 and beta = -beta1 (that is, phi = -phi1)
653 // Then, by odd symmetry of sine function, sinBeta = -sinBeta1,
654 // by even symmetry of cosine, cosBeta = cosBeta1
655 // and cosLambdaDiff = cos(180) = -1
656 // Thus denominator = 1 - sin^2(beta) - cos^2(beta) = 1-1 = 0
657 // We don't allow this for oblique aspect projections. This point,
658 // at the opposite side of the planet, is projected as a circle of
659 // radius 2R - we can't return a unique (x,y) value.
660 m_good = false;
661 return m_good;
662 }
663 double denominator = 1 + trigTerms;
664 double kprime = sqrt(2/denominator); // Snyder eqn (24-2) page 185
665 double B = m_Rq * kprime; // Snyder eqns (24-19) pg 187
666
667 x = B * m_D * cosBeta*sinLambdaDiff; // Snyder eqn (24-17) pg 187
668 y = (B/m_D) * (m_cosBeta1*sinBeta - m_sinBeta1*cosBeta*cosLambdaDiff);
669 // Snyder eqn (24-18) pg 187
670 // Note: y is well defined since D = 0 implies a = 0 or m1 = 0
671 // we know a > 0,
672 // so m1 = 0 would imply cosPhi1 = 0, thus phi1 = +/- 90
673 // but this calculation is not used for polar aspect,
674 // so y doesn't have zero denominator
675 }
676 // There are no ellipsoidal values for the scale factors h and k unless
677 // using the polar aspects, Snyder pg 26.
678 }
679 SetComputedXY(x, y); // sets m_x = x and m_y = y and handles rotation
680
681 m_good = true;
682 return m_good;
683 }
684
704 bool LambertAzimuthalEqualArea::SetCoordinate(const double x,
705 const double y) {
706 if (x == Null || y == Null) {
707 m_good = false;
708 return m_good;
709 }
710 // Save the coordinate
711 SetXY(x, y);
712
713 // 2012-06-11
714 // Uncomment following line if ellipsoid projection is implemented
715 if (!m_spherical) return setCoordinateEllipsoid(x,y);
716
717 double phi; // latitude value to be calculated
718 double lambda; // longitude value to be calculated
719
720 // Spherical Radius = Equatorial Radius, so
721 // Snyder's R = Snyder's a variable pg ix
722 double R = m_a;
723
724 double rho = sqrt(x*x+y*y); // Snyder eqn (20-18) pg 187
725 if (rho < DBL_EPSILON) { // this implies (x,y) = (0,0),
726 // so project to center lat/lon values
727 phi = m_phi1;
728 lambda = m_lambda0;
729 }
730 else {
731 if (fabs(rho/(2*R)) > 1 + DBL_EPSILON) {// given (x,y) point is off planet
732 // projection if distance from
733 // origin (rho) is greater than
734 // 2*radius of sphere
735 m_good = false;
736 return m_good;
737 }
738 else if (fabs(rho/(2*R)) > 1) { // if ratio is slightly larger than 1
739 // due to rounding error, then fix so we
740 // can take the arcsin
741 rho = 2*R;
742 }
743 double c = 2*asin(rho/(2*R)); // c is angular distance,
744 //Snyder eqn (24-16) pg 187
745 double sinC = sin(c);
746 double cosC = cos(c);
747
748 // verify the following is in the domain of the arcsin function
749 if (fabs(cosC*m_sinPhi1+y*sinC*m_cosPhi1/rho) > 1) {
750 m_good = false;
751 return m_good;
752 }
753 phi = asin(cosC*m_sinPhi1+y*sinC*m_cosPhi1/rho);//Snyder eq (20-14) pg 186
754 if (m_northPolarAspect ) { // spherical north polar aspect
755 lambda = m_lambda0 + atan2(x,-y); // Snyder eqn (20-16) pg 187,
756 // need to use atan2 to correct for
757 // quadrant - see pg 150
758 }
759 else if (m_southPolarAspect ) { // spherical north polar aspect
760 lambda = m_lambda0 + atan2(x,y); // Snyder eqn (20-17) pg 187, 150
761 }
762 else { // spherical oblique aspect
763 double denominator = rho*m_cosPhi1*cosC - y*m_sinPhi1*sinC;
764 // atan2 well defined for denominator = 0
765 // if numerator is also 0, returns 0
766 lambda = m_lambda0 + atan2(x * sinC, denominator);
767 // Snyder eqn (20-15) pg 186
768 }
769 }
770
771 m_latitude = phi * 180.0 / PI;
772 m_longitude = lambda * 180.0 / PI;
773
774 // Cleanup the longitude
775 if (m_longitudeDirection == PositiveWest) {
776 m_longitude *= -1.0;
777 }
778
779 if (m_longitudeDomain == 180) {
780 m_longitude = To180Domain(m_longitude);
781 }
782 else {
783 // Do this because longitudeDirection could cause (-360,0)
784 m_longitude = To360Domain(m_longitude);
785 }
786
787 // Cleanup the latitude
788 if (IsPlanetocentric()) {
789 m_latitude = ToPlanetocentric(m_latitude);
790 }
791
792 m_good = true;
793 return m_good;
794 }
795
813 bool LambertAzimuthalEqualArea::setCoordinateEllipsoid(const double x,
814 const double y) {
815 double phi; // latitude value to be calculated
816 double lambda; // longitude value to be calculated
817
818 // ellipsoidal projection
819 double q;
820 // for polar aspects
821 double rho = sqrt(x*x+y*y); // Snyder eqn (20-18) pg 190
822 if (m_northPolarAspect ) { // ellipsoidal north polar aspect
823 q = m_qp - (rho*rho/(m_a*m_a)); // Snyder eqn (24-31) pg 190
824 lambda = m_lambda0 + atan2(x,-y); // Snyder eqn (20-16) pgs 190, 150
825 }
826 else if (m_southPolarAspect ) { // ellipsoidal south polar aspect
827 q = -1.0*(m_qp - (rho*rho/(m_a*m_a))); // Snyder eqn (24-31) pg 190
828 lambda = m_lambda0 + atan2(x,y); // Snyder eqn (20-17) pg 190, 150
829 }
830 else {// ellipsoidal oblique aspect
831 double xD = x/m_D; // m_D = 0 implies polar aspect
832 // (see setGroundEllipsoid() general oblique case
833 double Dy = m_D*y;
834 double rho = sqrt((xD*xD)+(Dy*Dy)); // Snyder eqn (24-28) pg 189
835 if (fabs(rho) > fabs(2*m_Rq)) {
836 m_good = false;
837 return m_good;
838 }
839 double Ce = 2*asin(rho/(2*m_Rq)); // Snyder eqn (24-29) pg 189
840 // TYPO IN TEXT, parentheses required
841 // around denominator (see usage pg 335)
842 // Note: Ce well defined since Rq = 0 is impossible (a > 0 and q > 1)
843 double sinCe = sin(Ce);
844 double cosCe = cos(Ce);
845
846 if (rho < DBL_EPSILON) { // pg 189, first line appears to be typo in text,
847 // below is our interpretation.
848 // if rho = 0, then x=0 and (D=0 or y=0), so it
849 // makes sense that lambda = lambda0 in this case.
850 q = m_qp * m_sinBeta1;
851 lambda = m_lambda0;
852 }
853 else {
854 q = m_qp * (cosCe * m_sinBeta1 + m_D * y * sinCe * m_cosBeta1 / rho);
855 // Snyder eqn (24-27) pg 188
856 double numerator = x * sinCe;
857 double denominator = m_D * rho * m_cosBeta1 * cosCe
858 - m_D * m_D * y * m_sinBeta1 * sinCe;
859 // atan2 well defined for denominator = 0
860 // if numerator is also 0, returns 0
861 lambda = m_lambda0 + atan2(numerator, denominator);
862 // Snyder eqn (24-26) pg 188
863 }
864 }
865
866 if (qFuzzyCompare(fabs(q), fabs(m_qp))) {
867 phi = copysign(HALFPI,q);// Snyder eqn (14-20) pg 189,
868 //(see definition of qp on pg 187)
869 }
870 else {
871 if (fabs(q) > 2) {
872 // verify that q/2 is in the domain of arcsin
873 m_good = false;
874 return m_good;
875 }
876 phi = asin(q/2); // Snyder pg 189 above (14-20) describes iteration
877 // process and initial value for phi
878 double phiOld = phi;
879 bool converge = false;
880 double tolerance = 1e-10; // tolerance same as Projection::phi2Compute()
881 // should we attempt to relate tolerance to pixel resolution ???
882 int maximumIterations = 100;
883 int i = 0;
884 while (!converge) {
885 i++;
886 // calculate values to reduce calculations:
887 double sinPhi = sin(phi);
888 double eSinPhi = m_e*sinPhi;
889 double cosPhi = cos(phi);
890 double squareESinPhi = eSinPhi*eSinPhi;
891 double oneMinusSquareESinPhi = 1 - squareESinPhi;
892 // find next iteration of phi
893 phi += oneMinusSquareESinPhi*oneMinusSquareESinPhi / (2 * cosPhi)
894 * (q / (1 - m_e * m_e)
895 - sinPhi / (oneMinusSquareESinPhi)
896 + log( (1 - eSinPhi) / (1 + eSinPhi) ) / (2 * m_e));
897 //eqn (3-16) pg 188
898 if (fabs(phiOld - phi) < tolerance) {
899 converge = true;
900 }
901 if (i > maximumIterations) {
902 m_good = false;
903 return m_good;
904 }
905 phiOld = phi;
906 }
907 }
908 m_latitude = phi * 180.0 / PI;
909 m_longitude = lambda * 180.0 / PI;
910
911 // Cleanup the longitude
912 if (m_longitudeDirection == PositiveWest) {
913 m_longitude *= -1.0;
914 }
915
916 if (m_longitudeDomain == 180) {
917 m_longitude = To180Domain(m_longitude);
918 }
919 else {
920 // Do this because longitudeDirection could cause (-360,0)
921 m_longitude = To360Domain(m_longitude);
922 }
923
924 // Cleanup the latitude
925 if (IsPlanetocentric()) {
926 m_latitude = ToPlanetocentric(m_latitude);
927 }
928
929 m_good = true;
930 return m_good;
931 }
932
971 bool LambertAzimuthalEqualArea::XYRange(double &minX, double &maxX,
972 double &minY, double &maxY) {
973 // Global Polar projection
974 if ((m_northPolarAspect && qFuzzyCompare(-90.0, MinimumLatitude()))
975 || (m_southPolarAspect && qFuzzyCompare(90.0, MaximumLatitude()))) {
976 // for polar aspect projections, the antipodal point is the opposite pole.
977 // if it is included in lat range, the bounding circle will exist, no
978 // matter the longitude range.
979 double eRad = EquatorialRadius();
980 double pRad = PolarRadius();
981 double maxCoordValue = 0;
982
983 if (m_spherical) {
984 maxCoordValue = 2*EquatorialRadius();
985 }
986 else {
987 maxCoordValue = sqrt(2*eRad*eRad
988 + pRad*pRad*log((1+m_e)/(1-m_e))/m_e);
989 }
990 m_minimumX = -maxCoordValue;
991 m_maximumX = maxCoordValue;
992 m_minimumY = -maxCoordValue;
993 m_maximumY = maxCoordValue;
994 }
995 // Polar projection, not including antipodal point
996 else if (m_northPolarAspect || m_southPolarAspect) {
997 return xyRangeLambertAzimuthalPolar(minX, maxX, minY, maxY);
998 }
999 // Oblique projection
1000 else { // oblique cases (includes equatorial aspect)
1001 if (xyRangeOblique(minX, maxX, minY, maxY)) {
1002 // Make sure that the calculations didn't go beyond the accepatable x,y
1003 // values. As far as we know, the x, y values should not exceed
1004 // 2*LocalRadius(-phi)
1005 double maxCoordValue = 2*LocalRadius(-m_phi1*180/PI);
1006 if (m_minimumX < -maxCoordValue) m_minimumX = -maxCoordValue;
1007 if (m_maximumX > maxCoordValue) m_maximumX = maxCoordValue;
1008 if (m_minimumX < -maxCoordValue) m_minimumX = -maxCoordValue;
1009 if (m_maximumY > maxCoordValue) m_maximumY = maxCoordValue;
1010 }
1011 else {
1012 return false;
1013 }
1014 }
1015
1016 // If we haven't already returned, we have a Global Polar projection or an
1017 // Oblique projection that did not fail xyRangeOblique
1018
1019 // Make sure everything is ordered
1020 if (m_minimumX >= m_maximumX) return false;
1021 if (m_minimumY >= m_maximumY) return false;
1022
1023 // Return X/Y min/maxs
1024 minX = m_minimumX;
1025 maxX = m_maximumX;
1026 minY = m_minimumY;
1027 maxY = m_maximumY;
1028 return true;
1029 }
1030
1067 bool LambertAzimuthalEqualArea::xyRangeLambertAzimuthalPolar(double &minX,
1068 double &maxX,
1069 double &minY,
1070 double &maxY) {
1071 // Test the four combinations of min/max lat/lon
1072 XYRangeCheck(MinimumLatitude(), MinimumLongitude());
1073 XYRangeCheck(MinimumLatitude(), MaximumLongitude());
1074 XYRangeCheck(MaximumLatitude(), MinimumLongitude());
1075 XYRangeCheck(MaximumLatitude(), MaximumLongitude());
1076
1077 double centerLonDeg = m_lambda0 * 180.0 / PI;
1078 if (m_longitudeDirection == PositiveWest) centerLonDeg = centerLonDeg * -1.0;
1079
1080 // Since this projection is Polar aspect, 4 longitudes will lie
1081 // directly on the horizontal and vertical axes of the
1082 // projection.
1083 // test combinations with each of these longitudes:
1084 //
1085 // down (negative vertical axis) - center long for north polar
1086 // up (positive vertical axis) - center long for south polar
1087 // left (negative horizontal axis)
1088 // right (positive horizontal axis)
1089 //
1090 for (double lon = centerLonDeg; lon < centerLonDeg + 360; lon += 90) {
1091 checkLongitude(lon);
1092 }
1093
1094 // Make sure everything is ordered
1095 if (m_minimumX >= m_maximumX) return false;
1096 if (m_minimumY >= m_maximumY) return false;
1097
1098 // Return X/Y min/maxs
1099 minX = m_minimumX;
1100 maxX = m_maximumX;
1101 minY = m_minimumY;
1102 maxY = m_maximumY;
1103 return true;
1104 }
1105
1123 void LambertAzimuthalEqualArea::checkLongitude(double longitude) {
1124
1125 double centerLatDeg = m_phi1 * 180.0 / PI;
1126
1127 double innerLatitude, outerLatitude;
1128 if (m_northPolarAspect) {
1129 innerLatitude = MaximumLatitude();
1130 outerLatitude = MinimumLatitude();
1131 }
1132 else if (m_southPolarAspect) {
1133 innerLatitude = MinimumLatitude();
1134 outerLatitude = MaximumLatitude();
1135 }
1136 else {
1137 IString message = "checkLongitude() should only be called for a "
1138 "polar aspect projection. CenterLatitude is [";
1139 message = message + IString(centerLatDeg) + "] degrees.";
1140 throw IException(IException::Programmer, message, _FILEINFO_);
1141 }
1142
1143 // Check whether longitude is in the min/max longitude range
1144 bool thisLongitudeInMinMaxRange = false;
1145 if (MaximumLongitude() - MinimumLongitude() == 360) {
1146 thisLongitudeInMinMaxRange = true;
1147 }
1148 double adjustedLon = To360Domain(longitude);
1149 double adjustedMinLon = To360Domain(MinimumLongitude());
1150 double adjustedMaxLon = To360Domain(MaximumLongitude());
1151
1152 if (adjustedMinLon > adjustedMaxLon) {
1153 if (adjustedLon > adjustedMinLon) {
1154 adjustedLon -= 360;
1155 }
1156 adjustedMinLon -= 360;
1157 }
1158 // if lon value for this axis is between min lon and max lon
1159 if (adjustedMinLon <= adjustedLon
1160 && adjustedLon <= adjustedMaxLon) {
1161 thisLongitudeInMinMaxRange = true;
1162 }
1163 else {
1164 thisLongitudeInMinMaxRange = false;
1165 }
1166
1167 if (thisLongitudeInMinMaxRange) {
1168 // any shape that includes this longitude
1169 XYRangeCheck(outerLatitude, longitude);
1170 }
1171 else {
1172 // determine which boundary value (minlon or maxlon)
1173 // is closer to given longitude
1174 double distToMinLon = fabs(adjustedMinLon - adjustedLon);
1175 double distToMaxLon = fabs(adjustedMaxLon - adjustedLon);
1176
1177 if (distToMinLon >= 180 ) {
1178 distToMinLon = fabs(360 - distToMinLon);
1179 }
1180 if (distToMaxLon >= 180 ) {
1181 distToMaxLon = fabs(360 - distToMaxLon);
1182 }
1183
1184 double nearestBoundary = 0;
1185 if (distToMinLon < distToMaxLon) {
1186 nearestBoundary = MinimumLongitude();
1187 }
1188 else {
1189 nearestBoundary = MaximumLongitude();
1190 }
1191
1192 // shapes that come within 90 degrees of the given longitude
1193 if (distToMinLon <= 90 + DBL_EPSILON
1194 || distToMaxLon <= 90 + DBL_EPSILON) {
1195 XYRangeCheck(outerLatitude, nearestBoundary);
1196 }
1197 // shapes more than 90 degrees from the longitude that include the origin
1198 else if (qFuzzyCompare(MaximumLatitude(), centerLatDeg)) {
1199 XYRangeCheck(centerLatDeg,longitude);
1200 }
1201 // shapes more than 90 degrees from the longitude without the origin
1202 else {
1203 XYRangeCheck(innerLatitude, nearestBoundary);
1204 }
1205 }
1206 return;
1207 }
1208
1236 PvlGroup LambertAzimuthalEqualArea::Mapping() {
1237 PvlGroup mapping = TProjection::Mapping();
1238
1239 mapping += m_mappingGrp["CenterLatitude"];
1240 mapping += m_mappingGrp["CenterLongitude"];
1241
1242 return mapping;
1243 }
1244
1262 PvlGroup LambertAzimuthalEqualArea::MappingLatitudes() {
1263 PvlGroup mapping = TProjection::MappingLatitudes();
1264
1265 mapping += m_mappingGrp["CenterLatitude"];
1266
1267 return mapping;
1268 }
1269
1287 PvlGroup LambertAzimuthalEqualArea::MappingLongitudes() {
1288 PvlGroup mapping = TProjection::MappingLongitudes();
1289
1290 mapping += m_mappingGrp["CenterLongitude"];
1291
1292 return mapping;
1293 }
1294
1295 // There are no ellipsoidal values for the scale factors h and k unless
1296 // using the polar aspects, Snyder pg 26.
1297
1298 // These methods are not currently used in other Projection classes. However,
1299 // if similar methods are implemented for other projections, we should create
1300 // a virtual prototype in the parent Projection. Currently since they do not
1301 // exist in Projection, we can not call them if ProjectionFactory was used to
1302 // create the object.
1317 double LambertAzimuthalEqualArea::relativeScaleFactorLongitude() const {
1318 validateRelativeScaleFactor();
1319 return m_h;
1320 };
1321
1336 double LambertAzimuthalEqualArea::relativeScaleFactorLatitude() const {
1337 validateRelativeScaleFactor();
1338 return m_k;
1339 }
1340
1363 void LambertAzimuthalEqualArea::validateRelativeScaleFactor() const {
1364 if (!m_good) {
1365 IString message = "Projection failed or ground and coordinates have not "
1366 "been set. Relative scale factor can not be computed.";
1367 throw IException(IException::Unknown, message, _FILEINFO_);
1368 }
1369 if (!m_spherical && !(m_northPolarAspect || m_southPolarAspect)) {
1370 IString message = "For ellipsidal bodies, relative scale factor can only "
1371 "be computed for polar aspect projections.";
1372 throw IException(IException::Unknown, message, _FILEINFO_);
1373 }
1374 if (m_northPolarAspect && qFuzzyCompare(-90.0, m_latitude)) {
1375 IString message = "Relative scale factor can not be computed for north "
1376 "polar aspect projection when ground is set to "
1377 "latitude -90.";
1378 throw IException(IException::Unknown, message, _FILEINFO_);
1379 }
1380 if (m_southPolarAspect && qFuzzyCompare(90.0, m_latitude)) {
1381 IString message = "Relative scale factor can not be computed for south "
1382 "polar aspect projection when ground is set to "
1383 "latitude 90.";
1384 throw IException(IException::Unknown, message, _FILEINFO_);
1385 }
1386 if (m_k == Null || m_h == Null) {
1387 IString message = "Relative scale factor can not be computed.";
1388 throw IException(IException::Unknown, message, _FILEINFO_);
1389 }
1390 }
1391
1392} // end namespace isis
1393
1411extern "C" Isis::Projection *LambertAzimuthalEqualAreaPlugin(Isis::Pvl &lab,
1412 bool allowDefaults) {
1413 return new Isis::LambertAzimuthalEqualArea(lab, allowDefaults);
1414}
1415
Lambert Azimuthal Equal Area Map Projection.
Base class for Map Projections.
Definition Projection.h:155
Container for cube-like labels.
Definition Pvl.h:119
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 Null
Value for an Isis Null pixel.
const double HALFPI
The mathematical constant PI/2.
Definition Constants.h:41
const double PI
The mathematical constant PI.
Definition Constants.h:40
Namespace for the standard library.