001    package ui.model;
002    
003    import ui.recognizer.DataCoordinate;
004    import ui.recognizer.Calibration;
005    import ui.recognizer.Units;
006    
007    import java.io.*;
008    import java.util.List;
009    import java.util.ArrayList;
010    
011    /** A Spectra is a kind of data model which contains a list of DataCoordinates
012     * which could represent intensity versus wavelength. This list can represents
013     * a continuous spectrum or a discrete line list such as atomic transitions.
014     * A spectra can be contrusted from a pre-existing list of datacoordinates or
015     * from a file. There are several methods which can return the minimum and
016     * maximum values of wavelength and intensity. The wavelength unit is a number
017     * by which the DataCoordinate wavelength value is multiplied; it can be chosen
018     * among a small list of predifined units or can be chosen by the user.
019     *
020     * @author John Talbot
021     */
022    public class Spectra extends ArrayList<DataCoordinate> {
023    
024        /** The spectra attribute for the object's name (should use FITS header conventions for these) */
025        public static final String ATTRIBUTE_NAME = "NAME";
026    
027        /** The spectra attribute for the object's name (should use FITS header conventions for these) */
028        public static final String ATTRIBUTE_WAVELENGTH_UNIT = "WAVELENGTH_UNIT";
029    
030        static final int INITIAL_CAPACITY = 1000;  // The initial capacity of the ArrayList
031    
032        // special characters used in the spectra data file
033        protected char separator   = ' ';
034        protected char commentChar = '#';
035    
036        // the data ranges (will be different for most plots)
037        protected double minX =  Double.MAX_VALUE;
038        protected double maxX = -Double.MAX_VALUE;
039        protected double minY =  Double.MAX_VALUE;
040        protected double maxY = -Double.MAX_VALUE;
041    
042        protected Calibration calibration = new Calibration();
043    
044        // the wavelenght unit for the data coordinates
045        protected double wavelengthUnit = Units.ANGSTROMS;
046    
047        // true if there is an equal difference between successive wavelength samples
048        protected boolean uniformSampling = true;
049    
050        protected java.util.HashMap attributes = new java.util.HashMap();
051    
052        /** Create a spectra by loading file with the specified file name.
053         * @param aFileName a file name pointing to file containing the spectra data
054         */
055        public Spectra(String aFileName) {
056            this(new File(aFileName));
057        }
058    
059        /** Create a spectra by loading a file.
060         * @param aFile a file containing the spectra data
061         */
062        public Spectra(File aFile) {
063            this(aFile, Units.ANGSTROMS);
064        }
065    
066        /** Create a spectra by loading file with the specified wavelength unit.
067         * @param aFile a file containing the spectra data
068         * @param aWavelengthUnit the wavelength unit, all x coordinates in the data will be multipled by this number.
069         */
070        public Spectra(File aFile, double aWavelengthUnit) {
071            this(aWavelengthUnit);
072            setAttribute(ATTRIBUTE_NAME, aFile.getName());
073            load(aFile);
074        }
075    
076        /** Create a spectra using an array of DataCoordinate instances of the specified wavelength unit.
077         *
078         * @param aData an array of data coordinates representing spectra points
079         * @param aWavelengthUnit the wavelength unit, all x coordinates in the data will be multipled by this number.
080         */
081        public Spectra(DataCoordinate[] aData, double aWavelengthUnit) {
082            this(java.util.Arrays.asList(aData), aWavelengthUnit);
083        }
084    
085        /** Create a spectra using a list of DataCoordinate instances with the specified wavelength unit.
086         * @param aData an ordered collection of data coordinates representing spectra points
087         * @param aWavelengthUnit the wavelength unit, all x coordinates in the data will be multipled by this number.
088         */
089        public Spectra(List<DataCoordinate> aData, double aWavelengthUnit) {
090            this(aWavelengthUnit);
091            addAll(aData);
092        }
093    
094        /** Create a blank spectra with the specified wavelength unit.
095         * DataCoordinates can be added later by using the add() method.
096         * @param aWavelengthUnit the wavelength unit, all x coordinates in the data will be multipled by this number.
097         */
098        public Spectra(double aWavelengthUnit) {
099            super(INITIAL_CAPACITY);
100            getCalibration().setUnitX(aWavelengthUnit);
101        }
102    
103        /** Create a spectra using another spectra (copy constructor).
104         * @param aSpectra a spectra to be cloned
105         */
106        public Spectra(Spectra aSpectra) {
107            // Should use deep clone() because only references to DataCoordinates are used
108            // if we invoke scale(), the parent Spectra will also be affected.
109            super(INITIAL_CAPACITY);
110            setWavelengthUnit(aSpectra.getWavelengthUnit());
111            addAll(aSpectra);
112            Object[] keys = aSpectra.getAttributeKeys();
113            for (int i = 0; i < keys.length; i++) {
114                setAttribute(keys[i], aSpectra.getAttribute(keys[i]) );
115            }
116        }
117    
118        /** Load the spectra from a file and interpret the wavelength using the wavelengthUnit.
119         * @param aFile a file containing xy pairs, one per line, x and y separatated by a space
120         */
121        public void load(File aFile) {
122            try {
123                LineNumberReader in = new LineNumberReader(new FileReader(aFile));
124                String line = in.readLine();
125                double dataX, dataY;
126                clear();
127    
128                // variables required for determining uniform sampling (uniform is the default default)
129                boolean firstPoint = true;
130                double lastX = 0.0;
131                double dx = -1.0;
132                // how much dx must deviate from average before sampling is considered non-uniform
133                double dxFudgeFactor = 5.0e-3;
134    
135                while (line != null) {
136                    line = line.trim();
137                    //String[] numbers = line.split("\\s+");  // one or more spaces between numbers
138                    int whiteSpacePosition = line.indexOf(getSeparator());
139                    int commentPosition = line.indexOf(getCommentChar());
140                    if (commentPosition != -1) {  // extract attributes from comment lines
141                        String attributeLine = line.substring(commentPosition+1).trim();
142                        String attributeKey;
143                        String attributeValue;
144                        int attributeSeparator;
145                        if ((attributeSeparator = attributeLine.indexOf("=")) > -1) {  // FITS attribute
146                          attributeKey = attributeLine.substring(0, attributeSeparator).trim();
147                          attributeValue = attributeLine.substring(attributeSeparator+1).trim();
148                        } else {
149                          attributeSeparator = attributeLine.indexOf(getSeparator());
150                          attributeKey = attributeLine.substring(0, attributeSeparator).trim();
151                          attributeValue = attributeLine.substring(attributeSeparator+1).trim();
152                        }
153                        if (attributeKey != null && attributeKey.equals(ATTRIBUTE_WAVELENGTH_UNIT)) {
154                            setWavelengthUnit(Double.parseDouble(attributeValue));
155                        } else {
156                            setAttribute(attributeKey, attributeValue);
157                        }
158                    } else if (whiteSpacePosition != -1) { // extract x y pair from line
159                        try {
160                            // TODO: make the parsing more robust
161                            // 1. ignore junk after the second number
162                            // 2. skip multiple spaces between numbers or at beginning of line
163    
164                            dataX = Double.parseDouble(line.substring(0, whiteSpacePosition));
165    
166                            // is sampling uniform ?
167                            if (firstPoint)
168                                firstPoint = false;
169                            else if (isUniformSampling()) {
170                                double newdx = dataX - lastX;
171                                if (dx < 0.0)
172                                    dx = newdx;
173                                else if ( Math.abs((newdx - dx) / dx) > dxFudgeFactor) 
174                                    setUniformSampling(false);
175                            }
176                            String restOfLine = line.substring(whiteSpacePosition+1).trim();
177                            int secondWhiteSpacePosition = restOfLine.indexOf(getSeparator());
178                            String secondNumber = restOfLine;
179                            if (secondWhiteSpacePosition != -1) {
180                                secondNumber = restOfLine.substring(0, secondWhiteSpacePosition);
181                                //System.out.println(secondNumber + ":" + secondWhiteSpacePosition);
182                            }
183                            dataY = Double.parseDouble(secondNumber);
184                            minX = Math.min(minX, dataX);
185                            maxX = Math.max(maxX, dataX);
186                            //System.out.println("x=" + dataX + " maxX=" + maxX + " minX=" + minX);
187                            minY = Math.min(minY, dataY);
188                            maxY = Math.max(maxY, dataY);
189                            DataCoordinate entry = new DataCoordinate(dataX, dataY);
190                            add(entry);
191                            lastX = dataX;
192                        } catch(NumberFormatException e) {
193                            System.out.println("Could not parse number on line :" + in.getLineNumber());
194                            System.out.println(line);
195                        }
196                    }
197                    line = in.readLine();
198                }
199                // can invoke map() on the calibration object to convert from wavelength to pixel and vice-versa
200                // the rectangle represents this spectra if it were one pixel high and size() pixels wide
201                if (maxY == minY) minY = 0; // kludge to prevent divide by zero errors
202                if (maxX == minX) minX = 0; // kludge to prevent divide by zero errors
203                if (maxY == 0) maxY = 1.0d; // kludge to prevent divide by zero errors
204                setCalibration(new Calibration(new java.awt.Rectangle(0, 0, size(), 1),
205                                               minX, minY, maxX, maxY, getWavelengthUnit(), 1.0d/maxY));
206                in.close();
207            } catch (IOException ioe) {
208                ioe.printStackTrace();
209            }
210        }
211    
212        /** Save the spectra as a file with properties encoded as comments.
213         * @param aFile a file in which xy pairs will be added, one per line, x and y separatated by a space
214         */
215        public void save(File aFile) {
216            try {
217                PrintStream out = new PrintStream((OutputStream) new FileOutputStream(aFile));
218                Object[] keys = getAttributeKeys();
219                for (int i = 0; i < keys.length; i++) {
220                    out.println(getCommentChar() + " " + keys[i] + " " + getAttribute(keys[i]) );
221                }
222                for (java.util.Iterator iterator = this.iterator(); iterator.hasNext(); ) {
223                    out.println(iterator.next());
224                }
225                out.close();
226            } catch (IOException ioe) {
227                ioe.printStackTrace();
228            }
229        }
230    
231        /** Append the contents of given spectra to this spectra.
232         * If regions overlap then the new spectra will replace the old.
233         * The added spectra does not have to be sequential, it come before this spectra.
234         * A future version of this method could optionally scale the source spectra to more
235         * smoothly 'sew' the two spectra together in the overlap region.
236         * A future version of this method could contain a scaling value if the other spectra differs in vertical scale.
237         * @param aSpectra a spectra which will be appended to this spectra
238         */
239        public void append(Spectra aSpectra) {
240            if (getCalibration().getMaxX() < aSpectra.getCalibration().getMinX()) { // new spectra is placed after this spectra
241                addAll(aSpectra);
242            } else if(getCalibration().getMinX() > aSpectra.getCalibration().getMaxX()) { // new spectra is placed before this spectra
243                ArrayList<DataCoordinate> temp = new ArrayList<DataCoordinate>(this);
244                clear();
245                addAll(aSpectra);
246                addAll(temp);
247            } else {  //new spectra overlaps this spectra
248                // TODO: make new spectra replace the old in the overlap region.
249                // Optionally scale the source spectra to more smoothly 'sew' the two spectra together.
250            }
251        }
252    
253        /** Append the contents of given spectra to this spectra.
254         * If regions overlap then the new spectra will replace the old.
255         * The added spectra does not have to be sequential, it come before this spectra.
256         * A future version of this method could optionally scale the source spectra to more
257         * smoothly 'sew' the two spectra together in the overlap region.
258         * @param aSpectra a spectra which will be appended to this spectra
259         * @param aScale a scaling value to be used if the other spectra differs in vertical scale.
260         */
261        public void append(Spectra aSpectra, double aScale) {
262            Spectra tempSpectra = new Spectra(aSpectra);
263            tempSpectra.scaleIntensity(aScale);
264            append(tempSpectra);
265        }
266    
267        /** Scale the spectra intensity by the given factor.
268         * @param aScale a scale factor to multiply the spectra intensity values.
269         */
270        public void scaleIntensity(double aScale) {
271            scale(1.0d, aScale);
272        }
273    
274        /** Scale the spectra wavelength by the given factor.
275         * @param aScale a scale factor to multiply the spectra wavelength values.
276         */
277        public void scaleWavelength(double aScale) {
278            scale(aScale, 1.0d);
279        }
280    
281        /** Scale the spectra wavelength and intensity by the given factors.
282         * @param wavelengthScale a scale factor to multiply the spectra wavelength values.
283         * @param intensityScale a scale factor to multiply the spectra intensity values.
284         */
285        public void scale(double wavelengthScale, double intensityScale) {
286            for (java.util.Iterator iterator = this.iterator(); iterator.hasNext(); ) {
287                DataCoordinate data = (DataCoordinate) iterator.next();
288                data.setLocation(data.getX() * wavelengthScale, data.getY() * intensityScale);
289            }
290        }
291    
292        /** Get the spectra wavelength at the given integer position.
293         * @param position the integer position at which to obtain the wavelength
294         */
295        public double getWavelength(int position) {
296            DataCoordinate data = (DataCoordinate) get(position);
297            return data.getX();
298        }
299    
300        /** Get the spectra intensity at the given integer position.
301         * @param position the integer position at which to obtain the intensity
302         */
303        public double getIntensity(int position) {
304            DataCoordinate data = (DataCoordinate) get(position);
305            return data.getY();
306        }
307    
308        /** Get the spectra intensity given an arbitrary wavelength, interpolate if necessary.
309         *
310         * @param aWavelength the wavelength position at which the intensity is obtained 
311         */
312        public double getIntensity(double aWavelength) {
313            double intensity = 0.0;
314            // Linear interpolation
315            if (isUniformSampling()) {
316              double dx = (maxX - minX) / (size() - 1);  // sampling interval
317              double n = (aWavelength - minX) / dx;
318              int n1 = (int) Math.floor(n);
319              int n2 = (int) Math.ceil(n);
320              if (n1 >= 0 && n2 < size()) {
321                //System.out.println("size()=" + size() + " maxX=" + maxX + " minX=" + minX + "dx=" + dx + " n=" + n + " n1=" + n1 + " n2=" + n2 + " wavelength=" + aWavelength);
322                intensity = interpolate(n1, n2, aWavelength);
323              }
324            } else {  // non-uniformly sampled spectra
325              // TODO: FIXME binary search to locate position in spectra
326              if (aWavelength > minX && aWavelength < maxX) {
327                int guess = size() / 2;      // initial guess for wavelength position
328                boolean located = false;     // flag indicating that we have found the bin
329                int interval = guess;
330                //System.out.println("Wavelength=" + aWavelength);
331                while (!located) {
332                    interval = interval / 2;
333                    double x = getWavelength(guess);
334                    //System.out.println("  Guess=" + guess + " x=" + x + " dx=" + (x-aWavelength));
335                    if (x == aWavelength || interval == 0)
336                        located = true;
337                    else if (x > aWavelength)
338                        guess = guess - interval;
339                    else if (x < aWavelength)
340                        guess = guess + interval;
341                }
342                // find the points between which the wavelength is located
343                double x = getWavelength(guess);
344    
345                if (x == aWavelength) {
346                  intensity = getIntensity(guess);
347                } else {
348                  int point1 = guess;
349                  int point2 = guess;
350                  if (x < aWavelength)
351                    point2 = guess + 1;
352                  else if (x > aWavelength)
353                    point1 = guess - 1;
354    
355                  //KLUDGE: shift bracketting points slightly to make aWavelength fall within interval
356                  double x1 = getWavelength(point1);
357                  double x2 = getWavelength(point2);            
358                  boolean outside = aWavelength < x1 || aWavelength > x2;
359                  int increment = 1;
360                  if (aWavelength < x1)
361                    increment = -1;
362                  else if(aWavelength > x2)
363                    increment = 1;
364                  
365                  while(outside) {
366                      point1 += increment;
367                      point2 += increment;
368                      x1 = getWavelength(point1);
369                      x2 = getWavelength(point2);
370                      outside = aWavelength < x1 || aWavelength > x2;
371                  }
372                  //END_KLUDGE
373                  
374                  intensity = interpolate(point1, point2, aWavelength);
375                }
376                
377              } else if (aWavelength == minX) {
378                  intensity = getIntensity(0);
379              } else if (aWavelength == maxX) {
380                  intensity = getIntensity(size()-1);          
381              }
382                     
383            }
384            return intensity;        
385        }
386    
387        /** Interpolate linearly the intensity at a specified wavelength 
388         * position between two data points.
389         * Preconditions: 
390         * (1) The separation between points 1 and 2 must be greater than or equal to 1.
391         * (2) The wavelength must lie within the interval
392         * @param point1 integer representing the first point
393         * @param point2 integer representing the second point
394         * @param aWavelength 
395         */
396        protected double interpolate(int point1, int point2, double aWavelength) {
397            double x1 = getWavelength(point1);
398            double x2 = getWavelength(point2);            
399            double y1 = getIntensity(point1);
400            double y2 = getIntensity(point2);
401            double intensity;
402            if (x1 == x2)  
403              intensity = y1;   // pre-condition failure (should not happen) TODO: throw exception here
404            else {
405              double slope = (y2 - y1)/(x2 - x1);
406              intensity = slope * (aWavelength - x1) + y1;
407            }
408            return intensity;
409        }
410    
411        /** Get the intensity minus the continuum. The spectra is obtained by substracting
412         * a polynomical which approximate the average continuum.
413         * This creates a flattened spectrum in wich emission lines dominate.
414         * @param aWavelength
415         */
416        public double getIntensityMinusContinuum(double aWavelength) {
417            return getIntensity(aWavelength) - getContinuum(aWavelength);
418        }
419        
420        /** The order of the polynomial used to fit the continuum */
421        static final int ORDER = 6;
422        
423        /** An internal flag which indicates that the coefficients to
424         * continuum polynomial need to be computed. 
425         */
426        protected boolean continuumFitted = false;
427        
428        /** The polynomial fit coefficients to the continuum */
429        protected double[] continuum = new double[ORDER];
430        
431        /** Get the continuum of the spectra, obtained by fitting a
432         * polynomial to the regions of the spectra without emission lines.
433         * @param aWavelength
434         */
435        public double getContinuum(double aWavelength) {
436            if (!continuumFitted) {
437                // compute the coefficients to the continuum polynomial
438                //
439                // continuumFitted = true; 
440            }
441            // evaluate the 6th order polynomial continuum fit at aWavelength
442            return 0.0;
443        }
444        
445        protected double[] fitPolynomial() {
446            
447            return new double[6];
448        }
449    
450    //------Accessor methods-----------------------------------------------------------------
451    
452        /** Get the data coordinate for a given index in the spectra array.
453         *
454         * @param anIndex the position in the spectral point array
455         */
456        public DataCoordinate getDataCoordinate(int anIndex) {
457            return get(anIndex);
458        }
459    
460        /** Get the data coordinate scaled to unit intensity for a given index in the spectra array and with the wavelength in meters.
461         * The units are obtained from the calibration object in this spectra.
462         * @param anIndex the position in the spectral point array
463         */
464        public DataCoordinate getScaledDataCoordinate(int anIndex) {
465            DataCoordinate data = new DataCoordinate(getDataCoordinate(anIndex));  // make a copy
466            data.scale(getCalibration());
467            return data;
468        }
469    
470        /** Set the wavelength unit, all x coordinates in the data will be multipled by this number.
471         * @param aWavelengthUnit the wavelength unit
472         */
473        private void setWavelengthUnit(double aWavelengthUnit) {
474            wavelengthUnit = aWavelengthUnit;
475            setAttribute(ATTRIBUTE_WAVELENGTH_UNIT, new Double(aWavelengthUnit));
476            if (getCalibration() != null) {
477                getCalibration().setUnitX(aWavelengthUnit);
478            }
479        }
480    
481        /** Get the wavelength unit, all x coordinates in the data will be multipled by this number.
482         * @return the wavelength unit in double precision
483         */
484        private double getWavelengthUnit() {
485            return wavelengthUnit;
486        }
487    
488        /** Get the calibration for this spectra, required when wavelength to pixel/point conversion is required
489         */
490        public Calibration getCalibration() {
491            return calibration;
492        }
493    
494        /** Set the calibration for this spectra, required when wavelength to pixel/point conversion is required
495         * @param aCalibration a calibration object
496         */
497        public void setCalibration(Calibration aCalibration) {
498            calibration = aCalibration;
499        }
500    
501        /** Get the separator character used between the wavelength and the intensity in the the spectra file.
502         * @return a character used to separate the wavelength and intensity
503         */
504        public char getSeparator() {
505            return separator;
506        }
507    
508        /** Set the separator character used between the wavelength and the intensity in the the spectra file.
509         * @param aSeparator a character used to separate the wavelength and intensity
510         */
511        public void setSeparator(char aSeparator) {
512            separator = aSeparator;
513        }
514    
515        /** Get the comment character which indicates a comment line to be ignored.
516         *  A future option could be to parse comment lines for FITS headers.
517         * @return a character used to mark a comment line
518         */
519        public char getCommentChar() {
520            return commentChar;
521        }
522    
523        /** Set the comment character which indicates a comment line to be ignored.
524         * @param aCommentChar a character used to mark a comment line
525         */
526        public void setCommentChar(char aCommentChar) {
527            commentChar = aCommentChar;
528        }
529    
530        /** Get all the attribute keys.
531         * @return an array or keys one for each attribute
532         */
533        public Object[] getAttributeKeys() {
534            return attributes.keySet().toArray();
535        }
536    
537        /** Get a spectra attribute using a key.
538         * @param aKey an object representing a key
539         * @return an object which represents the value corresponding to the key
540         */
541        public Object getAttribute(Object aKey) {
542            return attributes.get(aKey);
543        }
544    
545        /** Set an attribute using a key and its corresponding value.
546         * @param aKey an object representing a key
547         * @param aValue an object which represents the value corresponding to the key
548         */
549        public void setAttribute(Object aKey, Object aValue) {
550            attributes.put(aKey, aValue);
551        }
552    
553        /** Is the wavelength sampling interval uniform. This should
554         * be automatically detected from the data file by substracting
555         * subsequent wavelength samples and determining that their separation
556         * is the same throughout the spectra.
557         */
558        public boolean isUniformSampling() {
559            return uniformSampling;
560        }
561    
562        public void setUniformSampling(boolean aUniformSampling) {
563            uniformSampling = aUniformSampling;
564        }
565        
566    
567    }