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 }