001    package edu.nrao.sss.astronomy;
002    
003    import java.awt.geom.Point2D;
004    import java.io.FileInputStream;
005    import java.io.FileNotFoundException;
006    import java.io.InputStream;
007    import java.io.IOException;
008    import java.io.Reader;
009    import java.io.Writer;
010    import java.math.BigDecimal;
011    import java.util.Date;
012    import java.util.List;
013    import java.util.SortedMap;
014    import java.util.TreeMap;
015    
016    import javax.xml.bind.JAXBException;
017    import javax.xml.bind.annotation.XmlElement;
018    import javax.xml.bind.annotation.XmlRootElement;
019    import javax.xml.stream.XMLStreamException;
020    
021    import edu.nrao.sss.math.CubicSpline;
022    import edu.nrao.sss.math.Interpolator;
023    import edu.nrao.sss.measure.ArcUnits;
024    import edu.nrao.sss.measure.DistanceUnits;
025    import edu.nrao.sss.measure.Latitude;
026    import edu.nrao.sss.measure.Distance;
027    import edu.nrao.sss.measure.LinearVelocity;
028    import edu.nrao.sss.measure.LinearVelocityUnits;
029    import edu.nrao.sss.measure.Longitude;
030    import edu.nrao.sss.measure.TimeInterval;
031    import edu.nrao.sss.util.JaxbUtility;
032    import edu.nrao.sss.util.StringUtil;
033    
034    /**
035     * A table of position entries for an astronomical source.
036     * <p>
037     * <b>Version Info:</b>
038     * <table style="margin-left:2em">
039     *   <tr><td>$Revision: 1707 $</td></tr>
040     *   <tr><td>$Date: 2008-11-14 10:23:59 -0700 (Fri, 14 Nov 2008) $</td></tr>
041     *   <tr><td>$Author: dharland $</td></tr>
042     * </table></p>
043     *  
044     * @author David M. Harland
045     * @since 2006-06-07
046     */
047    @XmlRootElement
048    public class EphemerisTable
049      extends AbsSkyPos
050    {
051      //Units used by interpolators to ensure that interpolation is done
052      //using a consistent set of units.
053      private static final ArcUnits INTERP_ARC_UNITS =
054        ArcUnits.ARC_SECOND;
055    
056      private static final DistanceUnits INTERP_DIST_UNITS =
057        DistanceUnits.KILOMETER;
058    
059      private static final LinearVelocityUnits INTERP_VELOC_UNITS =
060        LinearVelocityUnits.KILOMETERS_PER_SECOND;
061      
062      //The table itself
063      SortedMap<Date, EphemerisTableEntry> entries;
064      
065      //Used for calculating values in between the points in the table
066      private Interpolator interpLat;
067      private Interpolator interpLon;
068      private Interpolator interpDist;
069      private Interpolator interpVel;
070      
071      private boolean needToAdjustLongitudeInterpolator;
072      
073      /** Creates a new instance. */
074      public EphemerisTable()
075      {
076        super();
077        
078        entries = new TreeMap<Date, EphemerisTableEntry>();
079        
080        createInterpolators();
081        
082        needToAdjustLongitudeInterpolator = true;
083      }
084      
085      private void createInterpolators()
086      {
087        interpLat  = new CubicSpline();
088        interpLon  = new CubicSpline();
089        interpDist = new CubicSpline();
090        interpVel  = new CubicSpline();
091      }
092      
093      /**
094       * Clears all entries from this table.
095       * <p>
096       * A reset table is equivalent to a new table created via the no-argument
097       * constructor.  Note that this means that even the class used as an
098       * interpolator is reset by this method.</p>
099       */
100      @Override
101      public void reset()
102      {
103        super.reset();
104    
105        resetEntries();
106      }
107      
108      private void resetEntries()
109      {
110        entries.clear();
111        
112        createInterpolators();
113        
114        needToAdjustLongitudeInterpolator = true;
115      }
116      
117      /* (non-Javadoc)
118       * @see edu.nrao.sss.astronomy.SkyPosition#isMoving()
119       */
120      public boolean isMoving()  { return entries.size() > 1; }
121    
122      //===========================================================================
123      // VALID TIME RANGE
124      //===========================================================================
125      
126      /**
127       * Returns the interval of time for which this table is valid.
128       * 
129       * @return the interval of time for which this table is valid.
130       */
131      public TimeInterval getValidTime()
132      {
133        TimeInterval result = new TimeInterval();
134        
135        //TimeInterval is half-open, but we want closed interval, so we add
136        //smallest unit of time (one millisecond) to end of interval.
137        if (entries.size() > 0)
138          result.set(entries.firstKey(), new Date(entries.lastKey().getTime()+1L));
139        
140        return result;
141      }
142      
143      /**
144       * Returns <i>true</i> if this position is valid for
145       * the given point in time.
146       * 
147       * @param time the point in time to be checked.
148       */
149      public boolean isValidFor(Date time)
150      {
151        return getValidTime().contains(time);
152      }
153    
154      //===========================================================================
155      // INTERFACE SkyPosition
156      //===========================================================================
157    
158      /* (non-Javadoc)
159       * @see SkyPosition#getType()
160       */
161      public SkyPositionType getType()  { return SkyPositionType.EPHEMERIS_TABLE; }
162      
163      /**
164       * Returns the longitude of this position at the given point in time.
165       * <p>
166       * If {@link #isValidFor(Date) isValidFor(time)} returns <i>false</i>
167       * this method will throw an {@code IllegalArgumentException}.</p> 
168       * 
169       * @param time the point in time for which a value is requested.
170       * 
171       * @return the longitude of this position at the given point in time.
172       * 
173       * @throws IllegalArgumentException if {@code param} is not a valid time,
174       *           as determined by {@link #isValidFor(Date)}.
175       */
176      public Longitude getLongitude(Date time)
177      {
178        //Throw IllegalArgumentException if time is out of range
179        assertValidTime(time);
180        
181        //If this table was reconstituted from a persistent store (a database,
182        //XML file, etc.), the interpolators (as unpersisted attributes)
183        //may not be properly populated.
184        if (this.size() != interpLon.size())
185          repopulateInterpolators();
186        
187        //There are some special issues with longitude, due to the discontinuity
188        //at the 360/0-degree point.
189        if (needToAdjustLongitudeInterpolator)
190          rationalizeLongitudeInterpolator();
191        
192        //Use interpolator to get value in "normal" units
193        double value = interpLon.getValueFor(time.getTime());
194        
195        return new Longitude(Double.toString(value), INTERP_ARC_UNITS);
196      }
197    
198      /**
199       * Returns the latitude of this position at the given point in time.
200       * <p>
201       * If {@link #isValidFor(Date) isValidFor(time)} returns <i>false</i>
202       * this method will throw an {@code IllegalArgumentException}.</p> 
203       * 
204       * @param time the point in time for which a value is requested.
205       * 
206       * @return the latitude of this position at the given point in time.
207       * 
208       * @throws IllegalArgumentException if {@code param} is not a valid time,
209       *           as determined by {@link #isValidFor(Date)}.
210       */
211      public Latitude getLatitude(Date time)
212      {
213        //Throw IllegalArgumentException if time is out of range
214        assertValidTime(time);
215        
216        //If this table was reconstituted from a persistent store (a database,
217        //XML file, etc.), the interpolators (as unpersisted attributes)
218        //may not be properly populated.
219        if (this.size() != interpLat.size())
220          repopulateInterpolators();
221    
222        //Use interpolator to get value in "normal" units
223        double value = interpLat.getValueFor(time.getTime());
224        
225        return new Latitude(Double.toString(value), INTERP_ARC_UNITS);
226      }
227    
228      /**
229       * Returns the distance of this position at the given point in time.
230       * <p>
231       * If {@link #isValidFor(Date) isValidFor(time)} returns <i>false</i>
232       * this method will throw an {@code IllegalArgumentException}.</p> 
233       * 
234       * @param time the point in time for which a value is requested.
235       * 
236       * @return the distance of this position at the given point in time.
237       * 
238       * @throws IllegalArgumentException if {@code param} is not a valid time,
239       *           as determined by {@link #isValidFor(Date)}.
240       */
241      public Distance getDistance(Date time)
242      {
243        //Throw IllegalArgumentException if time is out of range
244        assertValidTime(time);
245        
246        //If this table was reconstituted from a persistent store (a database,
247        //XML file, etc.), the interpolators (as unpersisted attributes)
248        //may not be properly populated.
249        if (this.size() != interpDist.size())
250          repopulateInterpolators();
251    
252        //Use interpolator to get value in "normal" units
253        BigDecimal value =
254          BigDecimal.valueOf(interpDist.getValueFor(time.getTime()));
255        
256        return new Distance(value, INTERP_DIST_UNITS);
257      }
258      
259      /**
260       * Returns the radial velocity of this position at the given point in time.
261       * <p>
262       * If {@link #isValidFor(Date) isValidFor(time)} returns <i>false</i>
263       * this method will throw an {@code IllegalArgumentException}.</p> 
264       * 
265       * @param time the point in time for which a value is requested.
266       * 
267       * @return the radial velocity of this position at the given point in time.
268       * 
269       * @throws IllegalArgumentException if {@code param} is not a valid time,
270       *           as determined by {@link #isValidFor(Date)}.
271       */
272      public LinearVelocity getVelocity(Date time)
273      {
274        //Throw IllegalArgumentException if time is out of range
275        assertValidTime(time);
276        
277        //If this table was reconstituted from a persistent store (a database,
278        //XML file, etc.), the interpolators (as unpersisted attributes)
279        //may not be properly populated.
280        if (this.size() != interpVel.size())
281          repopulateInterpolators();
282    
283        //Use interpolator to get value in "normal" units
284        BigDecimal value =
285          BigDecimal.valueOf(interpVel.getValueFor(time.getTime()));
286        
287        return new LinearVelocity(value, INTERP_VELOC_UNITS);
288      }
289      
290      /**
291       * Returns the table entry with the latest time that is before or
292       * coincident with {@code time}.  If {@code time} is earlier than
293       * the earliest entry in the table, <i>null</i> is returned.
294       *
295       Doesn't look like we use this method. Not even for Hibernate or JAXB?
296       
297      private EphemerisTableEntry getEntryFor(Date time)
298      {
299        SortedMap<Date, EphemerisTableEntry> tailMap = entries.tailMap(time);
300       
301        return (tailMap.size() < 1) ? null : entries.get(tailMap.firstKey()); 
302      }*/
303    
304      /** Throws IllegalArgumentException it time is outside valid range. */
305      private void assertValidTime(Date time)
306      {
307        if (!this.isValidFor(time))
308          throw new IllegalArgumentException("The requested time of " + time +
309            " is not in the valid range, " + getValidTime() +
310            ", for this table.");
311      }
312      
313      //===========================================================================
314      // ENTRIES
315      //===========================================================================
316      
317      /**
318       * Adds {@code entry} to this table.
319       * <p>
320       * If this table already has an entry with the same timestamp as
321       * {@code entry}, the existing entry will be replaced with
322       * {@code entry}.</p>
323       * 
324       * @param entry a new, or replacement, entry for this table.  If this value
325       *              is <i>null</i> this method does nothing.
326       */
327      void add(EphemerisTableEntry entry)
328      {
329        if (entry != null)
330        {
331          entries.put(entry.getTime(), entry);
332          addToInterpolators(entry);
333        }
334      }
335    
336      /**
337       * Reads data from {@code fileName} and adds it to this table.
338       *  
339       * @param fileName the name of the file that holds the data.
340       * 
341       * @param tableType the particular type of table data in the file.
342       *                  At the present time the only
343       *                  supported type is "JPL".  See
344       *                  {@link EphemerisTableReaderFactory#getNewReader(String)}
345       *                  for more details.
346       *
347       * @throws IllegalArgumentException if {@code tableType} is cannot be
348       *           understood.
349       *
350       * @throws FileNotFoundException if no file can be found for {@code fileName}.
351       * 
352       * @throws IOException if anything goes wrong while reading the data.
353       *                 
354       * @see JplEphemerisTableReader
355       */
356      public void addFrom(String fileName, String tableType)
357        throws FileNotFoundException, IOException
358      {
359        addFrom(new FileInputStream(fileName), tableType);
360      }
361      
362      /**
363       * Reads data from {@code in} and adds it to this table.
364       *  
365       * @param in a stream that contains ephemeris data.
366       * 
367       * @param tableType the particular type of table data in the stream.
368       *                  At the present time the only
369       *                  supported type is "JPL".  See
370       *                  {@link EphemerisTableReaderFactory#getNewReader(String)}
371       *                  for more details.
372       *
373       * @throws IllegalArgumentException if {@code tableType} is cannot be
374       *           understood.
375       * 
376       * @throws IOException if anything goes wrong while reading the data.
377       * 
378       * @see JplEphemerisTableReader
379       */
380      public void addFrom(InputStream in, String tableType)
381        throws IOException, IllegalArgumentException
382      {
383        EphemerisTableReader reader = 
384          EphemerisTableReaderFactory.getSharedInstance().getNewReader(tableType);
385    
386        //This method calls-back this table's add method, which will update
387        //the interpolators.  Therefore we do not need to worry about that here.
388        reader.read(in, this);
389      }
390      
391      /**
392       * Creates a new ephemeris table and tries to load it with data from
393       * the given file.
394       * 
395       * @param fileName the name of a file that contains ephemeris data.
396       * 
397       * @param tableType the particular type of table data in the stream.
398       *                  At the present time the only
399       *                  supported type is "JPL".  See
400       *                  {@link EphemerisTableReaderFactory#getNewReader(String)}
401       *                  for more details.
402       *                 
403       * @return a new ephemeris table.
404       *
405       * @throws IllegalArgumentException if {@code tableType} is cannot be
406       *           understood.
407       *
408       * @throws FileNotFoundException if no file can be found for {@code fileName}.
409       * 
410       * @throws IOException if anything goes wrong while reading the data.
411       * 
412       * @see #addFrom(String, String)
413       */
414      public static EphemerisTable createFrom(String fileName, String tableType)
415        throws FileNotFoundException, IOException
416      {
417        EphemerisTable newTable = new EphemerisTable();
418        
419        newTable.addFrom(fileName, tableType);
420        
421        return newTable;
422      }
423    
424      /**
425       * Creates a new ephemeris table and tries to load it with data from
426       * the given stream.
427       * 
428       * @param in a stream that contains ephemeris data.
429       * 
430       * @param tableType the particular type of table data in the stream.
431       *                  At the present time the only
432       *                  supported type is "JPL".  See
433       *                  {@link EphemerisTableReaderFactory#getNewReader(String)}
434       *                  for more details.
435       *                  
436       * @return a new ephemeris table.
437       *
438       * @throws IllegalArgumentException if {@code tableType} is cannot be
439       *           understood.
440       * 
441       * @throws IOException if anything goes wrong while reading the data.
442       * 
443       * @see #addFrom(InputStream, String)
444       */
445      public static EphemerisTable createFrom(InputStream in, String tableType)
446        throws IOException
447      {
448        EphemerisTable newTable = new EphemerisTable();
449        
450        newTable.addFrom(in, tableType);
451        
452        return newTable;
453      }
454      
455      /**
456       * Returns the number of entries in this table.
457       * @return the number of entries in this table.
458       */
459      public int size()
460      {
461        return entries.size();
462      }
463      
464      /**
465       * Returns <i>true</i> if this table has no entries.
466       * @return <i>true</i> if this table has no entries, <i>false</i> otherwise.
467       */
468      public boolean isEmpty()
469      {
470        return entries.isEmpty();
471      }
472    
473      //===========================================================================
474      // INTERPOLATORS
475      //===========================================================================
476    
477      /**
478       * Sets the implementation of {@link Interpolator} that this table will use
479       * for calculating positional values.
480       * 
481       * @param interpClass a class that implements {@link Interpolator}.
482       * 
483       * @throws IllegalAccessException if this method "does not have access to the
484       *           definition of the specified class" (see Sun's documentation on
485       *           this exception).
486       *           
487       * @throws InstantiationException if "the specified class object cannot be
488       *           instantiated because it is an interface or is an abstract class"
489       *           (see Sun's documentation on this exception).
490       */
491      public void setInterpolatorClass(Class<? extends Interpolator> interpClass)
492        throws IllegalAccessException, InstantiationException
493      {
494        interpLat  = interpClass.newInstance();
495        interpLon  = interpClass.newInstance();
496        interpDist = interpClass.newInstance();
497        interpVel  = interpClass.newInstance();
498    
499        repopulateInterpolators();
500      }
501      
502      /** Clears the interpolators and repopulates them from the entry data. */
503      private void repopulateInterpolators()
504      {
505        interpLat.clear();
506        interpLon.clear();
507        interpDist.clear();
508        interpVel.clear();
509        
510        for (EphemerisTableEntry entry : entries.values())
511        {
512          addToInterpolators(entry);
513        }
514      }
515      
516      /** Adds the data from entry to the interpolators. */
517      private void addToInterpolators(EphemerisTableEntry entry)
518      {
519        Point2D xy = new Point2D.Double();
520        
521        //The time will be used as the independent variable
522        double x = (double)entry.getTime().getTime();
523    
524        //Dependent variable = latitude
525        double y = entry.getLatitude().toUnits(INTERP_ARC_UNITS).doubleValue();
526        xy.setLocation(x, y);
527        interpLat.addPoint(xy);
528    
529        //Dependent variable = longitude
530        y = entry.getLongitude().toUnits(INTERP_ARC_UNITS).doubleValue();
531        xy.setLocation(x, y);
532        interpLon.addPoint(xy);
533    
534        //Dependent variable = distance
535        y = entry.getDistance().toUnits(INTERP_DIST_UNITS).doubleValue();
536        xy.setLocation(x, y);
537        interpDist.addPoint(xy);
538    
539        //Dependent variable = velocity
540        y = entry.getVelocity().toUnits(INTERP_VELOC_UNITS).doubleValue();
541        xy.setLocation(x, y);
542        interpVel.addPoint(xy);
543      }
544      
545      /**
546       * Longitude values travel around in a circle.  As a longitude value
547       * approaches and then passes its maximum value, it reaches its
548       * minimum value and continues increasing from there.  (Thing of
549       * starting with a longitude of 355 degrees and progressing to
550       * 5 degrees.)
551       * 
552       * It is not appropriate for the interpolators to see this discontinuity.
553       * Instead the interpolators should be presented with a smoothed
554       * representation of the movement in longitude.  (In the example above,
555       * the interpolators should see 355 degrees and 365 degrees.)
556       * This method performs this adjustment.
557       */
558      private void rationalizeLongitudeInterpolator()
559      {
560        //This code relies on the fact that the Longitude class normalizes the
561        //angles sent to it so that they are always positive and that they
562        //are in the range [0..fullCircle), eg [0 degrees..360 degrees),
563        //[0 radians..2*pi radians), etc.
564        //
565        //It also relies on this table having a sufficient number of points
566        //to make interpolation reasonable.  That is, if a source is fast-moving,
567        //this code expects that the entries in the table are not widely
568        //separated in time.  This assumption allows us to deduce when we
569        //are crossing the 360/0 degree point.
570        
571        int positiveCrossings = 0;  //Going from large # to small (eg, 355d to 5d)
572        int negativeCrossings = 0;  //Going from small # to large (eg, 2d to 358d)
573        
574        final double FULL_CIRCLE = INTERP_ARC_UNITS.toFullCircle().doubleValue();
575        final double BIG_JUMP    = 0.8 * FULL_CIRCLE;
576    
577        double adjustment = 0.0;
578        
579        List<Point2D> points = interpLon.getPoints();
580        
581        double previousLongitude = points.get(0).getY();
582        
583        for (Point2D point : points)
584        {
585          double longitude = point.getY();
586          
587          //Probable discontinuity
588          if (Math.abs(longitude - previousLongitude) > BIG_JUMP)
589          {
590            if (previousLongitude > longitude)
591              positiveCrossings++;
592            else
593              negativeCrossings++;
594            
595            adjustment =
596              FULL_CIRCLE * (double)(positiveCrossings - negativeCrossings);
597          }
598          
599          //If an adjustment is needed, replace the interpolator's current point
600          //with one that contains the adjustment.
601          if (adjustment != 0.0)
602          {
603            point.setLocation(point.getX(), longitude + adjustment);
604            interpLon.addPoint(point);
605          }
606    
607          previousLongitude = longitude;  //unadjusted
608        }
609      
610        needToAdjustLongitudeInterpolator = false;
611      }
612      
613      //===========================================================================
614      // TEXT
615      //===========================================================================
616      
617      /**
618       * Returns a text representation of this table.
619       * <p>
620       * The form of the returned string is:<pre>
621       *   time<sub>1</sub>;rightAscension<sub>1</sub>;declination<sub>1</sub>;distance<sub>1</sub>;velocity<sub>1</sub>;
622       *   time<sub>2</sub>;rightAscension<sub>2</sub>;declination<sub>2</sub>;distance<sub>2</sub>;velocity<sub>2</sub>;
623       *   ...
624       *   time<sub>N</sub>;rightAscension<sub>N</sub>;declination<sub>N</sub>;distance<sub>N</sub>;velocity<sub>N</sub>;</pre></p>
625       */
626      public String toString()
627      {
628        StringBuilder buff = new StringBuilder();
629        
630        for (EphemerisTableEntry entry : entries.values())
631          buff = entry.appendTo(buff).append(StringUtil.EOL);
632        
633        return buff.toString();
634      }
635    
636      /**
637       * Returns an XML representation of this ephemeris table.
638       * @return an XML representation of this ephemeris table.
639       * @throws JAXBException if anything goes wrong during the conversion to XML.
640       */
641      public String toXml() throws JAXBException
642      {
643        return JaxbUtility.getSharedInstance().objectToXmlString(this);
644      }
645      
646      /**
647       * Writes an XML representation of this ephemeris table to {@code writer}.
648       * @param writer the device to which XML is written.
649       * @throws JAXBException if anything goes wrong during the conversion to XML.
650       */
651      public void writeAsXmlTo(Writer writer) throws JAXBException
652      {
653        JaxbUtility.getSharedInstance().writeObjectAsXmlTo(writer, this, null);
654      }
655    
656      /**
657       * Creates a new ephemeris table from the XML data in the given file.
658       * 
659       * @param xmlFile the name of an XML file.  This method will attempt to locate
660       *                the file by using {@link Class#getResource(String)}.
661       *                
662       * @return a new ephemeris table from the XML data in the given file.
663       * 
664       * @throws FileNotFoundException if the XML file cannot be found.
665       * 
666       * @throws JAXBException if the schema file used (if any) is malformed, if
667       *           the XML file cannot be read, or if the XML file is not
668       *           schema-valid.
669       * 
670       * @throws XMLStreamException if there is a problem opening the XML file,
671       *           if the XML is not well-formed, or for some other
672       *           "unexpected processing conditions".
673       */
674      public static EphemerisTable fromXml(String xmlFile)
675        throws JAXBException, XMLStreamException, FileNotFoundException
676      {
677        return JaxbUtility.getSharedInstance()
678                          .xmlFileToObject(xmlFile, EphemerisTable.class);
679      }
680      
681      /**
682       * Creates a new ephemeris table based on the XML data read from
683       * {@code reader}.
684       * 
685       * @param reader the source of the XML data.
686       *               If this value is <i>null</i>, <i>null</i> is returned.
687       *               
688       * @return a new ephemeris table based on the XML data read from
689       *         {@code reader}.
690       * 
691       * @throws XMLStreamException if the XML is not well-formed,
692       *           or for some other "unexpected processing conditions".
693       *           
694       * @throws JAXBException if anything else goes wrong during the
695       *           transformation.
696       */
697      public static EphemerisTable fromXml(Reader reader)
698        throws JAXBException, XMLStreamException
699      {
700        return JaxbUtility.getSharedInstance()
701                          .readObjectAsXmlFrom(reader, EphemerisTable.class, null);
702      }
703    
704      //Used only for JAXB.  Helps deal w/ the underlying sorted map.
705      @XmlElement(name="entry")
706      @SuppressWarnings("unused")
707      private EphemerisTableEntry[] getEntries()
708      {
709        EphemerisTableEntry[] result = new EphemerisTableEntry[size()];
710        
711        int e=0;
712        for (Date key : entries.keySet())
713          result[e++] = entries.get(key);
714    
715        return result;
716      }
717      
718      //Used only for JAXB.  Helps deal w/ the underlying sorted map.
719      @SuppressWarnings("unused")
720      private void setEntries(EphemerisTableEntry[] replacements)
721      {
722        resetEntries();
723        for (EphemerisTableEntry e : replacements)
724          add(e);
725      }
726    
727      //===========================================================================
728      // 
729      //===========================================================================
730    
731      /**
732       *  Returns a copy of this table.
733       *  <p>
734       *  If anything goes wrong during the cloning procedure,
735       *  a {@code RuntimeException} will be thrown.</p>
736       */
737      public EphemerisTable clone()
738      {
739        EphemerisTable clone = null;
740    
741        try
742        {
743          //This line takes care of the primitive fields properly
744          clone = (EphemerisTable)super.clone();
745    
746          //Clone the map and each of its entries
747          clone.entries = new TreeMap<Date, EphemerisTableEntry>();
748          
749          for (Date key : entries.keySet())
750            clone.add(this.entries.get(key).clone());
751          
752          //Give the clone the same kind of interpolator
753          clone.setInterpolatorClass(interpLon.getClass());
754        }
755        catch (Exception ex)
756        {
757          throw new RuntimeException(ex);
758        }
759          
760        return clone;
761      }
762      
763      /** Returns <i>true</i> if {@code o} is equal to this table. */
764      public boolean equals(Object o)
765      {
766        //Quick exit if o is this
767        if (o == this)
768          return true;
769        
770        //Quick exit if super class determines not equal
771        if (!super.equals(o))
772          return false;
773        
774        //Super class determined o is non-null & of same class
775        EphemerisTable other = (EphemerisTable)o;
776        
777        return other.entries.equals(this.entries);
778      }
779    
780      /** Returns a hash code value for this table. */
781      public int hashCode()
782      {
783        //Taken from the Effective Java book by Joshua Bloch.
784        //The constants 17 & 37 are arbitrary & carry no meaning.
785        
786        //You MUST keep this method in synch with equals()
787        
788        int result = super.hashCode();
789    
790        result = 37 * result + entries.hashCode();
791        
792        return result;
793      }
794    
795      //============================================================================
796      // 
797      //============================================================================
798      /*
799      public static void main(String[] args)
800      {
801        EphemerisTable table = new EphemerisTableBuilder().makeTable();
802        
803        try
804        {
805          System.out.println(table.toXml());
806        }
807        catch (JAXBException ex)
808        {
809          System.out.println("Trouble w/ table.toXml.  Msg:");
810          System.out.println(ex.getMessage());
811          ex.printStackTrace();
812        }
813        
814        System.out.println(table.toString());
815      }
816      */
817    }