001    package ui.recognizer;
002    
003    import ui.model.Spectra;
004    import java.awt.geom.*;
005    import java.awt.image.*;
006    import javax.swing.border.*;
007    import javax.swing.event.MouseInputAdapter;
008    import javax.swing.*;
009    import java.awt.image.BufferedImage;
010    import java.awt.event.*;
011    import java.awt.*;
012    
013    /** A JSpectra is a kind of JComponent which represents a spectra. The JSpectra
014     * contains an instance of Spectra which can be interpreted either as a
015     * continuous spectrum or a discrete series of spectral lines depending on the
016     * type of Spectra. Can be scaled and rotated. Can contain a movable cursor.
017     * Contains zero or more zones which can be highlighted.
018     * Cantains zero or more icons indicating interesting features and 'bookmarks' (as tooltips of the icons)
019     * Can be constructed by providing a file name for a spectra data text file.
020     *
021     * @author John Talbot
022     */
023    public class JSpectra extends JComponent {
024    
025        public JSpectra(Spectra aSpectra) {
026            this(aSpectra, DEFAULT_SCALE, DEFAULT_OFFSET, false, false,
027                 MonochromaticColor.MINIMUM_WAVELENGTH, MonochromaticColor.MAXIMUM_WAVELENGTH, true);
028        }
029    
030        public JSpectra(Spectra aSpectra, double aScale, double anOffset, boolean aShowTics, boolean aShowAxisLabels,
031                        double aMinWavelength, double aMaxWavelength, boolean aContinuous) {
032            setOpaque(true);
033    
034            scale = aScale;
035            offset = anOffset;
036            showTics = aShowTics;
037            showAxisLabels = aShowAxisLabels;
038            minWavelength = aMinWavelength;
039            maxWavelength = aMaxWavelength;
040            continuous = aContinuous;
041    
042            setSpectra(aSpectra);
043    
044            // wavelength zone selected by mouse
045            ZoneListener zoneListener = new ZoneListener();
046            addMouseListener(zoneListener);
047            addMouseMotionListener(zoneListener);
048        }
049    
050    
051        /** Create a buffered image containing the spectra.
052         * @param aSpectra the spectra data
053         */
054        public BufferedImage createSpectraImage(Spectra aSpectra) {
055            if (isContinuous())
056              return createContinuousSpectraImage(aSpectra);
057            else
058              return createDiscreteSpectraImage(aSpectra);
059        }
060    
061        /** Create a continous spectra in which there is a one to one correspondence
062         * between spectra sample points and pixels.
063         */
064        public BufferedImage createContinuousSpectraImage(Spectra aSpectra) {
065            spectraImage = new BufferedImage(aSpectra.size(), 1, BufferedImage.TYPE_INT_ARGB);
066            //Graphics2D g2 = spectraImage.createGraphics();
067    
068            // this only works for equally sampled spectra, will fail if wavelength sample spacing varies.
069            for (int i = 0; i < aSpectra.size(); i++) {
070                DataCoordinate data = aSpectra.getScaledDataCoordinate(i);
071                double wavelength = data.getX();
072                double intensity = Math.min(data.getY() * getScale() + getOffset(), 1.0d);
073                if (intensity < 0.0) intensity = 0.0;
074                Color color = new MonochromaticColor(wavelength, intensity);
075                spectraImage.setRGB(i, 0, color.getRGB());
076            }
077            return spectraImage;
078        }
079    
080        /** Create a discrete spectra which consists of a sum of overlapping gaussians
081         * representing the spectral lines.
082         */
083        public BufferedImage createDiscreteSpectraImage(Spectra aSpectra) {
084            double startWavelength = getMinWavelength();  //plot range
085            double endWavelength = getMaxWavelength();
086    
087            //TODO: the following should be parameters :
088            double lineWidth = 2.0e-10;  // in meters
089            double contrast = 20.0;
090            double continuum = 0.25;
091    
092            //Discrete spectral lines, should not be scaled or lines get too blurry and faint
093            int n = 2800;  // horizontal resolution which closely matches output resolution
094    
095            double[] intensity = new double[n];  // temporary spectra
096            double dwave = (endWavelength - startWavelength) / n;
097            double lineWidth2 = lineWidth * lineWidth;
098            double maxs = -1e23;
099    
100            for(int i = 0; i < n ;i++) {
101              double wavelength = i*dwave + startWavelength;
102              double sum = 0.0;
103              for(int j = 0; j < aSpectra.size(); j++) {  // sum of gaussian emission line profile for all lines
104                DataCoordinate data = aSpectra.getScaledDataCoordinate(j);
105                double delta = wavelength - data.getX();
106                sum = sum + data.getY() * Math.exp(-delta * delta / lineWidth2);
107              }
108              intensity[i] = sum;
109              if(sum > maxs) maxs = sum;
110            }
111            if(maxs == 0.0) maxs = 1.0;
112    
113            double scale = (1.0 - continuum) * contrast / maxs;
114            if(scale == 0.0) scale = 1.0 / maxs;
115    
116            spectraImage = new BufferedImage(n, 1, BufferedImage.TYPE_INT_ARGB);
117            for (int i = 0; i < n; i++) {
118                double wavelength = i*dwave + startWavelength;
119                double brightness = scale*intensity[i] + continuum;
120                if (brightness > 1.0) brightness = 1.0;
121                Color color = new MonochromaticColor(wavelength, brightness);
122                spectraImage.setRGB(i, 0, color.getRGB());
123            }
124            return spectraImage;
125        }
126    
127        public void paintComponent(Graphics g) {
128            Graphics2D g2 = (Graphics2D) g;
129            Rectangle drawHere = g2.getClipBounds();
130            if (drawHere != null) {
131                g2.setColor(Color.gray);         // Fill clipping area with neutral gray
132                g2.fillRect(drawHere.x, drawHere.y, drawHere.width, drawHere.height);
133            } else {
134                System.out.println("TODO: There is no clipBounds, therefore draw entire area ...");
135            }
136    
137            // the spectra box usually is much larger than the destination box
138    
139            // the destination box usually is much larger than the source box // should use drawHere
140    
141            //double clipXmin mapPixelToWavelength(drawHere.x);
142            //double clipXmax mapPixelToWavelength(drawHere.x + drawHere.width);
143    
144    
145            //TODO: Very Kludgy ...
146            // BUG: will not work if spectra starts after the JSpectra Minimum
147    
148            int destinationX1 = 0;                            // mapWavelengthToPixel(getMinWavelength());
149            int destinationY1 = 0;
150            int destinationX2 = (int) getSize().getWidth();   // mapWavelengthToPixel(getMaxWavelength());
151            int destinationY2 = (int) getSize().getHeight();
152    
153            Calibration calibration = getSpectra().getCalibration();
154    
155            ImageCoordinate source1 = calibration.mapInDataUnits(new DataCoordinate(getMinWavelength(), 0.0d));
156            ImageCoordinate source2 = calibration.mapInDataUnits(new DataCoordinate(getMaxWavelength(), 1.0d));
157    
158            //System.out.println(source1.getX() + "," + source1.getY() + "," + source2.getX() + "," + source2.getY());
159            g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
160            g2.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
161            g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
162    
163            if (isContinuous())
164                 g2.drawImage(getSpectraImage(), destinationX1,  destinationY1,  destinationX2,  destinationY2,
165                                            (int) source1.getX(), 0, (int) source2.getX(), 1, this);
166            else
167                 g2.drawImage(getSpectraImage(), destinationX1,  destinationY1,  destinationX2,  destinationY2,
168                                            0, 0, getSpectraImage().getWidth(), 1, this);
169    
170    
171            if (isShowAxisLabels()) {
172                g2.setPaint(Color.black);
173                for (double wave = getMinWavelength(); wave < getMaxWavelength(); wave+=2e-8) {
174                    // TODO: the next two lines should make use of mapWavelengthToPixel
175    
176                    double x = (wave - getMinWavelength())/(getMaxWavelength()-getMinWavelength());
177                    x = x * (destinationX2 - destinationX1) + destinationX1;
178    
179                    String label = "" + wave * Units.NANOMETERS;
180                    label = label.substring(0, 3);
181                    double labelWidth = 15;
182                    g2.drawString(label, (float) (x-labelWidth/2.0d), (float) (getSize().getHeight()/3.0d));
183                }
184            }
185            double ticWidth;
186            if (getSize().getWidth() < 1024.0)
187                ticWidth = 1;
188            else
189                ticWidth = 2;
190    
191            double ticHeight = getSize().getHeight()/4.0d;
192    
193            if (isShowTics()) {
194                // TODO: devise a more intelligent way to create tics
195                // NOTE: the fill(Rectangle2D) approach produces anti-aliased tics (good or bad ?)
196                g2.setPaint(Color.black);
197                for (double wave = 380 * Units.NANOMETERS; wave < getMaxWavelength(); wave+=10 * Units.NANOMETERS) {
198                    double x = (wave - getMinWavelength())/(getMaxWavelength()-getMinWavelength());
199                    x = x * (destinationX2 - destinationX1) + destinationX1;
200                    g2.fill(new java.awt.geom.Rectangle2D.Double(x, 0.0d, ticWidth, 0.25*ticHeight));
201                }
202                for (double wave = 400 * Units.NANOMETERS; wave < getMaxWavelength(); wave+=50 * Units.NANOMETERS) {
203                    double x = (wave - getMinWavelength())/(getMaxWavelength()-getMinWavelength());
204                    x = x * (destinationX2 - destinationX1) + destinationX1;
205                    g2.fill(new java.awt.geom.Rectangle2D.Double(x, 0.0d, ticWidth, 0.5*ticHeight));
206                }
207                for (double wave = 400 * Units.NANOMETERS; wave < getMaxWavelength(); wave+=100 * Units.NANOMETERS) {
208                    double x = (wave - getMinWavelength())/(getMaxWavelength()-getMinWavelength());
209                    x = x * (destinationX2 - destinationX1) + destinationX1;
210                    g2.fill(new java.awt.geom.Rectangle2D.Double(x, 0.0d, ticWidth, ticHeight));
211                }
212    
213            }
214            // if JSpectra instanceof PlotSpectra then do something else
215    
216            //If currentRect exists, paint a box on top.
217            if (currentRect != null) {
218                g2.setColor(Color.white);
219                g2.drawRect(rectToDraw.x, rectToDraw.y, rectToDraw.width - 1, rectToDraw.height - 1);
220                //controller.updateLabel(rectToDraw);   // do something with the wavelength zone information
221            }
222    
223        }
224    
225        Rectangle currentRect = null;
226        Rectangle rectToDraw = null;
227        Rectangle previousRectDrawn = new Rectangle();
228    
229        // Mouse selected wavelength zone from spectra
230        // from http://java.sun.com/docs/books/tutorial/uiswing/painting/example-swing/SelectionDemo.java
231        class ZoneListener extends MouseInputAdapter {
232    
233            public void mousePressed(MouseEvent e) {
234                currentRect = new Rectangle(e.getX(), e.getY(), 0, 0);
235                updateDrawableRect(getWidth(), getHeight());
236                repaint();
237            }
238    
239            public void mouseDragged(MouseEvent e) {
240                updateSize(e);
241            }
242    
243            public void mouseReleased(MouseEvent e) {
244                updateSize(e);
245            }
246    
247            /*
248             * Update the size of the current rectangle
249             * and call repaint.  Because currentRect
250             * always has the same origin, translate it
251             * if the width or height is negative.
252             *
253             * For efficiency (though
254             * that isn't an issue for this program),
255             * specify the painting region using arguments
256             * to the repaint() call.
257             *
258             */
259            void updateSize(MouseEvent e) {
260                // note this can make negative widths and heights
261                currentRect.setSize(e.getX() - currentRect.x, e.getY() - currentRect.y);
262                updateDrawableRect(getWidth(), getHeight());
263                Rectangle totalRepaint = rectToDraw.union(previousRectDrawn);
264                // repaint and restrict clip bound to changed area
265                repaint(totalRepaint.x, totalRepaint.y, totalRepaint.width, totalRepaint.height);
266            }
267        }
268    
269        void updateDrawableRect(int compWidth, int compHeight) {
270            int x = currentRect.x;
271            int y = currentRect.y;
272            int width = currentRect.width;
273            int height = currentRect.height;
274    
275            //Make the width and height positive, if necessary.
276            if (width < 0) {
277                width = -width;
278                x = x - width + 1;
279                if (x < 0) {
280                    width += x;
281                    x = 0;
282                }
283            }
284            if (height < 0) {
285                height = -height;
286                y = y - height + 1;
287                if (y < 0) {
288                    height += y;
289                    y = 0;
290                }
291            }
292    
293            //The rectangle shouldn't extend past the drawing area.
294            if ((x + width) > compWidth) {
295                width = compWidth - x;
296            }
297            if ((y + height) > compHeight) {
298                height = compHeight - y;
299            }
300    
301            //Update rectToDraw after saving old value.
302            if (rectToDraw != null) {
303                previousRectDrawn.setBounds(rectToDraw);
304                rectToDraw.setBounds(x, y, width, height);
305            } else {
306                rectToDraw = new Rectangle(x, y, width, height);
307            }
308        }
309    
310    //------Accessor methods-----------------------------------------------------------------
311    
312        public static final int DEFAULT_WIDTH = 1024;
313        public static final int DEFAULT_HEIGHT= 768;
314        public static final double DEFAULT_SCALE = 1.0d;
315        public static final double DEFAULT_OFFSET = 0.0d;
316    
317        /** The spectra object containing the spectra data
318         */
319        protected Spectra spectra;
320    
321        /** The width of the spectra in pixels (should this be a floating point number ?)
322         */
323        protected int spectraWidth  = DEFAULT_WIDTH;
324    
325        protected int spectraHeight = DEFAULT_HEIGHT;
326    
327        protected double minWavelength = MonochromaticColor.MINIMUM_WAVELENGTH;
328    
329        protected double maxWavelength = MonochromaticColor.MAXIMUM_WAVELENGTH;
330    
331        /** The spectra image with a one to one correspondence between spectra sample points and pixels.
332         */
333        protected BufferedImage spectraImage;
334    
335        protected double scale = DEFAULT_SCALE;
336    
337        protected double offset = DEFAULT_OFFSET;
338    
339        protected boolean showTics = true;
340    
341        protected boolean showAxisLabels = true;
342    
343        protected boolean continuous = true;
344    
345    
346        /** Get the spectra object from this JSpectra.
347         *
348         * @return a spectra object containing the spectra data.
349         */
350        public Spectra getSpectra() {
351            return spectra;
352        }
353    
354        public void setSpectra(Spectra aSpectra) {
355            spectra = aSpectra;
356            setSpectraImage(createSpectraImage(aSpectra));
357        }
358    
359        /** Is the Spectra continuous or does it represent a discrete set of lines ?
360         */
361        public boolean isContinuous() {
362            return continuous;
363        }
364    
365        public void setContinuous(boolean aContinuous) {
366            continuous = aContinuous;
367        }
368    
369        /** Are the tics shown ?
370         */
371        public boolean isShowTics() {
372            return showTics;
373        }
374    
375        public void setShowTics(boolean aShowTics) {
376            showTics = aShowTics;
377            // TODO: replot this JComponent
378        }
379    
380        /** Are the axis labels plotted ?
381         */
382        public boolean isShowAxisLabels() {
383            return showAxisLabels;
384        }
385    
386        public void setShowAxisLabels(boolean aShowAxisLabels) {
387            showAxisLabels = aShowAxisLabels;
388            // TODO: replot this JComponent
389        }
390    
391        /** Get the spectra image representing a one to one correspondence between spectra sample points and pixels.
392         *
393         * @return a buffered image representing the spectra at the optimum graphical resolution
394         */
395        public BufferedImage getSpectraImage() {
396            return spectraImage;
397        }
398    
399        /** Set the spectra image representing a one to one correspondence between spectra sample points and pixels.
400         *
401         * @param anImage a buffered image representing the spectra
402         */
403        public void setSpectraImage(BufferedImage anImage) {
404            spectraImage = anImage;
405        }
406    
407        /** Get the intensity scale factor to multiply the spectra intensity
408         */
409        public double getScale() {
410            return scale;
411        }
412    
413        public void setScale(double aScale) {
414            scale = aScale;
415            setSpectraImage(createSpectraImage(getSpectra()));  // must re-compute image with new intensity scale factor
416        }
417    
418        /** Get the intensity offset constant to add to the spectra intensity
419         */
420        public double getOffset() {
421            return offset;
422        }
423    
424        public void setOffset(double anOffset) {
425            offset = anOffset;
426            setSpectraImage(createSpectraImage(getSpectra()));  // must re-compute image with new offset scale factor
427        }
428    
429        /** Get points in the spectra
430         */
431        public int getPoints() {
432            if (spectra!=null)
433                return spectra.size();
434            else
435                return 0;
436        }
437    
438        /** Get the width of the JSpectra label in pixels (image of spectra may be smaller)
439         * This should be the same as preferred size.
440         */
441        public int getSpectraWidth() {
442            //getPreferredSize().getWidth();
443            return spectraWidth;
444        }
445    
446        public void setSpectraWidth(int aWidth) {
447            spectraWidth = aWidth;
448        }
449    
450        public int getSpectraHeight() {
451            //getPreferredSize().getHeight();
452            return spectraHeight;
453        }
454    
455        public void setSpectraHeight(int aHeight) {
456            spectraHeight = aHeight;
457        }
458    
459        /** Get the minimum wavelength of this JSpectra label.
460         * This is not the minimum wavelength of the Spectra object which it contains.
461         * Usually this is less than the actual spectra.
462         *
463         * @return the wavelength at which the JSpectra starts
464         */
465        public double getMinWavelength() {
466            return minWavelength;
467        }
468    
469        public void setMinWavelength(double aWavelength) {
470            minWavelength = aWavelength;
471        }
472    
473        public double getMaxWavelength() {
474            return maxWavelength;
475        }
476    
477        public void setMaxWavelength(double aWavelength) {
478            maxWavelength = aWavelength;
479        }
480    
481    
482        /** Convert or map a pixel position in JLabel to a wavelength.
483         *
484         * @return a pixel position equivalent to the wavelength or -1 if out of range
485         */
486        //public double mapPixelToWavelength(int aPixel) {
487        //    double min = getMinWavelength();
488        //    double max = getMaxWavelength();
489        //    if (aWavelength >= min && aWavelength <= max)
490        //        return (double) ( getSize().getWidth() * (aPixel - getSize().getmin) / (max - min));
491        //    else
492        //        return -1;
493        //}
494    
495        /** Convert or map a wavelength to the corresponding position in the JLabel.
496         *
497         * @return a pixel position equivalent to the wavelength or -1 if out of range
498         */
499        public int mapWavelengthToPixel(double aWavelength) {
500            double min = getMinWavelength();
501            double max = getMaxWavelength();
502            if (aWavelength >= min && aWavelength <= max)
503                return (int) ( ((double) getSpectraWidth()) * (aWavelength - min) / (max - min));
504            else
505                return -1;
506        }
507    }