001    package edu.nrao.sss.astronomy;
002    
003    import java.io.BufferedReader;
004    import java.io.FileInputStream;
005    import java.io.InputStream;
006    import java.io.InputStreamReader;
007    import java.io.IOException;
008    import java.text.ParseException;
009    import java.text.SimpleDateFormat;
010    import java.util.HashMap;
011    import java.util.Map;
012    
013    import edu.nrao.sss.measure.ArcUnits;
014    import edu.nrao.sss.measure.Latitude;
015    import edu.nrao.sss.measure.Distance;
016    import edu.nrao.sss.measure.DistanceUnits;
017    import edu.nrao.sss.measure.LinearVelocity;
018    import edu.nrao.sss.measure.LinearVelocityUnits;
019    import edu.nrao.sss.measure.Longitude;
020    
021    /**
022     * A reader of JPL 
023     * <a href="http://ssd.jpl.nasa.gov/?horizons">Horizons</a>
024     * Ephemeris Tables.  An instance of this class may be obtained
025     * from the factory that creates ephemeris tables by using
026     * the code "JPL".
027     * <p>
028     * <b><u>Requirements for Successful Reading</u></b><br/>
029     * <ul>
030     *   <li>Only columns 1 (RA & DEC) and 20 (distance & velocity) are allowed.</li>
031     *   <li>Angle format is either HMS or Decimal Degrees.</li>
032     *   <li>Date/time format is CAL (calendar).</li>
033     *   <li>Time digits is either HH:MM, HH:MM:SS, or HH:MM:SS.SSS.</li>
034     *   <li>Range units is AU (astronomical units).<sup>1</sup></li>
035     *   <li>Output units is km/s.<sup>1</sup></li>
036     *   <li>CSV format is off.</li>
037     *   <sup>1</sup><i>These restrictions may be lifted in the future.</i>
038     * </ul></p>
039     * <p>
040     * <b><u>Example</u></b><br/>
041     * This is a portion of a table that is readable:
042     * <pre>
043     *  ...
044     *  RA format       : DEG
045     *  Time format     : CAL 
046     *  ...
047     *  2006-May-07 17:50     23 02 15.2288 -06 59 14.999 20.5170849503044 -25.7582836
048     *  2006-May-07 18:00     23 02 15.2799 -06 59 14.697 20.5169816409424 -25.7597799
049     *  2006-May-07 18:10     23 02 15.3309 -06 59 14.395 20.5168783255799 -25.7612759
050     *  2006-May-07 18:20     23 02 15.3819 -06 59 14.093 20.5167750042186 -25.7627715
051     *  ...
052     * </pre>
053     * A full JPL file can be found
054     * <a href="doc-files/jplEphemTable.txt">here</a>.</p>
055     * <p>
056     * <b>Version Info:</b>
057     * <table style="margin-left:2em">
058     *   <tr><td>$Revision: 762 $</td></tr>
059     *   <tr><td>$Date: 2007-07-09 09:08:38 -0600 (Mon, 09 Jul 2007) $</td></tr>
060     *   <tr><td>$Author: dharland $</td></tr>
061     * </table></p>
062     *  
063     * @author David M. Harland
064     * @since 2006-06-12
065     */
066    public class JplEphemerisTableReader
067      implements EphemerisTableReader
068    {
069      //Labels in the JPL files that show start & end of table
070      private static final String START_OF_ENTRIES_MARKER = "$$SOE";
071      private static final String END_OF_ENTRIES_MARKER   = "$$EOE";
072      
073      //The JPL date can be in any one of these formates
074      private static final String DATE_FORMAT_SHORT  = "yyyy-MMM-dd_HH:mmZ";
075      private static final String DATE_FORMAT_MEDIUM = "yyyy-MMM-dd_HH:mm:ssZ";
076      private static final String DATE_FORMAT_LONG   = "yyyy-MMM-dd_HH:mm:ss.SSSZ";
077      
078      private static final Map<Integer, String> DATE_FORMATS =
079        new HashMap<Integer, String>();
080      
081      static
082      {
083        DATE_FORMATS.put(DATE_FORMAT_SHORT.length(),  DATE_FORMAT_SHORT);
084        DATE_FORMATS.put(DATE_FORMAT_MEDIUM.length(), DATE_FORMAT_MEDIUM);
085        DATE_FORMATS.put(DATE_FORMAT_LONG.length(),   DATE_FORMAT_LONG);
086      }
087      
088      //Used for intermethod communication during parsing
089      private BufferedReader   dataReader;
090      private boolean          angleAsDegrees;
091      private EphemerisTable   ephemerisTable;
092      private SimpleDateFormat dateFormatter;
093      
094      JplEphemerisTableReader()
095      {
096        dateFormatter  = new SimpleDateFormat();
097      }
098    
099      private void reset()
100      {
101        dataReader     = null;
102        angleAsDegrees = true;
103        ephemerisTable = null;
104      }
105    
106      /* (non-Javadoc)
107       * @see edu.nrao.sss.astronomy.EphemerisTableReader#readFile(java.lang.String)
108       */
109      public EphemerisTable read(String fileName)
110        throws IOException
111      {
112        return read(fileName, null);
113      }
114    
115      /* (non-Javadoc)
116       * @see edu.nrao.sss.astronomy.EphemerisTableReader#readFile(
117       * java.lang.String, edu.nrao.sss.astronomy.EphemerisTable)
118       */
119      public EphemerisTable read(String fileName, EphemerisTable destination)
120        throws IOException
121      {
122        return read(new FileInputStream(fileName), destination);
123      }
124      
125      /* (non-Javadoc)
126       * @see edu.nrao.sss.astronomy.EphemerisTableReader#readFile(
127       * java.io.InputStream)
128       */
129      public EphemerisTable read(InputStream in)
130        throws IOException
131      {
132              return read(in, null);
133      }
134      
135      /* (non-Javadoc)
136       * @see edu.nrao.sss.astronomy.EphemerisTableReader#readFile(
137       * java.io.InputStream, edu.nrao.sss.astronomy.EphemerisTable)
138       */
139      public EphemerisTable read(InputStream in, EphemerisTable destination)
140        throws IOException
141      {
142        this.reset();
143        
144        ephemerisTable = (destination == null) ? new EphemerisTable()
145                                               : destination;
146        try
147        {
148          dataReader = new BufferedReader(new InputStreamReader(in));
149          
150          findFormats();
151        
152          fillTable();
153    
154          confirmUnits();
155          
156          return ephemerisTable;
157        }
158        //No catch; make client aware of the problem
159        finally
160        {
161          dataReader.close();
162        }
163      }
164      
165      /** Finds the angle and time formats used in the file. */
166      private void findFormats() throws IOException
167      {
168        boolean foundRA = false;
169        boolean foundTime = false;
170        
171        while (!foundRA || !foundTime)
172        {
173          String line = dataReader.readLine();
174    
175          if (line == null)
176            break;
177            
178          if (line.startsWith("RA format"))
179          {
180            line = line.replaceAll("\\s", "");
181            angleAsDegrees = line.endsWith("DEG");
182            foundRA = true;
183          }
184            
185          if (line.startsWith("Time format"))
186          {
187            line = line.replaceAll("\\s", "");
188            if (!line.endsWith("CAL")) 
189              throw new IOException("Expected this line to end with 'CAL':\n" +
190                                    line);
191            foundTime = true;
192          }
193        }
194      }
195      
196      /** Fills an ephemeris table from the table section of the file. */
197      private void fillTable() throws IOException
198      {
199        seekTableStart();
200        
201        String line = dataReader.readLine();
202        
203        while (!line.replaceAll("\\s","").equals(END_OF_ENTRIES_MARKER))
204        {
205          EphemerisTableEntry entry = new EphemerisTableEntry();
206    
207          int charPosition = 0;
208          
209          charPosition = parseTimeStamp     (entry, line, charPosition);
210          charPosition = parseRightAscension(entry, line, charPosition);
211          charPosition = parseDeclination   (entry, line, charPosition);
212          charPosition = parseDistance      (entry, line, charPosition);
213          charPosition = parseVelocity      (entry, line, charPosition);
214          
215          ephemerisTable.add(entry);
216    
217          line = dataReader.readLine();
218        }
219      }
220      
221      /**
222       * Throws exception if it finds unexpected distance or velocity units.
223       * Does nothing if it finds expected units, or if it finds no units at all.
224       */
225      private void confirmUnits() throws IOException
226      {
227        String line = dataReader.readLine().trim();
228        
229        boolean foundParagraph = false;
230        
231        //Read to end of file if necessary
232        while (line != null)
233        {
234          if (!foundParagraph)
235          {
236            foundParagraph = line.startsWith("delta  deldot");
237          }
238          else
239          {
240            if (line.contains("Units:"))
241            {
242              int index = line.indexOf("Units:") + "Units:".length();
243              
244              //Expecting: "AU and KM/S"
245              String endOfLine = line.substring(index).trim();
246              
247              String[] units = endOfLine.split(" ");
248              
249              if (!units[0].equalsIgnoreCase("AU"))
250                throw new IOException("Expected distance units of 'AU', found '" +
251                                      units[0] + "'.");
252              
253              if (!units[2].equalsIgnoreCase("KM/S"))
254                throw new IOException("Expected velocity units of 'KM/S', found '" +
255                                      units[2] + "'.");
256              
257              break; //Once we find "Units:" in "delta  deldot" paragraph, stop
258            }
259          }
260          
261          line = dataReader.readLine().trim();
262        }
263      }
264      
265      /**
266       * Advances the file pointer to the line following the line that contains
267       * the start-of-table marker.
268       */
269      private void seekTableStart() throws IOException
270      {
271        String line = "";
272        
273        while (!line.equals(START_OF_ENTRIES_MARKER))
274        {
275          line = dataReader.readLine();
276          
277          if (line == null)
278            throw new IOException("Did not find start-of-table marker (" +
279                                  START_OF_ENTRIES_MARKER + ")");
280    
281          line = line.replaceAll("\\s", "");
282        }
283      }
284      
285      /** Parses line beginning at startPos and calls entry.setTime. */
286      private int parseTimeStamp(EphemerisTableEntry entry,
287                                 String line, int startPos) throws IOException
288      {
289        //Skip past any leading whitespace
290        int pos = skipWhiteSpace(line, startPos);
291    
292        StringBuilder timeStamp = new StringBuilder();
293    
294        //Find the date part.  Expect yyyy-MMM-dd.
295        pos = appendNonWhiteSpaceTo(timeStamp, line, pos);
296    
297        //Skip past any intervening whitespace
298        pos = skipWhiteSpace(line, pos);
299    
300        //Use '_' as date/time separator
301        timeStamp.append('_');
302    
303        //Find the time part.  Expect hh:mm, hh:mm:ss, or hh:mm:ss.sss
304        pos = appendNonWhiteSpaceTo(timeStamp, line, pos);
305        
306        //Turn the text into a date object.  The "+1" is for "Z" suffix.
307        dateFormatter.applyPattern(DATE_FORMATS.get(timeStamp.length()+1));
308        
309        try {
310          //The "+0000" indicates time is in UT.
311          entry.setTime(dateFormatter.parse(timeStamp.toString()+"+0000"));
312        }
313        catch (ParseException ex) {
314          throw new IOException("Trouble parsing date '" + timeStamp.toString() +
315                                "'.");
316        }
317    
318        return pos;
319      }
320      
321      /** Parses line beginning at startPos and calls entry.setRightAscension. */
322      private int parseRightAscension(EphemerisTableEntry entry,
323                                      String line, int startPos) throws IOException
324      {
325        StringBuilder raText = new StringBuilder();
326        
327        String[] symbols = {ArcUnits.HOUR.getSymbol(),
328                            ArcUnits.MINUTE.getSymbol(),
329                            ArcUnits.SECOND.getSymbol()};
330      
331        int pos = parseAngle(raText, line, startPos, symbols);
332        
333        entry.setLongitude(Longitude.parse(raText.toString()));
334        
335        return pos;
336      }
337      
338      /** Parses line beginning at startPos and calls entry.setDeclination. */
339      private int parseDeclination(EphemerisTableEntry entry,
340                                   String line, int startPos) throws IOException
341      {
342        StringBuilder decText = new StringBuilder();
343        
344        String[] symbols = {ArcUnits.DEGREE.getSymbol(),
345                            ArcUnits.ARC_MINUTE.getSymbol(),
346                            ArcUnits.ARC_SECOND.getSymbol()};
347        
348        int pos = parseAngle(decText, line, startPos, symbols);
349        
350        entry.setLatitude(Latitude.parse(decText.toString()));
351    
352        return pos;
353      }
354      
355      /** Does the actual parsing for RA & Dec. */
356      private int parseAngle(StringBuilder buff, String line, int startPos,
357                             String[] symbols) throws IOException
358      {
359        //Skip past any leading whitespace
360        int pos = skipWhiteSpace(line, startPos);
361        
362        //Find the first number.
363        pos = appendNonWhiteSpaceTo(buff, line, pos);
364     
365        //If we're not using decimal degrees, then we need two more numbers.
366        if (!angleAsDegrees)
367        {
368          buff.append(symbols[0]);
369    
370          //Skip past any intervening whitespace
371          pos = skipWhiteSpace(line, pos);
372    
373          //Find the next number
374          pos = appendNonWhiteSpaceTo(buff, line, pos);
375          buff.append(symbols[1]);
376    
377          //Skip past any intervening whitespace
378          pos = skipWhiteSpace(line, pos);
379    
380          //Find the last number
381          pos = appendNonWhiteSpaceTo(buff, line, pos);
382          buff.append(symbols[2]);
383        }
384        else
385        {
386          buff.append(ArcUnits.DEGREE.getSymbol());
387        }
388        
389        return pos;
390      }
391     
392      /** Parses line beginning at startPos and calls entry.setDistance. */
393      private int parseDistance(EphemerisTableEntry entry,
394                                String line, int startPos) throws IOException
395      {
396        int pos = skipWhiteSpace(line, startPos);
397    
398        StringBuilder distance = new StringBuilder();
399        
400        pos = appendNonWhiteSpaceTo(distance, line, pos);
401    
402        distance.append(DistanceUnits.ASTRONOMICAL_UNIT.getSymbol());
403        
404        entry.setDistance(Distance.parse(distance.toString()));
405    
406        return pos;
407      }
408      
409      /** Parses line beginning at startPos and calls entry.setVelocity. */
410      private int parseVelocity(EphemerisTableEntry entry,
411                                String line, int startPos) throws IOException
412      {
413        int pos = skipWhiteSpace(line, startPos);
414    
415        StringBuilder velocity = new StringBuilder();
416        
417        pos = appendNonWhiteSpaceTo(velocity, line, pos);
418        
419        velocity.append(LinearVelocityUnits.KILOMETERS_PER_SECOND.getSymbol());
420        
421        entry.setVelocity(LinearVelocity.parse(velocity.toString()));
422    
423        return pos;
424      }
425      
426      /**
427       * Skips all whitespace in text beginning at startPos.
428       * The returned position is either that of the first
429       * non-whitespace character at or after startPos,
430       * or is equal to the length of text.
431       */
432      private int skipWhiteSpace(String text, int startPos)
433      {
434        int pos     = startPos;
435        int lastPos = text.length();
436        
437        while (pos < lastPos)
438        {
439          if (!Character.isSpaceChar(text.charAt(pos)))
440            break;
441          
442          pos++;
443        }
444        
445        return pos;
446      }
447    
448      /**
449       * Appends to buff a run of consecutive non-whitespace
450       * characters from text beginning at startPos.
451       * The returned position is either that of the first
452       * whitespace character at or after startPos,
453       * or is equal to the length of text.
454       */
455      private int appendNonWhiteSpaceTo(StringBuilder buff,
456                                        String text, int startPos)
457      {
458        int pos     = startPos;
459        int lastPos = text.length();
460        
461        while (pos < lastPos)
462        {
463          char character = text.charAt(pos);
464          
465          if (Character.isSpaceChar(character))
466            break;
467          
468          buff.append(character);
469          pos++;
470        }
471        
472        return pos;
473      }
474      
475      //This is here for quick, manual, testing
476      /*
477      public static void main(String[] args)
478      {
479        JplEphemerisTableReader reader = new JplEphemerisTableReader();
480    
481        try {
482          EphemerisTable newTable = 
483            reader.read("/export/home/calmer/dharland/JUNK/jplEphem02.txt");
484          System.out.println("Table size = " + newTable.size());
485        }
486        catch (IOException ex) {
487          System.out.println("It didn't work because...");
488          System.out.println(ex.getMessage());
489          ex.printStackTrace();
490        }
491      }*/
492    }