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 }