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    }