001 package edu.nrao.sss.model.source.parser; 002 003 import java.io.IOException; 004 import java.io.LineNumberReader; 005 import java.io.Reader; 006 import java.util.Date; 007 import java.text.SimpleDateFormat; 008 import java.text.ParseException; 009 010 import edu.nrao.sss.astronomy.StokesParameter; 011 import edu.nrao.sss.measure.Frequency; 012 import edu.nrao.sss.measure.FrequencyRange; 013 import edu.nrao.sss.measure.FluxDensity; 014 import edu.nrao.sss.measure.TimeInterval; 015 import edu.nrao.sss.model.source.SourceCatalog; 016 import edu.nrao.sss.model.source.Source; 017 import edu.nrao.sss.model.source.Subsource; 018 import edu.nrao.sss.model.source.PointBrightness; 019 import edu.nrao.sss.model.source.BrightnessDistribution; 020 import edu.nrao.sss.model.source.SourceBrightness; 021 022 /** 023 */ 024 public class CarmaSourceCatalogReader 025 extends AbstractSourceCatalogReader 026 { 027 private LineNumberReader dataReader; 028 private State parserState; 029 private int[] columnInd; 030 private Source[] sources; 031 032 /** Creates a new instance. */ 033 public CarmaSourceCatalogReader() 034 { 035 this.parserState = State.READING_COMMENTS; 036 this.columnInd = null; 037 this.sources = null; 038 } 039 040 /** Prepares this instance to read from a new source. */ 041 private void reset(Reader in, SourceCatalog destination) 042 { 043 this.catalog = (destination == null) ? new SourceCatalog() : destination; 044 045 this.readWasSuccessful = false; 046 this.errors.clear(); 047 048 this.columnInd = null; 049 this.parserState = State.READING_COMMENTS; 050 this.dataReader = new LineNumberReader(in); 051 } 052 053 private enum State 054 { 055 READING_COMMENTS, 056 READING_SRCS_LINE, 057 READING_EXTERNAL_INFO, 058 READING_INFO, 059 DONE; 060 } 061 062 //============================================================================ 063 // INTERFACE SourceCatalogReader 064 //============================================================================ 065 066 /** 067 * NOTE: This method closes the incoming Reader! 068 * 069 * @see AbstractSourceCatalogReader#read(java.io.Reader, 070 * edu.nrao.sss.model.source.SourceCatalog) 071 */ 072 public boolean read(Reader in, SourceCatalog destination) 073 { 074 reset(in, destination); 075 076 for (String line = getNextLine(); line != null && this.parserState != State.DONE; line = getNextLine()) 077 { 078 switch(this.parserState) 079 { 080 case READING_COMMENTS: 081 { 082 // The next line after this one is a line listing all the sources 083 if (line.matches("DATE\\s+FREQ.*")) 084 { 085 this.parserState = State.READING_SRCS_LINE; 086 } 087 break; 088 } 089 090 // This line will tell me exactly where each column starts in the 091 // following lines 092 case READING_SRCS_LINE: 093 { 094 if (line.startsWith("----------------")) 095 { 096 this.parserState = State.READING_EXTERNAL_INFO; 097 } 098 099 else 100 { 101 // If this line doesn't parse, we can't continue 102 if (!processSourcesLine(line)) 103 this.parserState = State.DONE; 104 } 105 break; 106 } 107 108 // We currently don't care about this extra info. 109 case READING_EXTERNAL_INFO: 110 { 111 if (line.startsWith("----------------")) 112 { 113 this.parserState = State.READING_INFO; 114 } 115 break; 116 } 117 118 case READING_INFO: 119 { 120 if (line.startsWith("----------------")) 121 { 122 this.parserState = State.DONE; 123 } 124 125 else 126 { 127 // Now we should actually have records to parse. 128 // We do not stop processing more lines if one line fails to parse. 129 processInfo(line); 130 } 131 break; 132 } 133 } 134 } 135 136 try 137 { 138 this.dataReader.close(); 139 } 140 141 catch(IOException ioe) 142 { 143 putError(0, "Error closing Reader: " + ioe.getMessage()); 144 } 145 146 this.readWasSuccessful = getErrorCount() == 0; 147 return this.readWasSuccessful; 148 } 149 150 //============================================================================ 151 // 152 //============================================================================ 153 154 /** Reads lines, returning first one that does not cause an error. */ 155 private String getNextLine() 156 { 157 int attempt = 0; 158 159 while (++attempt <= 100) 160 { 161 try { 162 return this.dataReader.readLine(); 163 } 164 catch (IOException ex) { 165 putError("I/O Exception: " + ex.getMessage()); 166 } 167 } 168 169 throw new RuntimeException("Failed " + attempt + 170 " times while attempting to read a line."); 171 } 172 173 /** 174 * @return the <code>n</code><sup>th</sup> field (i.e. column) in the string 175 * according to the column layout found by parsing the table header. 176 */ 177 private String getField(int n, String line) 178 { 179 int lineLength = line.length(); 180 181 int start = 0; 182 int end = lineLength; 183 184 if (n <= (this.columnInd.length - 2)) 185 { 186 start = this.columnInd[n]; 187 end = this.columnInd[n + 1]; 188 } 189 190 else if (n == (this.columnInd.length - 1)) 191 { 192 start = this.columnInd[n]; 193 } 194 195 // Make sure that this field exists in this current line before trying a 196 // subtring 197 if (start >= lineLength) 198 return ""; 199 200 else if (end > lineLength) 201 return line.substring(start).trim(); 202 203 else 204 return line.substring(start, end).trim(); 205 } 206 207 private SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd"); 208 209 /** 210 * Parses field, discarding any characters after the date itself and returns 211 * a Date object. May return null. 212 */ 213 private Date getDate(String field) 214 { 215 try 216 { 217 if (field.length() >= 8) 218 { 219 return format.parse(field.substring(0, 8)); 220 } 221 222 else 223 { 224 putError("Invalid date entry found: " + field); 225 } 226 } 227 228 catch (ParseException pe) 229 { 230 putError("Error parsing date string: " + pe.getMessage()); 231 } 232 233 return null; 234 } 235 236 /** 237 * @returns a Frequency in GHz with the value specified in field. May return null. 238 */ 239 private Frequency getFrequency(String field) 240 { 241 try 242 { 243 field = field.replaceAll("\\*", ""); 244 return Frequency.parse(field + " GHz"); 245 } 246 247 catch (IllegalArgumentException iae) 248 { 249 putError("Error parsing Frequency: " + iae.getMessage()); 250 return null; 251 } 252 } 253 254 255 /** 256 * @returns a Frequency in GHz with the value specified in field. May return null. 257 */ 258 private FluxDensity getFluxDensity(String field) 259 { 260 try 261 { 262 // These fields should either be empty or have a <number> <SPACE> 263 // <character(s)> 264 if (field.length() > 0) 265 { 266 field = field.split("\\s+")[0]; 267 return FluxDensity.parse(field); 268 } 269 } 270 271 catch (IllegalArgumentException iae) 272 { 273 putError("Error parsing FluxDensity: " + iae.getMessage()); 274 } 275 276 return null; 277 } 278 279 /** 280 * Adds <code>fd</code> to the <code>sourceNum</code><sup>th</sup> source 281 */ 282 private void addBrightness(int sourceNum, Date date, Frequency freq, FluxDensity fd) 283 { 284 Subsource center = this.sources[sourceNum].getCentralSubsource(); 285 PointBrightness pb = (PointBrightness)SourceBrightness.createBrightness(BrightnessDistribution.POINT); 286 287 pb.setPolarization(StokesParameter.I); 288 289 if (date != null) 290 { 291 TimeInterval ti = new TimeInterval(); 292 ti.set(date, ti.getEnd()); 293 pb.setValidTime(ti); 294 } 295 296 if (freq != null) 297 { 298 pb.setValidFrequency(new FrequencyRange(freq, freq)); 299 } 300 301 if (fd != null) 302 { 303 pb.setPeakFluxDensity(fd); 304 } 305 306 center.addBrightness(pb); 307 } 308 309 /** 310 * Processes a line that looks like: 311 * <p><code>yyyymmdd (GHz) 0319+415 0530+135 0854+201 0927+390 1229+020 1256-057 1642+398 1751+096 MWC349 2148+069 2225-049 2251+158 312 * </code>.</p> 313 * This will determine the start index in the following strings of each 314 * column. It also determines the names of the sources we are collecting 315 * information on. 316 * @returns true if successful 317 */ 318 private boolean processSourcesLine(String line) 319 { 320 String[] columnHeaders = line.split("\\s+"); 321 if (columnHeaders.length < 3) 322 { 323 putError("There are not enough column headers"); 324 return false; 325 } 326 327 else 328 { 329 // Now run through the array finding the first index of each 330 // column header. These integers will be the starting indexes of all 331 // of our columns. 332 this.columnInd = new int[columnHeaders.length]; 333 this.sources = new Source[columnHeaders.length - 2]; 334 335 this.columnInd[0] = line.indexOf(columnHeaders[0]); 336 this.columnInd[1] = line.indexOf(columnHeaders[1]); 337 338 for (int i = 2; i < columnHeaders.length; i++) 339 { 340 this.columnInd[i] = line.indexOf(columnHeaders[i]); 341 this.sources[i - 2] = new Source(columnHeaders[i]); 342 this.sources[i - 2].setOriginOfInformation(this.sourceInfoOrigin); 343 this.catalog.addItem(this.sources[i - 2]); 344 } 345 346 return true; 347 } 348 } 349 350 /** 351 * Each row holds a date, frequency, and flux measurements for each source 352 * named in the header line. Unfortunately, all columns are optional 353 * @returns true if successful 354 */ 355 private boolean processInfo(String line) 356 { 357 Date date = getDate(getField(0, line)); 358 Frequency freq = getFrequency(getField(1, line)); 359 360 for (int n = 2; n < this.columnInd.length; n++) 361 { 362 FluxDensity fd = getFluxDensity(getField(n, line)); 363 if (fd != null) 364 { 365 addBrightness(n - 2, date, freq, fd); 366 } 367 } 368 369 return true; 370 } 371 372 /** Adds a new parsing error to our list. */ 373 private void putError(String message) 374 { 375 putError(this.dataReader.getLineNumber(), message); 376 } 377 }