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 }