File failed to load: https://isis.astrogeology.usgs.gov/6.0.0/Object/assets/jax/output/NativeMML/config.js
Isis 3 Programmer Reference
Spice.cpp
1 
6 /* SPDX-License-Identifier: CC0-1.0 */
7 #include "Spice.h"
8 
9 #include <cfloat>
10 #include <iomanip>
11 
12 #include <QDebug>
13 #include <QVector>
14 
15 #include <getSpkAbCorrState.hpp>
16 
17 #include <ale/Load.h>
18 
19 #include <nlohmann/json.hpp>
20 using json = nlohmann::json;
21 
22 
23 #include "Constants.h"
24 #include "Distance.h"
25 #include "EllipsoidShape.h"
26 #include "EndianSwapper.h"
27 #include "FileName.h"
28 #include "IException.h"
29 #include "IString.h"
30 #include "iTime.h"
31 #include "Longitude.h"
32 #include "LightTimeCorrectionState.h"
33 #include "NaifStatus.h"
34 #include "ShapeModel.h"
35 #include "SpacecraftPosition.h"
36 #include "Target.h"
37 #include "Blob.h"
38 
39 using namespace std;
40 
41 namespace Isis {
63  // TODO: DOCUMENT EVERYTHING
64  Spice::Spice(Cube &cube) {
65  Pvl &lab = *cube.label();
66  if (cube.hasBlob("CSMState", "String")) {
67  csmInit(cube, lab);
68  }
69  else {
70  PvlGroup kernels = lab.findGroup("Kernels", Pvl::Traverse);
71  bool hasTables = (kernels["TargetPosition"][0] == "Table");
72  // BONUS TODO: update to pull out separate init methods
73  init(lab, !hasTables);
74  }
75  }
76 
77 
84  Spice::Spice(Pvl &lab, json isd) {
85  init(lab, true, isd);
86  }
87 
88 
97  void Spice::csmInit(Cube &cube, Pvl label) {
98  defaultInit();
99  m_target = new Target;
100  NaifStatus::CheckErrors();
101  }
102 
103 
107  void Spice::defaultInit() {
108  m_solarLongitude = new Longitude;
109 
110  m_et = nullptr;
111  m_kernels = new QVector<QString>;
112 
113  m_startTime = new iTime;
114  m_endTime = new iTime;
115  m_cacheSize = new SpiceDouble;
116  *m_cacheSize = 0;
117 
118  m_startTimePadding = new SpiceDouble;
119  *m_startTimePadding = 0;
120  m_endTimePadding = new SpiceDouble;
121  *m_endTimePadding = 0;
122 
123  m_instrumentPosition = nullptr;
124  m_instrumentRotation = nullptr;
125 
126  m_sunPosition = nullptr;
127  m_bodyRotation = nullptr;
128 
129  m_allowDownsizing = false;
130 
131  m_spkCode = nullptr;
132  m_ckCode = nullptr;
133  m_ikCode = nullptr;
134  m_sclkCode = nullptr;
135  m_spkBodyCode = nullptr;
136  m_bodyFrameCode = nullptr;
137  m_target = nullptr;
138  }
139 
140 
154  void Spice::init(Pvl &lab, bool noTables, json isd) {
155  NaifStatus::CheckErrors();
156  // Initialize members
157  defaultInit();
158 
159  m_spkCode = new SpiceInt;
160  m_ckCode = new SpiceInt;
161  m_ikCode = new SpiceInt;
162  m_sclkCode = new SpiceInt;
163  m_spkBodyCode = new SpiceInt;
164  m_bodyFrameCode = new SpiceInt;
165 
166  m_naifKeywords = new PvlObject("NaifKeywords");
167  // m_sky = false;
168 
169  // Get the kernel group and load main kernels
170  PvlGroup kernels = lab.findGroup("Kernels", Pvl::Traverse);
171 
172  // Get the time padding first
173  if (kernels.hasKeyword("StartPadding")) {
174  *m_startTimePadding = toDouble(kernels["StartPadding"][0]);
175  }
176  else {
177  *m_startTimePadding = 0.0;
178  }
179 
180  if (kernels.hasKeyword("EndPadding")) {
181  *m_endTimePadding = toDouble(kernels["EndPadding"][0]);
182  }
183  else {
184  *m_endTimePadding = 0.0;
185  }
186 
187  // We should remove this completely in the near future
188  m_usingNaif = !lab.hasObject("NaifKeywords") || noTables;
189  m_usingAle = false;
190 
191 
192  // Modified to load planetary ephemeris SPKs before s/c SPKs since some
193  // missions (e.g., MESSENGER) may augment the s/c SPK with new planet
194  // ephemerides. (2008-02-27 (KJB))
195  if (m_usingNaif) {
196  try {
197  // At this time ALE does not compute pointing for the nadir option in spiceinit
198  // If NADIR is turned on fail here so ISIS can create nadir pointing
199  if (kernels["InstrumentPointing"][0].toUpper() == "NADIR") {
200  QString msg = "Falling back to ISIS generation of nadir pointing";
201  throw IException(IException::Programmer, msg, _FILEINFO_);
202  }
203 
204  if (isd == NULL){
205  // try using ALE
206  std::ostringstream kernel_pvl;
207  kernel_pvl << kernels;
208 
209  json props;
210  props["kernels"] = kernel_pvl.str();
211 
212  isd = ale::load(lab.fileName().toStdString(), props.dump(), "ale");
213  }
214 
215  json aleNaifKeywords = isd["naif_keywords"];
216  m_naifKeywords = new PvlObject("NaifKeywords", aleNaifKeywords);
217 
218  // Still need to load clock kernels for now
219  load(kernels["LeapSecond"], noTables);
220  if ( kernels.hasKeyword("SpacecraftClock")) {
221  load(kernels["SpacecraftClock"], noTables);
222  }
223  m_usingAle = true;
224  }
225  catch(...) {
226  // Backup to standard ISIS implementation
227  if (noTables) {
228  load(kernels["TargetPosition"], noTables);
229  load(kernels["InstrumentPosition"], noTables);
230  load(kernels["InstrumentPointing"], noTables);
231  }
232 
233  if (kernels.hasKeyword("Frame")) {
234  load(kernels["Frame"], noTables);
235  }
236 
237  load(kernels["TargetAttitudeShape"], noTables);
238  if (kernels.hasKeyword("Instrument")) {
239  load(kernels["Instrument"], noTables);
240  }
241  // Always load after instrument
242  if (kernels.hasKeyword("InstrumentAddendum")) {
243  load(kernels["InstrumentAddendum"], noTables);
244  }
245 
246  // Still need to load clock kernels for now
247  load(kernels["LeapSecond"], noTables);
248  if ( kernels.hasKeyword("SpacecraftClock")) {
249  load(kernels["SpacecraftClock"], noTables);
250  }
251 
252  // Modified to load extra kernels last to allow overriding default values
253  // (2010-04-07) (DAC)
254  if (kernels.hasKeyword("Extra")) {
255  load(kernels["Extra"], noTables);
256  }
257  }
258 
259  // Moved the construction of the Target after the NAIF kenels have been loaded or the
260  // NAIF keywords have been pulled from the cube labels, so we can find target body codes
261  // that are defined in kernels and not just body codes build into spicelib
262  // TODO: Move this below the else once the rings code below has been refactored
263  m_target = new Target(this, lab);
264 
265  // This should not be here. Consider having spiceinit add the necessary rings kernels to the
266  // Extra parameter if the user has set the shape model to RingPlane.
267  // If Target is Saturn and ShapeModel is RingPlane, load the extra rings pck file
268  // which changes the prime meridian values to report longitudes with respect to
269  // the ascending node of the ringplane.
270  if (m_target->name().toUpper() == "SATURN" && m_target->shape()->name().toUpper() == "PLANE") {
271  PvlKeyword ringPck = PvlKeyword("RingPCK","$cassini/kernels/pck/saturnRings_v001.tpc");
272  load(ringPck, noTables);
273  }
274  }
275  else {
276  *m_naifKeywords = lab.findObject("NaifKeywords");
277 
278  // Moved the construction of the Target after the NAIF kenels have been loaded or the
279  // NAIF keywords have been pulled from the cube labels, so we can find target body codes
280  // that are defined in kernels and not just body codes build into spicelib
281  // TODO: Move this below the else once the rings code above has been refactored
282 
283  m_target = new Target(this, lab);
284  }
285 
286  // Get NAIF ik, spk, sclk, and ck codes
287  //
288  // Use ikcode to get parameters from instrument kernel such as focal
289  // length, distortions, focal plane maps, etc
290  //
291  // Use spkcode to get spacecraft position from spk file
292  //
293  // Use sclkcode to transform times from et to tics
294  //
295  // Use ckcode to transform between frames
296  //
297  // Use bodycode to obtain radii and attitude (pole position/omega0)
298  //
299  // Use spkbodycode to read body position from spk
300 
301  QString trykey = "NaifIkCode";
302  if (kernels.hasKeyword("NaifFrameCode")) trykey = "NaifFrameCode";
303  *m_ikCode = toInt(kernels[trykey][0]);
304 
305  *m_spkCode = *m_ikCode / 1000;
306  *m_sclkCode = *m_spkCode;
307  *m_ckCode = *m_ikCode;
308 
309  if (!m_target->isSky()) {
310  // Get target body code and radii and store them in the Naif group
311  // DAC modified to look for and store body code so that the radii keyword name
312  // will be able to be constructed even for new bodies not in the standard PCK yet.
313  QString radiiKey = "BODY" + Isis::toString(m_target->naifBodyCode()) + "_RADII";
314  QVariant result = m_target->naifBodyCode();
315  storeValue("BODY_CODE", 0, SpiceIntType, result);
316  std::vector<Distance> radii(3,Distance());
317  radii[0] = Distance(getDouble(radiiKey, 0), Distance::Kilometers);
318  radii[1] = Distance(getDouble(radiiKey, 1), Distance::Kilometers);
319  radii[2] = Distance(getDouble(radiiKey, 2), Distance::Kilometers);
320  // m_target doesn't have the getDouble method so Spice gets the radii for it
321  m_target->setRadii(radii);
322  }
323 
324  *m_spkBodyCode = m_target->naifBodyCode();
325 
326  // Override them if they exist in the labels
327  if (kernels.hasKeyword("NaifSpkCode")) {
328  *m_spkCode = (int) kernels["NaifSpkCode"];
329  }
330 
331  if (kernels.hasKeyword("NaifCkCode")) {
332  *m_ckCode = (int) kernels["NaifCkCode"];
333  }
334 
335  if (kernels.hasKeyword("NaifSclkCode")) {
336  *m_sclkCode = (int) kernels["NaifSclkCode"];
337  }
338 
339  if (!m_target->isSky()) {
340  if (kernels.hasKeyword("NaifSpkBodyCode")) {
341  *m_spkBodyCode = (int) kernels["NaifSpkBodyCode"];
342  }
343  }
344 
345  if (m_target->isSky()) {
346  // Create the identity rotation for sky targets
347  // Everything in bodyfixed will really be J2000
348  m_bodyRotation = new SpiceRotation(1);
349  }
350  else {
351  // JAA - Modified to store and look for the frame body code in the cube labels
352  SpiceInt frameCode;
353  if ((m_usingNaif) || (!m_naifKeywords->hasKeyword("BODY_FRAME_CODE"))) {
354  char frameName[32];
355  SpiceBoolean found;
356  cidfrm_c(*m_spkBodyCode, sizeof(frameName), &frameCode, frameName, &found);
357 
358  if (!found) {
359  QString naifTarget = "IAU_" + m_target->name().toUpper();
360  namfrm_c(naifTarget.toLatin1().data(), &frameCode);
361  if (frameCode == 0) {
362  QString msg = "Can not find NAIF BODY_FRAME_CODE for target ["
363  + m_target->name() + "]";
364  throw IException(IException::Io, msg, _FILEINFO_);
365  }
366  }
367 
368  QVariant result = (int)frameCode;
369  storeValue("BODY_FRAME_CODE", 0, SpiceIntType, result);
370  }
371  else {
372  frameCode = getInteger("BODY_FRAME_CODE", 0);
373  }
374 
375  m_bodyRotation = new SpiceRotation(frameCode);
376  *m_bodyFrameCode = frameCode;
377  }
378 
379  m_instrumentRotation = new SpiceRotation(*m_ckCode);
380 
381  // Set up for observer/target and light time correction to between s/c
382  // and target body.
383  LightTimeCorrectionState ltState(*m_ikCode, this);
385 
386  vector<Distance> radius = m_target->radii();
387  Distance targetRadius((radius[0] + radius[2])/2.0);
388  m_instrumentPosition = new SpacecraftPosition(*m_spkCode, *m_spkBodyCode,
389  ltState, targetRadius);
390 
391  m_sunPosition = new SpicePosition(10, m_target->naifBodyCode());
392 
393 
394  // Check to see if we have nadir pointing that needs to be computed &
395  // See if we have table blobs to load
396  if (m_usingAle) {
397  m_sunPosition->LoadCache(isd["sun_position"]);
398  if (m_sunPosition->cacheSize() > 3) {
399  m_sunPosition->Memcache2HermiteCache(0.01);
400  }
401  m_bodyRotation->LoadCache(isd["body_rotation"]);
402  m_bodyRotation->MinimizeCache(SpiceRotation::DownsizeStatus::Yes);
403  if (m_bodyRotation->cacheSize() > 5) {
404  m_bodyRotation->LoadTimeCache();
405  }
406  solarLongitude();
407  }
408  else if (kernels["TargetPosition"][0].toUpper() == "TABLE") {
409  Table t("SunPosition", lab.fileName(), lab);
410  m_sunPosition->LoadCache(t);
411 
412  Table t2("BodyRotation", lab.fileName(), lab);
413  m_bodyRotation->LoadCache(t2);
414  if (t2.Label().hasKeyword("SolarLongitude")) {
415  *m_solarLongitude = Longitude(t2.Label()["SolarLongitude"],
416  Angle::Degrees);
417  }
418  else {
419  solarLongitude();
420  }
421  }
422 
423  // We can't assume InstrumentPointing & InstrumentPosition exist, old
424  // files may be around with the old keywords, SpacecraftPointing &
425  // SpacecraftPosition. The old keywords were in existance before the
426  // Table option, so we don't need to check for Table under the old
427  // keywords.
428 
429  if (kernels["InstrumentPointing"].size() == 0) {
430  throw IException(IException::Unknown,
431  "No camera pointing available",
432  _FILEINFO_);
433  }
434 
435  // 2009-03-18 Tracie Sucharski - Removed test for old keywords, any files
436  // with the old keywords should be re-run through spiceinit.
437  if (kernels["InstrumentPointing"][0].toUpper() == "NADIR") {
438  if (m_instrumentRotation) {
439  delete m_instrumentRotation;
440  m_instrumentRotation = NULL;
441  }
442 
443  m_instrumentRotation = new SpiceRotation(*m_ikCode, *m_spkBodyCode);
444  }
445  else if (m_usingAle) {
446  m_instrumentRotation->LoadCache(isd["instrument_pointing"]);
447  m_instrumentRotation->MinimizeCache(SpiceRotation::DownsizeStatus::Yes);
448  if (m_instrumentRotation->cacheSize() > 5) {
449  m_instrumentRotation->LoadTimeCache();
450  }
451  }
452  else if (kernels["InstrumentPointing"][0].toUpper() == "TABLE") {
453  Table t("InstrumentPointing", lab.fileName(), lab);
454  m_instrumentRotation->LoadCache(t);
455  }
456 
457 
458  if (kernels["InstrumentPosition"].size() == 0) {
459  throw IException(IException::Unknown,
460  "No instrument position available",
461  _FILEINFO_);
462  }
463 
464  if (m_usingAle) {
465  m_instrumentPosition->LoadCache(isd["instrument_position"]);
466  if (m_instrumentPosition->cacheSize() > 3) {
467  m_instrumentPosition->Memcache2HermiteCache(0.01);
468  }
469  }
470  else if (kernels["InstrumentPosition"][0].toUpper() == "TABLE") {
471  Table t("InstrumentPosition", lab.fileName(), lab);
472  m_instrumentPosition->LoadCache(t);
473  }
474  NaifStatus::CheckErrors();
475  }
476 
477 
486  void Spice::load(PvlKeyword &key, bool noTables) {
487  NaifStatus::CheckErrors();
488 
489  for (int i = 0; i < key.size(); i++) {
490  if (key[i] == "") continue;
491  if (key[i].toUpper() == "NULL") break;
492  if (key[i].toUpper() == "NADIR") break;
493  if (key[i].toUpper() == "TABLE" && !noTables) break;
494  if (key[i].toUpper() == "TABLE" && noTables) continue;
495  FileName file(key[i]);
496  if (!file.fileExists()) {
497  QString msg = "Spice file does not exist [" + file.expanded() + "]";
498  throw IException(IException::Io, msg, _FILEINFO_);
499  }
500  QString fileName = file.expanded();
501  furnsh_c(fileName.toLatin1().data());
502  m_kernels->push_back(key[i]);
503  }
504 
505  NaifStatus::CheckErrors();
506  }
507 
511  Spice::~Spice() {
512  NaifStatus::CheckErrors();
513 
514  if (m_solarLongitude != NULL) {
515  delete m_solarLongitude;
516  m_solarLongitude = NULL;
517  }
518 
519  if (m_et != NULL) {
520  delete m_et;
521  m_et = NULL;
522  }
523 
524  if (m_startTime != NULL) {
525  delete m_startTime;
526  m_startTime = NULL;
527  }
528 
529  if (m_endTime != NULL) {
530  delete m_endTime;
531  m_endTime = NULL;
532  }
533 
534  if (m_cacheSize != NULL) {
535  delete m_cacheSize;
536  m_cacheSize = NULL;
537  }
538 
539  if (m_startTimePadding != NULL) {
540  delete m_startTimePadding;
541  m_startTimePadding = NULL;
542  }
543 
544  if (m_endTimePadding != NULL) {
545  delete m_endTimePadding;
546  m_endTimePadding = NULL;
547  }
548 
549  if (m_instrumentPosition != NULL) {
550  delete m_instrumentPosition;
551  m_instrumentPosition = NULL;
552  }
553 
554  if (m_instrumentRotation != NULL) {
555  delete m_instrumentRotation;
556  m_instrumentRotation = NULL;
557  }
558 
559  if (m_sunPosition != NULL) {
560  delete m_sunPosition;
561  m_sunPosition = NULL;
562  }
563 
564  if (m_bodyRotation != NULL) {
565  delete m_bodyRotation;
566  m_bodyRotation = NULL;
567  }
568 
569  if (m_spkCode != NULL) {
570  delete m_spkCode;
571  m_spkCode = NULL;
572  }
573 
574  if (m_ckCode != NULL) {
575  delete m_ckCode;
576  m_ckCode = NULL;
577  }
578 
579  if (m_ikCode != NULL) {
580  delete m_ikCode;
581  m_ikCode = NULL;
582  }
583 
584  if (m_sclkCode != NULL) {
585  delete m_sclkCode;
586  m_sclkCode = NULL;
587  }
588 
589  if (m_spkBodyCode != NULL) {
590  delete m_spkBodyCode;
591  m_spkBodyCode = NULL;
592  }
593 
594  if (m_bodyFrameCode != NULL) {
595  delete m_bodyFrameCode;
596  m_bodyFrameCode = NULL;
597  }
598 
599  if (m_target != NULL) {
600  delete m_target;
601  m_target = NULL;
602  }
603 
604  // Unload the kernels (TODO: Can this be done faster)
605  for (int i = 0; m_kernels && i < m_kernels->size(); i++) {
606  FileName file(m_kernels->at(i));
607  QString fileName = file.expanded();
608  unload_c(fileName.toLatin1().data());
609  }
610 
611  if (m_kernels != NULL) {
612  delete m_kernels;
613  m_kernels = NULL;
614  }
615  NaifStatus::CheckErrors();
616  }
617 
649  void Spice::createCache(iTime startTime, iTime endTime,
650  int cacheSize, double tol) {
651  NaifStatus::CheckErrors();
652 
653  // Check for errors
654  if (cacheSize <= 0) {
655  QString msg = "Argument cacheSize must be greater than zero";
656  throw IException(IException::Programmer, msg, _FILEINFO_);
657  }
658 
659  if (startTime > endTime) {
660  QString msg = "Argument startTime must be less than or equal to endTime";
661  throw IException(IException::Programmer, msg, _FILEINFO_);
662  }
663 
664  if (*m_cacheSize > 0) {
665  QString msg = "A cache has already been created";
666  throw IException(IException::Programmer, msg, _FILEINFO_);
667  }
668 
669  if (cacheSize == 1 && (*m_startTimePadding != 0 || *m_endTimePadding != 0)) {
670  QString msg = "This instrument does not support time padding";
671  throw IException(IException::User, msg, _FILEINFO_);
672  }
673 
674  string abcorr;
675  if (getSpkAbCorrState(abcorr)) {
676  instrumentPosition()->SetAberrationCorrection("NONE");
677  }
678 
679  iTime avgTime((startTime.Et() + endTime.Et()) / 2.0);
680  computeSolarLongitude(avgTime);
681 
682  // Cache everything
683  if (!m_bodyRotation->IsCached()) {
684  int bodyRotationCacheSize = cacheSize;
685  if (cacheSize > 2) bodyRotationCacheSize = 2;
686  m_bodyRotation->LoadCache(
687  startTime.Et() - *m_startTimePadding,
688  endTime.Et() + *m_endTimePadding,
689  bodyRotationCacheSize);
690  }
691 
692  if (m_instrumentRotation->GetSource() < SpiceRotation::Memcache) {
693  if (cacheSize > 3) m_instrumentRotation->MinimizeCache(SpiceRotation::Yes);
694  m_instrumentRotation->LoadCache(
695  startTime.Et() - *m_startTimePadding,
696  endTime.Et() + *m_endTimePadding,
697  cacheSize);
698  }
699 
700  if (m_instrumentPosition->GetSource() < SpicePosition::Memcache) {
701  m_instrumentPosition->LoadCache(
702  startTime.Et() - *m_startTimePadding,
703  endTime.Et() + *m_endTimePadding,
704  cacheSize);
705  if (cacheSize > 3) m_instrumentPosition->Memcache2HermiteCache(tol);
706  }
707 
708  if (!m_sunPosition->IsCached()) {
709  int sunPositionCacheSize = cacheSize;
710  if (cacheSize > 2) sunPositionCacheSize = 2;
711  m_sunPosition->LoadCache(
712  startTime.Et() - *m_startTimePadding,
713  endTime.Et() + *m_endTimePadding,
714  sunPositionCacheSize);
715  }
716 
717  // Save the time and cache size
718  *m_startTime = startTime;
719  *m_endTime = endTime;
720  *m_cacheSize = cacheSize;
721  m_et = NULL;
722 
723  // Unload the kernels (TODO: Can this be done faster)
724  for (int i = 0; i < m_kernels->size(); i++) {
725  FileName file(m_kernels->at(i));
726  QString fileName = file.expanded();
727  unload_c(fileName.toLatin1().data());
728  }
729 
730  m_kernels->clear();
731 
732  NaifStatus::CheckErrors();
733  }
734 
735 
743  iTime Spice::cacheStartTime() const {
744  if (m_startTime) {
745  return *m_startTime;
746  }
747 
748  return iTime();
749  }
750 
758  iTime Spice::cacheEndTime() const {
759  if (m_endTime) {
760  return *m_endTime;
761  }
762 
763  return iTime();
764  }
765 
780  void Spice::setTime(const iTime &et) {
781 
782  if (m_et == NULL) {
783  m_et = new iTime();
784 
785  // Before the Spice is cached, but after the camera aberration correction
786  // is set, check to see if the instrument position kernel was created
787  // by spkwriter. If so turn off aberration corrections because the camera
788  // set aberration corrections are included in the spk already.
789  string abcorr;
790  if (*m_cacheSize == 0) {
791  if (m_startTime->Et() == 0.0 && m_endTime->Et() == 0.0
792  && getSpkAbCorrState(abcorr)) {
793  instrumentPosition()->SetAberrationCorrection("NONE");
794  }
795  }
796  }
797 
798  *m_et = et;
799 
800  m_bodyRotation->SetEphemerisTime(et.Et());
801  m_instrumentRotation->SetEphemerisTime(et.Et());
802  m_instrumentPosition->SetEphemerisTime(et.Et());
803  m_sunPosition->SetEphemerisTime(et.Et());
804 
805  std::vector<double> uB = m_bodyRotation->ReferenceVector(m_sunPosition->Coordinate());
806  m_uB[0] = uB[0];
807  m_uB[1] = uB[1];
808  m_uB[2] = uB[2];
809 
810  computeSolarLongitude(*m_et);
811  }
812 
822  void Spice::instrumentPosition(double p[3]) const {
823  instrumentBodyFixedPosition(p);
824  }
825 
835  void Spice::instrumentBodyFixedPosition(double p[3]) const {
836  if (m_et == NULL) {
837  QString msg = "Unable to retrieve instrument's body fixed position."
838  " Spice::SetTime must be called first.";
839  throw IException(IException::Programmer, msg, _FILEINFO_);
840  }
841 
842  std::vector<double> sB = m_bodyRotation->ReferenceVector(m_instrumentPosition->Coordinate());
843  p[0] = sB[0];
844  p[1] = sB[1];
845  p[2] = sB[2];
846  }
847 
853  void Spice::instrumentBodyFixedVelocity(double v[3]) const {
854  if (m_et == NULL) {
855  QString msg = "Unable to retrieve instrument's body fixed velocity."
856  " Spice::SetTime must be called first.";
857  throw IException(IException::Programmer, msg, _FILEINFO_);
858  }
859 
860  std::vector<double> state;
861  state.push_back(m_instrumentPosition->Coordinate()[0]);
862  state.push_back(m_instrumentPosition->Coordinate()[1]);
863  state.push_back(m_instrumentPosition->Coordinate()[2]);
864  state.push_back(m_instrumentPosition->Velocity()[0]);
865  state.push_back(m_instrumentPosition->Velocity()[1]);
866  state.push_back(m_instrumentPosition->Velocity()[2]);
867 
868  std::vector<double> vB = m_bodyRotation->ReferenceVector(state);
869  v[0] = vB[3];
870  v[1] = vB[4];
871  v[2] = vB[5];
872  }
873 
874 
884  iTime Spice::time() const {
885  if (m_et == NULL) {
886  QString msg = "Unable to retrieve the time."
887  " Spice::SetTime must be called first.";
888  throw IException(IException::Programmer, msg, _FILEINFO_);
889  }
890  return *m_et;
891  }
892 
893 
902  void Spice::sunPosition(double p[3]) const {
903  if (m_et == NULL) {
904  QString msg = "Unable to retrieve sun's position."
905  " Spice::SetTime must be called first.";
906  throw IException(IException::Programmer, msg, _FILEINFO_);
907  }
908  p[0] = m_uB[0];
909  p[1] = m_uB[1];
910  p[2] = m_uB[2];
911  }
912 
918  double Spice::targetCenterDistance() const {
919  std::vector<double> sB = m_bodyRotation->ReferenceVector(m_instrumentPosition->Coordinate());
920  return sqrt(pow(sB[0], 2) + pow(sB[1], 2) + pow(sB[2], 2));
921  }
922 
930  void Spice::radii(Distance r[3]) const {
931  for (int i = 0; i < 3; i++)
932  r[i] =m_target->radii()[i];
933  }
934 
941  SpiceInt Spice::naifBodyCode() const {
942  return (int) m_target->naifBodyCode();
943  }
944 
950  SpiceInt Spice::naifSpkCode() const {
951  return *m_spkCode;
952  }
953 
959  SpiceInt Spice::naifCkCode() const {
960  return *m_ckCode;
961  }
962 
968  SpiceInt Spice::naifIkCode() const {
969  return *m_ikCode;
970  }
971 
978  SpiceInt Spice::naifSclkCode() const {
979  return *m_sclkCode;
980  }
981 
989  SpiceInt Spice::naifBodyFrameCode() const {
990  return *m_bodyFrameCode;
991  }
992 
993 
998  PvlObject Spice::getStoredNaifKeywords() const {
999  return *m_naifKeywords;
1000  }
1001 
1002 
1009  double Spice::resolution() {
1010  return 1.0;
1011  };
1012 
1013 
1025  SpiceInt Spice::getInteger(const QString &key, int index) {
1026  return readValue(key, SpiceIntType, index).toInt();
1027  }
1028 
1039  SpiceDouble Spice::getDouble(const QString &key, int index) {
1040  return readValue(key, SpiceDoubleType, index).toDouble();
1041  }
1042 
1043 
1053  iTime Spice::getClockTime(QString clockValue, int sclkCode, bool clockTicks) {
1054  if (sclkCode == -1) {
1055  sclkCode = naifSclkCode();
1056  }
1057 
1058  iTime result;
1059 
1060  QString key = "CLOCK_ET_" + Isis::toString(sclkCode) + "_" + clockValue;
1061  QVariant storedClockTime = getStoredResult(key, SpiceDoubleType);
1062 
1063  if (storedClockTime.isNull()) {
1064  SpiceDouble timeOutput;
1065  NaifStatus::CheckErrors();
1066  if (clockTicks) {
1067  sct2e_c(sclkCode, (SpiceDouble) clockValue.toDouble(), &timeOutput);
1068  }
1069  else {
1070  scs2e_c(sclkCode, clockValue.toLatin1().data(), &timeOutput);
1071  }
1072  NaifStatus::CheckErrors();
1073  storedClockTime = timeOutput;
1074  storeResult(key, SpiceDoubleType, timeOutput);
1075  }
1076 
1077  result = storedClockTime.toDouble();
1078 
1079  return result;
1080  }
1081 
1082 
1093  QVariant Spice::readValue(QString key, SpiceValueType type, int index) {
1094  QVariant result;
1095 
1096  if (m_usingNaif && !m_usingAle) {
1097  NaifStatus::CheckErrors();
1098 
1099  // This is the success status of the naif call
1100  SpiceBoolean found = false;
1101 
1102  // Naif tells us how many values were read, but we always just read one.
1103  // Use this variable to make naif happy.
1104  SpiceInt numValuesRead;
1105 
1106  if (type == SpiceDoubleType) {
1107  SpiceDouble kernelValue;
1108  gdpool_c(key.toLatin1().data(), (SpiceInt)index, 1,
1109  &numValuesRead, &kernelValue, &found);
1110 
1111  if (found)
1112  result = kernelValue;
1113  }
1114  else if (type == SpiceStringType) {
1115  char kernelValue[512];
1116  gcpool_c(key.toLatin1().data(), (SpiceInt)index, 1, sizeof(kernelValue),
1117  &numValuesRead, kernelValue, &found);
1118 
1119  if (found)
1120  result = kernelValue;
1121  }
1122  else if (type == SpiceIntType) {
1123  SpiceInt kernelValue;
1124  gipool_c(key.toLatin1().data(), (SpiceInt)index, 1, &numValuesRead,
1125  &kernelValue, &found);
1126 
1127  if (found)
1128  result = (int)kernelValue;
1129  }
1130 
1131  if (!found) {
1132  QString msg = "Can not find [" + key + "] in text kernels";
1133  throw IException(IException::Io, msg, _FILEINFO_);
1134  }
1135 
1136  storeValue(key, index, type, result);
1137  NaifStatus::CheckErrors();
1138  }
1139  else {
1140  // Read from PvlObject that is our naif keywords
1141  result = readStoredValue(key, type, index);
1142 
1143  if (result.isNull()) {
1144  QString msg = "The camera is requesting spice data [" + key + "] that "
1145  "was not attached, please re-run spiceinit";
1146  throw IException(IException::Unknown, msg, _FILEINFO_);
1147  }
1148  }
1149 
1150  return result;
1151  }
1152 
1153 
1154  void Spice::storeResult(QString name, SpiceValueType type, QVariant value) {
1155  if (type == SpiceDoubleType) {
1156  EndianSwapper swapper("LSB");
1157 
1158  double doubleVal = value.toDouble();
1159  doubleVal = swapper.Double(&doubleVal);
1160  QByteArray byteCode((char *) &doubleVal, sizeof(double));
1161  value = byteCode;
1162  type = SpiceByteCodeType;
1163  }
1164 
1165  storeValue(name + "_COMPUTED", 0, type, value);
1166  }
1167 
1168 
1169  QVariant Spice::getStoredResult(QString name, SpiceValueType type) {
1170  bool wasDouble = false;
1171 
1172  if (type == SpiceDoubleType) {
1173  wasDouble = true;
1174  type = SpiceByteCodeType;
1175  }
1176 
1177  QVariant stored = readStoredValue(name + "_COMPUTED", type, 0);
1178 
1179  if (wasDouble && !stored.isNull()) {
1180  EndianSwapper swapper("LSB");
1181  double doubleVal = swapper.Double((void *)QByteArray::fromHex(
1182  stored.toByteArray()).data());
1183  stored = doubleVal;
1184  }
1185 
1186  return stored;
1187  }
1188 
1189 
1190  void Spice::storeValue(QString key, int index, SpiceValueType type,
1191  QVariant value) {
1192  if (!m_naifKeywords->hasKeyword(key)) {
1193  m_naifKeywords->addKeyword(PvlKeyword(key));
1194  }
1195 
1196  PvlKeyword &storedKey = m_naifKeywords->findKeyword(key);
1197 
1198  while(index >= storedKey.size()) {
1199  storedKey.addValue("");
1200  }
1201 
1202  if (type == SpiceByteCodeType) {
1203  storedKey[index] = QString(value.toByteArray().toHex().data());
1204  }
1205  else if (type == SpiceStringType) {
1206  storedKey[index] = value.toString();
1207  }
1208  else if (type == SpiceDoubleType) {
1209  storedKey[index] = toString(value.toDouble());
1210  }
1211  else if (type == SpiceIntType) {
1212  storedKey[index] = toString(value.toInt());
1213  }
1214  else {
1215  QString msg = "Unable to store variant in labels for key [" + key + "]";
1216  throw IException(IException::Unknown, msg, _FILEINFO_);
1217  }
1218  }
1219 
1220 
1221  QVariant Spice::readStoredValue(QString key, SpiceValueType type,
1222  int index) {
1223  // Read from PvlObject that is our naif keywords
1224  QVariant result;
1225 
1226  if (m_naifKeywords->hasKeyword(key) && (!m_usingNaif || m_usingAle)) {
1227  PvlKeyword &storedKeyword = m_naifKeywords->findKeyword(key);
1228 
1229  try {
1230  if (type == SpiceDoubleType) {
1231  result = toDouble(storedKeyword[index]);
1232  }
1233  else if (type == SpiceStringType) {
1234  result = storedKeyword[index];
1235  }
1236  else if (type == SpiceByteCodeType || SpiceStringType) {
1237  result = storedKeyword[index].toLatin1();
1238  }
1239  else if (type == SpiceIntType) {
1240  result = toInt(storedKeyword[index]);
1241  }
1242  }
1243  catch(IException &) {
1244  }
1245  }
1246 
1247  return result;
1248  }
1249 
1261  QString Spice::getString(const QString &key, int index) {
1262  return readValue(key, SpiceStringType, index).toString();
1263  }
1264 
1265 
1278  void Spice::subSpacecraftPoint(double &lat, double &lon) {
1279  NaifStatus::CheckErrors();
1280 
1281  if (m_et == NULL) {
1282  QString msg = "Unable to retrieve subspacecraft position."
1283  " Spice::SetTime must be called first.";
1284  throw IException(IException::Programmer, msg, _FILEINFO_);
1285  }
1286 
1287  SpiceDouble usB[3], dist;
1288  std::vector<double> vsB = m_bodyRotation->ReferenceVector(m_instrumentPosition->Coordinate());
1289  SpiceDouble sB[3];
1290  sB[0] = vsB[0];
1291  sB[1] = vsB[1];
1292  sB[2] = vsB[2];
1293  unorm_c(sB, usB, &dist);
1294 
1295  std::vector<Distance> radii = target()->radii();
1296  SpiceDouble a = radii[0].kilometers();
1297  SpiceDouble b = radii[1].kilometers();
1298  SpiceDouble c = radii[2].kilometers();
1299 
1300  SpiceDouble originB[3];
1301  originB[0] = originB[1] = originB[2] = 0.0;
1302 
1303  SpiceBoolean found;
1304  SpiceDouble subB[3];
1305 
1306  surfpt_c(originB, usB, a, b, c, subB, &found);
1307 
1308  SpiceDouble mylon, mylat;
1309  reclat_c(subB, &a, &mylon, &mylat);
1310  lat = mylat * 180.0 / PI;
1311  lon = mylon * 180.0 / PI;
1312  if (lon < 0.0) lon += 360.0;
1313 
1314  NaifStatus::CheckErrors();
1315  }
1316 
1317 
1329  void Spice::subSolarPoint(double &lat, double &lon) {
1330  NaifStatus::CheckErrors();
1331 
1332  if (m_et == NULL) {
1333  QString msg = "Unable to retrieve subsolar point."
1334  " Spice::SetTime must be called first.";
1335  throw IException(IException::Programmer, msg, _FILEINFO_);
1336  }
1337 
1338  SpiceDouble uuB[3], dist;
1339  unorm_c(m_uB, uuB, &dist);
1340  std::vector<Distance> radii = target()->radii();
1341 
1342  SpiceDouble a = radii[0].kilometers();
1343  SpiceDouble b = radii[1].kilometers();
1344  SpiceDouble c = radii[2].kilometers();
1345 
1346  SpiceDouble originB[3];
1347  originB[0] = originB[1] = originB[2] = 0.0;
1348 
1349  SpiceBoolean found;
1350  SpiceDouble subB[3];
1351  surfpt_c(originB, uuB, a, b, c, subB, &found);
1352 
1353  SpiceDouble mylon, mylat;
1354  reclat_c(subB, &a, &mylon, &mylat);
1355 
1356  lat = mylat * 180.0 / PI;
1357  lon = mylon * 180.0 / PI;
1358  if (lon < 0.0) lon += 360.0;
1359  NaifStatus::CheckErrors();
1360  }
1361 
1362 
1368  Target *Spice::target() const {
1369  return m_target;
1370  }
1371 
1372 
1378  QString Spice::targetName() const {
1379  return m_target->name();
1380  }
1381 
1382 
1383  double Spice::sunToBodyDist() const {
1384  std::vector<double> sunPosition = m_sunPosition->Coordinate();
1385  std::vector<double> bodyRotation = m_bodyRotation->Matrix();
1386 
1387  double sunPosFromTarget[3];
1388  mxv_c(&bodyRotation[0], &sunPosition[0], sunPosFromTarget);
1389 
1390  return vnorm_c(sunPosFromTarget);
1391  }
1392 
1393 
1400  void Spice::computeSolarLongitude(iTime et) {
1401  NaifStatus::CheckErrors();
1402 
1403  if (m_target->isSky()) {
1404  *m_solarLongitude = Longitude();
1405  return;
1406  }
1407 
1408  if (m_usingAle || !m_usingNaif) {
1409  double og_time = m_bodyRotation->EphemerisTime();
1410  m_bodyRotation->SetEphemerisTime(et.Et());
1411  m_sunPosition->SetEphemerisTime(et.Et());
1412 
1413  std::vector<double> bodyRotMat = m_bodyRotation->Matrix();
1414  std::vector<double> sunPos = m_sunPosition->Coordinate();
1415  std::vector<double> sunVel = m_sunPosition->Velocity();
1416  double sunAv[3];
1417 
1418  ucrss_c(&sunPos[0], &sunVel[0], sunAv);
1419 
1420  double npole[3];
1421  for (int i = 0; i < 3; i++) {
1422  npole[i] = bodyRotMat[6+i];
1423  }
1424 
1425  double x[3], y[3], z[3];
1426  vequ_c(sunAv, z);
1427  ucrss_c(npole, z, x);
1428  ucrss_c(z, x, y);
1429 
1430  double trans[3][3];
1431  for (int i = 0; i < 3; i++) {
1432  trans[0][i] = x[i];
1433  trans[1][i] = y[i];
1434  trans[2][i] = z[i];
1435  }
1436 
1437  double pos[3];
1438  mxv_c(trans, &sunPos[0], pos);
1439 
1440  double radius, ls, lat;
1441  reclat_c(pos, &radius, &ls, &lat);
1442 
1443  *m_solarLongitude = Longitude(ls, Angle::Radians).force360Domain();
1444 
1445  NaifStatus::CheckErrors();
1446  m_bodyRotation->SetEphemerisTime(og_time);
1447  m_sunPosition->SetEphemerisTime(og_time);
1448  return;
1449  }
1450 
1451  if (m_bodyRotation->IsCached()) return;
1452 
1453  double tipm[3][3], npole[3];
1454  char frameName[32];
1455  SpiceInt frameCode;
1456  SpiceBoolean found;
1457 
1458  cidfrm_c(*m_spkBodyCode, sizeof(frameName), &frameCode, frameName, &found);
1459 
1460  if (found) {
1461  pxform_c("J2000", frameName, et.Et(), tipm);
1462  }
1463  else {
1464  tipbod_c("J2000", *m_spkBodyCode, et.Et(), tipm);
1465  }
1466 
1467  for (int i = 0; i < 3; i++) {
1468  npole[i] = tipm[2][i];
1469  }
1470 
1471  double state[6], lt;
1472  spkez_c(*m_spkBodyCode, et.Et(), "J2000", "NONE", 10, state, &lt);
1473 
1474  double uavel[3];
1475  ucrss_c(state, &state[3], uavel);
1476 
1477  double x[3], y[3], z[3];
1478  vequ_c(uavel, z);
1479  ucrss_c(npole, z, x);
1480  ucrss_c(z, x, y);
1481 
1482  double trans[3][3];
1483  for (int i = 0; i < 3; i++) {
1484  trans[0][i] = x[i];
1485  trans[1][i] = y[i];
1486  trans[2][i] = z[i];
1487  }
1488 
1489  spkez_c(10, et.Et(), "J2000", "LT+S", *m_spkBodyCode, state, &lt);
1490 
1491  double pos[3];
1492  mxv_c(trans, state, pos);
1493 
1494  double radius, ls, lat;
1495  reclat_c(pos, &radius, &ls, &lat);
1496 
1497  *m_solarLongitude = Longitude(ls, Angle::Radians).force360Domain();
1498 
1499  NaifStatus::CheckErrors();
1500 
1501  }
1502 
1503 
1509  Longitude Spice::solarLongitude() {
1510  if (m_et) {
1511  computeSolarLongitude(*m_et);
1512  return *m_solarLongitude;
1513  }
1514 
1515  return Longitude();
1516  }
1517 
1518 
1526  bool Spice::hasKernels(Pvl &lab) {
1527 
1528  // Get the kernel group and check main kernels
1529  PvlGroup kernels = lab.findGroup("Kernels", Pvl::Traverse);
1530  std::vector<string> keywords;
1531  keywords.push_back("TargetPosition");
1532 
1533  if (kernels.hasKeyword("SpacecraftPosition")) {
1534  keywords.push_back("SpacecraftPosition");
1535  }
1536  else {
1537  keywords.push_back("InstrumentPosition");
1538  }
1539 
1540  if (kernels.hasKeyword("SpacecraftPointing")) {
1541  keywords.push_back("SpacecraftPointing");
1542  }
1543  else {
1544  keywords.push_back("InstrumentPointing");
1545  }
1546 
1547  if (kernels.hasKeyword("Frame")) {
1548  keywords.push_back("Frame");
1549  }
1550 
1551  if (kernels.hasKeyword("Extra")) {
1552  keywords.push_back("Extra");
1553  }
1554 
1555  PvlKeyword key;
1556  for (int ikey = 0; ikey < (int) keywords.size(); ikey++) {
1557  key = kernels[ikey];
1558 
1559  for (int i = 0; i < key.size(); i++) {
1560  if (key[i] == "") return false;
1561  if (key[i].toUpper() == "NULL") return false;
1562  if (key[i].toUpper() == "NADIR") return false;
1563  if (key[i].toUpper() == "TABLE") return false;
1564  }
1565  }
1566  return true;
1567  }
1568 
1569 
1577  bool Spice::isTimeSet(){
1578  return !(m_et == NULL);
1579  }
1580 
1581 
1589  SpicePosition *Spice::sunPosition() const {
1590  return m_sunPosition;
1591  }
1592 
1600  SpicePosition *Spice::instrumentPosition() const {
1601  return m_instrumentPosition;
1602  }
1603 
1611  SpiceRotation *Spice::bodyRotation() const {
1612  return m_bodyRotation;
1613  }
1614 
1622  SpiceRotation *Spice::instrumentRotation() const {
1623  return m_instrumentRotation;
1624  }
1625 
1626  bool Spice::isUsingAle(){
1627  return m_usingAle;
1628  }
1629 }
Isis::SpicePosition
Obtain SPICE position information for a body.
Definition: SpicePosition.h:173
Isis::PvlObject::findGroup
PvlGroupIterator findGroup(const QString &name, PvlGroupIterator beg, PvlGroupIterator end)
Find a group with the specified name, within these indexes.
Definition: PvlObject.h:129
Isis::PvlObject
Contains Pvl Groups and Pvl Objects.
Definition: PvlObject.h:61
Isis::PI
const double PI
The mathematical constant PI.
Definition: Constants.h:40
Isis::PvlKeyword
A single keyword-value pair.
Definition: PvlKeyword.h:82
Isis::Table::Label
PvlObject & Label()
The Table's label.
Definition: Table.cpp:260
Isis::iTime
Parse and return pieces of a time string.
Definition: iTime.h:65
Isis::FileName
File name manipulation and expansion.
Definition: FileName.h:100
Isis::LightTimeCorrectionState::checkSpkKernelsForAberrationCorrection
bool checkSpkKernelsForAberrationCorrection()
Check for light time/stellar aberration tag in SPK comments.
Definition: LightTimeCorrectionState.cpp:173
Isis::FileName::fileExists
bool fileExists() const
Returns true if the file exists; false otherwise.
Definition: FileName.cpp:449
Isis::PvlContainer::hasKeyword
bool hasKeyword(const QString &name) const
Check to see if a keyword exists.
Definition: PvlContainer.cpp:159
Isis::Pvl
Container for cube-like labels.
Definition: Pvl.h:119
Isis::toString
QString toString(bool boolToConvert)
Global function to convert a boolean to a string.
Definition: IString.cpp:211
Isis::Distance
Distance measurement, usually in meters.
Definition: Distance.h:34
Isis::Cube::hasBlob
bool hasBlob(const QString &name, const QString &type)
Check to see if the cube contains a BLOB.
Definition: Cube.cpp:2019
Isis::Longitude
This class is designed to encapsulate the concept of a Longitude.
Definition: Longitude.h:40
Isis::FileName::expanded
QString expanded() const
Returns a QString of the full file name including the file path, excluding the attributes.
Definition: FileName.cpp:196
Isis::iTime::Et
double Et() const
Returns the ephemeris time (TDB) representation of the time as a double.
Definition: iTime.h:126
Isis::PvlGroup
Contains multiple PvlContainers.
Definition: PvlGroup.h:41
Isis::toInt
int toInt(const QString &string)
Global function to convert from a string to an integer.
Definition: IString.cpp:93
Isis::PvlObject::hasKeyword
bool hasKeyword(const QString &kname, FindOptions opts) const
See if a keyword is in the current PvlObject, or deeper inside other PvlObjects and Pvlgroups within ...
Definition: PvlObject.cpp:236
Isis::Table
Class for storing Table blobs information.
Definition: Table.h:61
Isis::PvlObject::findObject
PvlObjectIterator findObject(const QString &name, PvlObjectIterator beg, PvlObjectIterator end)
Find the index of object with a specified name, between two indexes.
Definition: PvlObject.h:274
Isis::PvlContainer::fileName
QString fileName() const
Returns the filename used to initialise the Pvl object.
Definition: PvlContainer.h:232
Isis::Cube
IO Handler for Isis Cubes.
Definition: Cube.h:167
Isis::IException
Isis exception class.
Definition: IException.h:91
Isis::PvlObject::hasObject
bool hasObject(const QString &name) const
Returns a boolean value based on whether the object exists in the current PvlObject or not.
Definition: PvlObject.h:323
Isis::toDouble
double toDouble(const QString &string)
Global function to convert from a string to a double.
Definition: IString.cpp:149
std
Namespace for the standard library.
Isis::Cube::label
Pvl * label() const
Returns a pointer to the IsisLabel object associated with the cube.
Definition: Cube.cpp:1701
Isis::EndianSwapper
Byte swapper.
Definition: EndianSwapper.h:38
Isis::PvlKeyword::size
int size() const
Returns the number of values stored in this keyword.
Definition: PvlKeyword.h:125
QVector< QString >
Isis::SpacecraftPosition
Provides swap observer/target and improved light time correction.
Definition: SpacecraftPosition.h:60
Isis::LightTimeCorrectionState
Provides interface to user configurable Light Time correction feature.
Definition: LightTimeCorrectionState.h:48
Isis::Longitude::force360Domain
Longitude force360Domain() const
This returns a longitude that is constricted to 0-360 degrees.
Definition: Longitude.cpp:267
Isis::Target
This class is used to create and store valid Isis targets.
Definition: Target.h:63
Isis::Spice::SpiceValueType
SpiceValueType
NAIF value primitive type.
Definition: Spice.h:348
Isis
This is free and unencumbered software released into the public domain.
Definition: Apollo.h:16
Isis::Target::name
QString name() const
Return target name.
Definition: Target.cpp:516
Isis::SpiceRotation
Obtain SPICE rotation information for a body.
Definition: SpiceRotation.h:209

U.S. Department of the Interior | U.S. Geological Survey
ISIS | Privacy & Disclaimers | Astrogeology Research Program
To contact us, please post comments and questions on the USGS Astrogeology Discussion Board
To report a bug, or suggest a feature go to: ISIS Github
File Modified: 07/13/2023 15:17:18