package ij.gui;
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.awt.datatransfer.*;
import java.util.*;
import ij.*;
import ij.process.*;
import ij.util.*;
import ij.text.TextWindow;
import ij.plugin.filter.Analyzer;
import ij.measure.*;
import ij.io.SaveDialog;
/** This class implements the Analyze>Plot Profile command.
* @authors Michael Schmid and Wayne Rasband
*/
public class PlotWindow extends ImageWindow implements ActionListener, ClipboardOwner,
MouseListener, MouseMotionListener, KeyListener, ImageListener, Runnable {
/** Display points using a circle 5 pixels in diameter. */
public static final int CIRCLE = 0;
/** Display points using an X-shaped mark. */
public static final int X = 1;
/** Display points using an box-shaped mark. */
public static final int BOX = 3;
/** Display points using an tiangular mark. */
public static final int TRIANGLE = 4;
/** Display points using an cross-shaped mark. */
public static final int CROSS = 5;
/** Connect points with solid lines. */
public static final int LINE = 2;
private static final int WIDTH = 450;
private static final int HEIGHT = 200;
private static final String MIN = "pp.min";
private static final String MAX = "pp.max";
private static final String PLOT_WIDTH = "pp.width";
private static final String PLOT_HEIGHT = "pp.height";
private static final String OPTIONS = "pp.options";
private static final int SAVE_X_VALUES = 1;
private static final int AUTO_CLOSE = 2;
private static final int LIST_VALUES = 4;
private static final int INTERPOLATE = 8;
private static final int NO_GRID_LINES = 16;
private Button list, save, copy, live;
private Label coordinates;
private static String defaultDirectory = null;
private static int options;
private int defaultDigits = -1;
private int markSize = 5;
private static Plot staticPlot;
private Plot plot;
private String blankLabel = " ";
private ImagePlus srcImp; // the source image for live plotting
private Thread bgThread; // thread for plotting (in the background)
private boolean doUpdate; // tells the background thread to update
/** Save x-values only. To set, use Edit/Options/
Profile Plot Options. */
public static boolean saveXValues;
/** Automatically close window after saving values. To
set, use Edit/Options/Profile Plot Options. */
public static boolean autoClose;
/** The width of the plot in pixels. */
public static int plotWidth = WIDTH;
/** The height of the plot in pixels. */
public static int plotHeight = HEIGHT;
/** Display the XY coordinates in a separate window. To
set, use Edit/Options/Profile Plot Options. */
public static boolean listValues;
/** Interpolate line profiles. To
set, use Edit/Options/Profile Plot Options. */
public static boolean interpolate;
/** Add grid lines to plots */
public static boolean noGridLines;
// static initializer
static {
options = Prefs.getInt(OPTIONS, SAVE_X_VALUES);
saveXValues = (options&SAVE_X_VALUES)!=0;
autoClose = (options&AUTO_CLOSE)!=0;
listValues = (options&LIST_VALUES)!=0;
plotWidth = Prefs.getInt(PLOT_WIDTH, WIDTH);
plotHeight = Prefs.getInt(PLOT_HEIGHT, HEIGHT);
interpolate = (options&INTERPOLATE)==0; // 0=true, 1=false
noGridLines = (options&NO_GRID_LINES)!=0;
}
/**
* @deprecated
* replaced by the Plot class.
*/
public PlotWindow(String title, String xLabel, String yLabel, float[] xValues, float[] yValues) {
super(createImage(title, xLabel, yLabel, xValues, yValues));
plot = staticPlot;
}
/**
* @deprecated
* replaced by the Plot class.
*/
public PlotWindow(String title, String xLabel, String yLabel, double[] xValues, double[] yValues) {
this(title, xLabel, yLabel, Tools.toFloat(xValues), Tools.toFloat(yValues));
}
/** Creates a PlotWindow from a Plot object. */
PlotWindow(Plot plot) {
super(plot.getImagePlus());
this.plot = plot;
draw();
//addComponentListener(this);
}
/** Called by the constructor to generate the image the plot will be drawn on.
This is a static method because constructors cannot call instance methods. */
static ImagePlus createImage(String title, String xLabel, String yLabel, float[] xValues, float[] yValues) {
staticPlot = new Plot(title, xLabel, yLabel, xValues, yValues);
return new ImagePlus(title, staticPlot.getBlankProcessor());
}
/** Sets the x-axis and y-axis range. */
public void setLimits(double xMin, double xMax, double yMin, double yMax) {
plot.setLimits(xMin, xMax, yMin, yMax);
}
/** Adds a set of points to the plot or adds a curve if shape is set to LINE.
* @param x the x-coodinates
* @param y the y-coodinates
* @param shape CIRCLE, X, BOX, TRIANGLE, CROSS or LINE
*/
public void addPoints(float[] x, float[] y, int shape) {
plot.addPoints(x, y, shape);
}
/** Adds a set of points to the plot using double arrays.
Must be called before the plot is displayed. */
public void addPoints(double[] x, double[] y, int shape) {
addPoints(Tools.toFloat(x), Tools.toFloat(y), shape);
}
/** Adds error bars to the plot. */
public void addErrorBars(float[] errorBars) {
plot.addErrorBars(errorBars);
}
/** Draws a label. */
public void addLabel(double x, double y, String label) {
plot.addLabel(x, y, label);
}
/** Changes the drawing color. The frame and labels are
always drawn in black. */
public void setColor(Color c) {
plot.setColor(c);
}
/** Changes the line width. */
public void setLineWidth(int lineWidth) {
plot.setLineWidth(lineWidth);
}
/** Changes the font. */
public void changeFont(Font font) {
plot.changeFont(font);
}
/** Displays the plot. */
public void draw() {
Panel buttons = new Panel();
int hgap = IJ.isMacOSX()?1:5;
buttons.setLayout(new FlowLayout(FlowLayout.RIGHT,hgap,0));
list = new Button(" List ");
list.addActionListener(this);
buttons.add(list);
save = new Button("Save...");
save.addActionListener(this);
buttons.add(save);
copy = new Button("Copy...");
copy.addActionListener(this);
buttons.add(copy);
live = new Button("Live");
live.addActionListener(this);
buttons.add(live);
coordinates = new Label("X=12345678, Y=12345678");
coordinates.setFont(new Font("Monospaced", Font.PLAIN, 12));
coordinates.setBackground(new Color(220, 220, 220));
buttons.add(coordinates);
add(buttons);
plot.draw();
pack();
coordinates.setText(blankLabel);
ImageProcessor ip = plot.getProcessor();
if ((ip instanceof ColorProcessor) && (imp.getProcessor() instanceof ByteProcessor))
imp.setProcessor(null, ip);
else
imp.updateAndDraw();
if (listValues)
showList();
}
int getDigits(double n1, double n2) {
if (Math.round(n1)==n1 && Math.round(n2)==n2)
return 0;
else {
n1 = Math.abs(n1);
n2 = Math.abs(n2);
double n = n1<n2&&n1>0.0?n1:n2;
double diff = Math.abs(n2-n1);
if (diff>0.0 && diff<n) n = diff;
int digits = 1;
if (n<10.0) digits = 2;
if (n<0.01) digits = 3;
if (n<0.001) digits = 4;
if (n<0.0001) digits = 5;
return digits;
}
}
/** Updates the graph X and Y values when the mouse is moved.
Overrides mouseMoved() in ImageWindow.
@see ij.gui.ImageWindow#mouseMoved
*/
public void mouseMoved(int x, int y) {
super.mouseMoved(x, y);
if (plot!=null && plot.frame!=null && coordinates!=null) {
String coords = plot.getCoordinates(x,y) + blankLabel;
coordinates.setText(coords.substring(0, blankLabel.length()));
}
}
/** shows the data of the backing plot in a Textwindow with columns */
void showList(){
String headings = createHeading();
String data = createData();
TextWindow tw = new TextWindow("Plot Values", headings, data, 230, 400);
if (autoClose)
{imp.changes=false; close();}
}
/** creates the headings corresponding to the showlist funcion*/
private String createHeading(){
String head = "";
int sets = plot.storedData.size()/2;
if (saveXValues || sets>1)
head += sets==1?"X\tY\t":"X0\tY0\t";
else
head += sets==1?"Y0\t":"Y0\t";
if (plot.errorBars!=null)
head += "ERR\t";
for (int j = 1; j<sets; j++){
if (saveXValues || sets>1)
head += "X" + j + "\tY" + j + "\t";
else
head += "Y" + j + "\t";
}
return head;
}
/** creates the data that fills the showList() function values */
private String createData(){
int max = 0;
/** find the longest x-value data set */
float[] column;
for(int i = 0; i<plot.storedData.size(); i+=2){
column = (float[])plot.storedData.get(i);
int s = column.length;
max = s>max?s:max;
}
/** stores the values that will be displayed*/
ArrayList displayed = new ArrayList(plot.storedData);
boolean eb_test = false;
/** includes error bars.*/
if (plot.errorBars !=null)
displayed.add(2, plot.errorBars);
StringBuffer sb = new StringBuffer();
String v;
int n = displayed.size();
for (int i = 0; i<max; i++) {
eb_test = plot.errorBars != null;
for (int j = 0; j<n;) {
int xdigits = 0;
if (saveXValues || n>2) {
column = (float[])displayed.get(j);
xdigits = getPrecision(column);
v = i<column.length?IJ.d2s(column[i],xdigits):"";
sb.append(v);
sb.append("\t");
}
j++;
column = (float[])displayed.get(j);
int ydigits = xdigits;
if (ydigits==0)
ydigits = getPrecision(column);
v = i<column.length?IJ.d2s(column[i],ydigits):"";
sb.append(v);
sb.append("\t");
j++;
if (eb_test){
column = (float[])displayed.get(j);
v = i<column.length?IJ.d2s(column[i],ydigits):"";
sb.append(v);
sb.append("\t");
j++;
eb_test=false;
}
}
sb.append("\n");
}
return sb.toString();
}
void saveAsText() {
SaveDialog sd = new SaveDialog("Save as Text", "Values", ".txt");
String name = sd.getFileName();
if (name==null) return;
String directory = sd.getDirectory();
PrintWriter pw = null;
try {
FileOutputStream fos = new FileOutputStream(directory+name);
BufferedOutputStream bos = new BufferedOutputStream(fos);
pw = new PrintWriter(bos);
}
catch (IOException e) {
IJ.error("" + e);
return;
}
IJ.wait(250); // give system time to redraw ImageJ window
IJ.showStatus("Saving plot values...");
pw.print(createData());
pw.close();
if (autoClose)
{imp.changes=false; close();}
}
void copyToClipboard() {
Clipboard systemClipboard = null;
try {systemClipboard = getToolkit().getSystemClipboard();}
catch (Exception e) {systemClipboard = null; }
if (systemClipboard==null)
{IJ.error("Unable to copy to Clipboard."); return;}
IJ.showStatus("Copying plot values...");
int xdigits = 0;
if (saveXValues)
xdigits = getPrecision(plot.xValues);
int ydigits = xdigits;
if (ydigits==0)
ydigits = getPrecision(plot.yValues);
CharArrayWriter aw = new CharArrayWriter(plot.nPoints*4);
PrintWriter pw = new PrintWriter(aw);
for (int i=0; i<plot.nPoints; i++) {
if (saveXValues)
pw.print(IJ.d2s(plot.xValues[i],xdigits)+"\t"+IJ.d2s(plot.yValues[i],ydigits)+"\n");
else
pw.print(IJ.d2s(plot.yValues[i],ydigits)+"\n");
}
String text = aw.toString();
pw.close();
StringSelection contents = new StringSelection(text);
systemClipboard.setContents(contents, this);
IJ.showStatus(text.length() + " characters copied to Clipboard");
if (autoClose)
{imp.changes=false; close();}
}
int getPrecision(float[] values) {
int setDigits = Analyzer.getPrecision();
int measurements = Analyzer.getMeasurements();
boolean scientificNotation = (measurements&Measurements.SCIENTIFIC_NOTATION)!=0;
int minDecimalPlaces = 4;
if (scientificNotation) {
if (setDigits<minDecimalPlaces)
setDigits = minDecimalPlaces;
return -setDigits;
}
int digits = minDecimalPlaces;
if (setDigits>digits)
digits = setDigits;
boolean realValues = false;
for (int i=0; i<values.length; i++) {
if ((int)values[i]!=values[i]) {
realValues = true;
break;
}
}
if (!realValues)
digits = 0;
return digits;
}
public void lostOwnership(Clipboard clipboard, Transferable contents) {}
public void actionPerformed(ActionEvent e) {
Object b = e.getSource();
if (b==live)
toggleLiveProfiling();
else if (b==list)
showList();
else if (b==save)
saveAsText();
else
copyToClipboard();
}
public float[] getXValues() {
return plot.xValues;
}
public float[] getYValues() {
return plot.yValues;
}
/** Returns the X and Y plot values as a ResultsTable. */
public ResultsTable getResultsTable() {
int sets = plot.storedData.size()/2;
int max = 0;
for(int i = 0; i<plot.storedData.size(); i+=2) {
float[] column = (float[])plot.storedData.get(i);
int s = column.length;
if (column.length>max) max=column.length;
}
ResultsTable rt = new ResultsTable();
for (int row=0; row<max; row++) {
rt.incrementCounter();
for (int i=0; i<sets; i++) {
float[] x = (float[])plot.storedData.get(i*2);
float[] y = (float[])plot.storedData.get(i*2+1);
if (row<x.length) rt.addValue("x"+i, x[row]);
if (row<y.length) rt.addValue("y"+i, y[row]);
}
}
return rt;
}
/** Draws a new plot in this window. */
public void drawPlot(Plot plot) {
this.plot = plot;
imp.setProcessor(null, plot.getProcessor());
}
/** Called once when ImageJ quits. */
public static void savePreferences(Properties prefs) {
double min = ProfilePlot.getFixedMin();
double max = ProfilePlot.getFixedMax();
if (!(min==0.0&&max==0.0) && min<max) {
prefs.put(MIN, Double.toString(min));
prefs.put(MAX, Double.toString(max));
}
if (plotWidth!=WIDTH || plotHeight!=HEIGHT) {
prefs.put(PLOT_WIDTH, Integer.toString(plotWidth));
prefs.put(PLOT_HEIGHT, Integer.toString(plotHeight));
}
int options = 0;
if (saveXValues) options |= SAVE_X_VALUES;
if (autoClose && !listValues) options |= AUTO_CLOSE;
if (listValues) options |= LIST_VALUES;
if (!interpolate) options |= INTERPOLATE; // true=0, false=1
if (noGridLines) options |= NO_GRID_LINES;
prefs.put(OPTIONS, Integer.toString(options));
}
private void toggleLiveProfiling() {
boolean liveMode = live.getForeground()==Color.red;
if (liveMode)
removeListeners();
else
enableLiveProfiling();
}
private void enableLiveProfiling() {
if (plot!=null && bgThread==null) {
int id = plot.getSourceImageID();
srcImp = WindowManager.getImage(id);
if (srcImp==null) return;
bgThread = new Thread(this, "Live Profiler");
bgThread.setPriority(Math.max(bgThread.getPriority()-3, Thread.MIN_PRIORITY));
bgThread.start();
imageUpdated(srcImp);
}
createListeners();
if (srcImp!=null)
imageUpdated(srcImp);
}
// these listeners are activated if the selection is changed in the source ImagePlus
public synchronized void mousePressed(MouseEvent e) { doUpdate = true; notify(); }
public synchronized void mouseDragged(MouseEvent e) { doUpdate = true; notify(); }
public synchronized void mouseClicked(MouseEvent e) { doUpdate = true; notify(); }
public synchronized void keyPressed(KeyEvent e) { doUpdate = true; notify(); }
// unused listeners
public void mouseReleased(MouseEvent e) {}
public void mouseExited(MouseEvent e) {}
public void mouseEntered(MouseEvent e) {}
public void mouseMoved(MouseEvent e) {}
public void keyTyped(KeyEvent e) {}
public void keyReleased(KeyEvent e) {}
public void imageOpened(ImagePlus imp) {}
// This listener is called if the source image content is changed
public synchronized void imageUpdated(ImagePlus imp) {
if (imp==srcImp) {
if (!isSelection())
IJ.run(imp, "Restore Selection", "");
doUpdate = true;
notify();
}
}
// If either the source image or this image are closed, exit
public void imageClosed(ImagePlus imp) {
if (imp==srcImp || imp==this.imp) {
if (bgThread!=null)
bgThread.interrupt();
bgThread = null;
removeListeners();
srcImp = null;
}
}
// the background thread for live plotting.
public void run() {
while (true) {
IJ.wait(50); //delay to make sure the roi has been updated
Plot plot = getProfilePlot();
if (doUpdate && plot!=null) {
this.plot = plot;
ImageProcessor ip = plot.getProcessor();
if (ip!=null)
imp.setProcessor(null, ip);
}
synchronized(this) {
if (doUpdate) {
doUpdate = false; //and loop again
} else {
try {wait();} //notify wakes up the thread
catch(InterruptedException e) { //interrupted tells the thread to exit
return;
}
}
}
}
}
private void createListeners() {
//IJ.log("createListeners");
if (srcImp==null) return;
ImageCanvas ic = srcImp.getCanvas();
if (ic==null) return;
ic.addMouseListener(this);
ic.addMouseMotionListener(this);
ic.addKeyListener(this);
srcImp.addImageListener(this);
Font font = live.getFont();
live.setFont(new Font(font.getName(), Font.BOLD, font.getSize()));
live.setForeground(Color.red);
}
private void removeListeners() {
//IJ.log("removeListeners");
if (srcImp==null) return;
ImageCanvas ic = srcImp.getCanvas();
ic.removeMouseListener(this);
ic.removeMouseMotionListener(this);
ic.removeKeyListener(this);
srcImp.removeImageListener(this);
Font font = live.getFont();
live.setFont(new Font(font.getName(), Font.PLAIN, font.getSize()));
live.setForeground(Color.black);
}
/** Returns true if there is a straight line selection or rectangular selection */
private boolean isSelection() {
if (srcImp==null)
return false;
Roi roi = srcImp.getRoi();
if (roi==null)
return false;
return roi.getType()==Roi.LINE || roi.getType()==Roi.RECTANGLE;
}
/** Get a source image profile plot. */
private Plot getProfilePlot() {
if (srcImp==null || !isSelection())
return null;
Roi roi = srcImp.getRoi();
if (roi == null)
return null;
if (!(roi.isLine() || roi.getType()==Roi.RECTANGLE))
return null;
boolean averageHorizontally = Prefs.verticalProfile || IJ.altKeyDown();
ProfilePlot pp = new ProfilePlot(srcImp, averageHorizontally);
return pp.getPlot();
}
}