//===================================================================== // File: GraphView.java // Class: GraphView // Package: AFLPgui // // Author: James J. Benham // Date: January 6, 1999 // Contact: james_benham@hmc.edu // // Genographer v1.0 - Computer assisted scoring of gels. // Copyright (C) 1998 Montana State University // // This program is free software; you can redistribute it and/or // modify it under the terms of the GNU General Public License // as published by the Free Software Foundation; version 2 // of the License. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program; if not, write to the Free Software // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. // // The GNU General Public License is distributed in the file GPL //===================================================================== package AFLPgui; import java.awt.Choice; import java.awt.Color; import java.awt.Dimension; import java.awt.Frame; import java.awt.Graphics; import java.awt.Image; import java.awt.Label; import java.awt.Panel; import java.awt.PrintGraphics; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.ItemEvent; import java.awt.event.ItemListener; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; import AFLPcore.Bin; import AFLPcore.Cutoff; import AFLPcore.CutoffFunction; import AFLPcore.DataList; import AFLPcore.FeatureList; import AFLPcore.Lane; import AFLPcore.Option; import AFLPcore.ProgOptions; import AFLPcore.ScoringFailure; import AFLPcore.ScoreFunction; /** * Displays a graph of a bin. This class manages the size of the graph * and draws the axis and axis labels. It also provides a slider * to manipulate the cutoffs in the bin. The actually drawing of the * graph is done by objects of the type Graph. Normally, * the display is double-buffered, but this is disabled if the class is * drawing to a printer page. When it first displays, it will adjust the * cutoffs so that only one will appear in the bin, (the cutoff with * the largest starting position). The size of the display and the * graph can be manipulated by external classes. * * @see Graph * @see CutoffSlider * * @author James J. Benham * @version 1.1.0 * @date January, 1999 */ public class GraphView extends Panel implements ActionListener, ItemListener, MouseListener, MouseMotionListener { // Info Bar component parameters private static int LABEL_H_INSET = 5; private static int LABEL_V_INSET = 3; private static int LABEL_WIDTH = 600; private static int LABEL_HEIGHT = 18; // Error message location private static int LABEL_HORZ = 20; private static int LABEL_VERT = 20; // Graph layout stuff private static int SLIDER_WIDTH = 22; private static int H_SPACE = 50; private static int TICK_L_WIDTH = 30; private static int TICK_WIDTH = 3; private static int BOTTOM_BORDER = 15; // Button bar constants private static int HORZ_SPACE = 5; private static int GRAPH_CHOICE_WIDTH = 70; // Files for images private static String REL_PATH = ""; private static String PREVIOUS_FILE = "l_arrow.gif"; private static String NEXT_FILE = "r_arrow.gif"; private static String ADDCT_FILE = "plus.gif"; private static String CTDIALOG_FILE = "cutoff.gif"; // Double buffering stuff private Dimension offDimension; private Image offImage; private Graphics offGraphics; // Bar stuff private ButtonBar buttonBar; private Bar infoBar; private Label infoLabel; // Button Bar components Choice graphChoice; ImgButton previousButton; ImgButton nextButton; ImgButton addCutoffButton; ImgButton cutoffDialogButton; CutoffDialog ctDialog; private CutoffSlider slider; private Bin currentBin; private int height; private int width; private int h_inset; private int v_inset; private int graph_width; private double maxSize; private double minSize; private double scale; private double tickInc; private double maxIntensity; private DataList lanes; private DataList bins; private Graph graph[]; private int graphIndex; private Frame topWindow; /** * Create a new GraphView with the specified parameters. * * @param lanes the lanes to include in the graph. * @param bins a list of bins in the gel. * @param parentWindow an owner for dialog boxes. */ public GraphView(DataList lanes, DataList bins, Frame parentWindow) { this.lanes = lanes; this.bins = bins; topWindow = parentWindow; height = 320; width = 500; h_inset = 10; v_inset = 10; graph_width = 400; currentBin = null; graph = new Graph[2]; graph[0] = new BarGraph(); graph[1] = new ScatterGraph(); graphIndex = 0; graphChoice = new Choice(); graphChoice.add("Bar"); graphChoice.add("Scatter"); graphChoice.select(graphIndex); // add the slider control slider = new CutoffSlider(); slider.addMouseListener(this); slider.addMouseMotionListener(this); add(slider); slider.setBounds(0, v_inset, SLIDER_WIDTH, height - v_inset - BOTTOM_BORDER); setLayout(null); setBounds(0, 0, width, height); ctDialog = new CutoffDialog(topWindow, "Cutoffs...", true); createButtonBar(); createInfoBar(); } /** * Initialize the graph view. This will do things like check to see * that only a single cutoff applies to the specified bin in the * lanes. It will also set the scale for the graph and score any bins * that need scoring. * * @param bin the bin to show a graph for * @param lanes the lanes to include in the graph * @param bins a list of bins in the gel, used so the graph could switch * to another bin. */ public void init(Bin bin, DataList lanes, DataList bins) { // make sure the size is still set. setBounds(0, 0, width, height); // update the lists since the gel may have changed this.lanes = lanes; this.bins = bins; currentBin = bin; if(bin != null) { maxSize = bin.getLocation() + bin.getRange(); minSize = bin.getLocation() - bin.getRange(); } else { maxSize = -1; minSize = 0; } //=================== find the max intensity========= maxIntensity = 0; double tempIntensity; for(int i=0; i < lanes.size(); i++) { tempIntensity = ((Lane) lanes.dataAt(i)).getMaxHeight(minSize, maxSize); if(tempIntensity > maxIntensity) maxIntensity = tempIntensity; } //=================== find the scale================ scale = (height - v_inset - BOTTOM_BORDER - 1)/maxIntensity; //=================== set the tick increment======== if(maxIntensity >= 2000) tickInc = 1000; else if(maxIntensity >= 1000) tickInc = 500; else tickInc = 100; slider.setScale(scale); slider.setSize(minSize); slider.refresh(); // fix anything that has multiple cutoffs in the bin by taking the // largest one and applying it to the bin. Cutoff ct; Lane ln; for(int i=0; i < lanes.size(); i++) { ln = (Lane) lanes.dataAt(i); ct = ln.cutoffUnder(maxSize); // Look out for errors, like MaxSize = -1, if no bin, so // ct will be null. In that case, don't worryabout this stuff if( (ct != null) && (ct.getStartPos() > minSize)) { // make this cutoff cover the whole bin. // first delete anything between it and the edge of the bin double pos; Cutoff temp = ln.cutoffUnder(ct.getStartPos() - 0.00000001); if(temp != null) pos = temp.getStartPos(); else pos = -1; while(pos > minSize) { ln.getCutoffs().removeElement(temp); // find the next one, subtract a little number to avoid // equailty temp = ln.cutoffUnder(pos - 0.00000001); if(temp != null) pos = temp.getStartPos(); else pos = -1; } //Now adjust the cutoff. ct.setStartPos(minSize); } } // set the slider stuff // if we don't have any lanes, then we won't be displaying anything // anyway, so don't worry about that case. if(lanes.size() > 0) { slider.setCutoff( ((Lane)lanes.dataAt(0)).cutoffUnder(maxSize)); slider.setLanes(lanes); } //=====score the bin initially================ // see if it is neccessary first, but check to see that it exists first // It is possible that no bin is selected, if(currentBin != null) { if(!currentBin.isScored()) try{ currentBin.score(lanes); } catch(ScoringFailure error) { handleScoreError(error); } // set the info label infoLabel.setText(currentBin.getScoreInfo()[0]); } } /** * Gives the height of the graph display area, including borders. * * @return the height */ public int getHeight() { return height; } /** * Sets the height of the graph display area, including borders, to * the specified value. * * @param height the new height for the view */ public void setHeight(int height) { this.height = height; setBounds(0, 0, width, height); } /** * Gives the width of the graph display area, including borders. * * @return the width */ public int getWidth() { return width; } /** * Sets the width of the graph display area, including borders, to * the specified value. * * @param width the new width */ public void setWidth(int width) { this.width = width; setBounds(0, 0, width, height); } /** * Gets the width of the graph, which does not include borders. This * is the value that will be passed to the graph drawing object. * * @return the width * * @see Graph */ public int getGraphWidth() { return graph_width; } /** * Sets the width of the actual graph, which does not include the border. * This is the value that will be passed to the graph drawing object. * * @param width the new width for the graph * * @see Graph */ public void setGraphWidth(int width) { graph_width = width; } /** * Gives the bin that was used to produce the graph. * * @return the bin currently displayed as a graph */ public Bin getBin() { return currentBin; } /** * Draws the graph. It will actually draw the axis and the tick * marks, as well as the cutoffs, but the actual graph is * drawn by the Graph object selected. The display is * normally double buffered, but double-buffering is disabled if * the method is drawing to a printed page. */ public void paint(Graphics g) { //=======================Determine the size=============== int temp = graph[graphIndex].getPreferredWidth(); int fullWidth = width - h_inset - H_SPACE; if(temp == -1) graph_width = fullWidth; else if(temp <= fullWidth) graph_width = temp; else { // decide if we should resize or not? graph_width = fullWidth; } if( !(g instanceof PrintGraphics)) { //=======================Off screen buffer setup========== // make sure we have the offscreen buffer and that it is the right // size. Dimension d = getSize(); if ( (offGraphics == null) || (d.width != offDimension.width) || (d.height != offDimension.height) ) { offDimension = d; offImage = createImage(d.width, d.height); offGraphics = offImage.getGraphics(); } } else { // don't need to double buffer the printer offGraphics = g; } //=================== find the scale================ // can change when we go to the printer of come back from it scale = (height - v_inset - BOTTOM_BORDER - 1)/maxIntensity; // make sure we have a bin. If not, we don't have anything to display. // Draw to the graphics screen and then bail. don't use the double // buffer. if(currentBin == null) { slider.setVisible(false); g.clearRect(0, 0, width, height); g.drawString("No Bin Selected. Graph unavailable.", LABEL_HORZ, LABEL_VERT); return; // bail! } // ============== Normal drawing =================== // clear the screen offGraphics.clearRect(0, 0, width, height); slider.setVisible(true); // set some of the stuff int x = h_inset + H_SPACE; int h = height - BOTTOM_BORDER; // Draw the axis offGraphics.setColor(Color.black); offGraphics.drawLine(x, v_inset, x, h); offGraphics.drawLine(x, h, x + graph_width, h); //================== Draw Tick marks ===================== int y; int tickLoc = (int) tickInc; double maxHeight = (h-v_inset - 1)/scale; while(tickLoc < maxHeight) { // Find the location for the tick y = h - (int) (scale*tickLoc); offGraphics.drawLine(x - TICK_WIDTH, y, x, y); offGraphics.drawString("" + tickLoc, x - TICK_L_WIDTH, y + 5); tickLoc += (int) tickInc; } // ==================Draw the graph========================= // Call whichever graphing method is selected. graph[graphIndex].drawGraph(offGraphics, x, v_inset, graph_width, h - v_inset, BOTTOM_BORDER, scale, minSize, maxSize, lanes); // ================= Draw the Cutoffs ======================= // use the first lane for the whole graph, and extend it // straight across the graph. offGraphics.setColor(Color.lightGray); Cutoff ct = ((Lane)lanes.dataAt(0)).cutoffUnder(maxSize); // see if we only have one cutoff. if(ct.getStartPos() > minSize) System.err.println("Cutoff does not span bin!"); int numLevels = ct.getNumLevels(); int intensity; for(int i=0; i < numLevels; i++) { // Just draw lines, since the graph doesn't really have a // horizontal scale for the different places in the bin. intensity = h - 1 - (int) (scale * ct.getCutoff(minSize, i)); offGraphics.drawLine(x + 1, intensity, x + graph_width, intensity); } if(!(g instanceof PrintGraphics)) g.drawImage(offImage, 0, 0, this); } /** * Changes the graph to the one selected in the choice box. */ public void itemStateChanged(ItemEvent e) { if(e.getSource() == graphChoice) { graphIndex = graphChoice.getSelectedIndex(); refresh(); } } /** * Handles the buttons in the button bar. */ public void actionPerformed(ActionEvent e) { if(e.getSource() == previousButton) { // find the location of this bin in the list int location = bins.find(currentBin.getSearchKey()).location; // now move to the previous one in the list if(location <= 0) infoLabel.setText("Already on first bin."); else { currentBin = (Bin) bins.dataAt(location - 1); init(currentBin, lanes, bins); infoLabel.setText("Bin number " + (location)); refresh(); } } else if(e.getSource() == nextButton) { // find the location of this bin in the list int location = bins.find(currentBin.getSearchKey()).location; // now move to the previous one in the list if(location == (bins.size() - 1)) infoLabel.setText("Already on last bin."); else { currentBin = (Bin) bins.dataAt(location + 1); init(currentBin, lanes, bins); infoLabel.setText("Bin number " + (location + 2)); refresh(); } } else if(e.getSource() == addCutoffButton) { // Make a new function, set at half height CutoffFunction newFunction; newFunction = (CutoffFunction) FeatureList.getCutoffMgr().getDefault(); newFunction = (CutoffFunction) newFunction.clone(); Option opts[] = newFunction.getOptions(); opts[0].setValue(maxIntensity/2); newFunction.setOptions(opts); slider.addCutoffFunction(newFunction); // rescore the bin try{ currentBin.score(lanes); } catch(ScoringFailure error) { handleScoreError(error); } refresh(); } else if(e.getSource() == cutoffDialogButton) { ctDialog.init(((Lane) lanes.dataAt(0)).cutoffUnder(maxSize), lanes, minSize); ctDialog.setVisible(true); slider.setCutoff(((Lane) lanes.dataAt(0)).cutoffUnder(maxSize)); slider.refresh(); refresh(); } } /** * Called when the mouse is released. It will rescore a bin if the * mouse is released on the slider. */ public void mouseReleased(MouseEvent e) { if(e.getSource() == slider) { try{ currentBin.score(lanes); } catch(ScoringFailure error) { handleScoreError(error); } refresh(); infoLabel.setText(currentBin.getScoreInfo()[0]); } } /** * Called when the mouse is dragged. It will refresh the display if * the event originated with the cutoff slider. */ public void mouseDragged(MouseEvent e) { if(e.getSource() == slider) refresh(); } /** * Called to update the screen by Java. It simply calls paint. */ public void update(Graphics g) { paint(g); } /** * Gives the ButtonBar associated with the graph. * * @return the button bar. */ public ButtonBar getButtonBar() { return buttonBar; } /** * Shows an options dialog if an error occurs when scoring a bin. * The most likely reason is that the number of cutoffs do not match * the number expected by the scoring function. The options are used * to adjust the scoring function. * * @param error the orignal failure */ protected void handleScoreError(ScoringFailure error) { ErrorDialog errorDialog = new ErrorDialog(topWindow); errorDialog.showError(error); // try setting the number of levels for the scoring method ScoreFunction scoreFn = currentBin.getScoreMethod(); Option opts[] = scoreFn.getOptions(); // find out the number of levels we have, try using the first lane. Cutoff ct = new Cutoff(minSize, 2); if(!lanes.isEmpty()) ct = ((Lane) lanes.dataAt(0)).cutoffUnder(maxSize); // see if the first option is a number if(opts[0].getType() != Option.NUMBER) throw new IllegalArgumentException("Error in scoring method " + scoreFn.getName() + ". First " + "option should be number of levels" + " expected."); opts[0].setValue((double) ct.getNumLevels() + 1); scoreFn.setOptions(opts); // show the options again. OptionDialog optD = new OptionDialog(scoreFn.getOptions(), topWindow, scoreFn.getName() + " Parameters"); optD.setVisible(true); if(!optD.isCanceled()) { opts = optD.getOptions(); scoreFn.setOptions(opts); } } /** * Lays out the components on the button bar, uses the constants declared * in this class to control the layout. */ private void createButtonBar() { buttonBar = new ButtonBar(); buttonBar.setBounds(0, 0, 640, 32); // Let the parent window handle the action event for the orignal // button bar components buttonBar.sendActionEventsTo( (ActionListener) topWindow); // ===========Create the graph choice================= // already created in constructor, initiallized with graph types buttonBar.add(graphChoice); graphChoice.addItemListener(this); int startX = buttonBar.getFreeHorzPos(); graphChoice.setBounds(startX + HORZ_SPACE, ButtonBar.VERT_INSET, GRAPH_CHOICE_WIDTH, ButtonBar.BUTTON_HEIGHT); startX += GRAPH_CHOICE_WIDTH + HORZ_SPACE; //===========Create the previous button================ previousButton = new ImgButton(ButtonBar.retrieveImage(ProgOptions.homePath + REL_PATH + PREVIOUS_FILE)); buttonBar.add(previousButton); previousButton.setBounds(startX + HORZ_SPACE, ButtonBar.VERT_INSET, ButtonBar.BUTTON_WIDTH, ButtonBar.BUTTON_HEIGHT); startX += ButtonBar.BUTTON_WIDTH + HORZ_SPACE; previousButton.addActionListener(this); //===========Create the next button==================== nextButton = new ImgButton(ButtonBar.retrieveImage(ProgOptions.homePath + REL_PATH + NEXT_FILE)); buttonBar.add(nextButton); nextButton.setBounds(startX, ButtonBar.VERT_INSET, ButtonBar.BUTTON_WIDTH, ButtonBar.BUTTON_HEIGHT); startX += ButtonBar.BUTTON_WIDTH; nextButton.addActionListener(this); //===========Create the addCutoff button==================== String imgFileLoc = ProgOptions.homePath + REL_PATH + ADDCT_FILE; addCutoffButton = new ImgButton(ButtonBar.retrieveImage(imgFileLoc)); buttonBar.add(addCutoffButton); addCutoffButton.setBounds(startX + HORZ_SPACE, ButtonBar.VERT_INSET, ButtonBar.BUTTON_WIDTH, ButtonBar.BUTTON_HEIGHT); startX += ButtonBar.BUTTON_WIDTH + HORZ_SPACE; addCutoffButton.addActionListener(this); //===========Create the cutoff dialog button=============== cutoffDialogButton = new ImgButton(ButtonBar.retrieveImage(ProgOptions.homePath + REL_PATH + CTDIALOG_FILE)); buttonBar.add(cutoffDialogButton); cutoffDialogButton.setBounds(startX + HORZ_SPACE, ButtonBar.VERT_INSET, ButtonBar.BUTTON_WIDTH, ButtonBar.BUTTON_HEIGHT); startX += ButtonBar.BUTTON_WIDTH + HORZ_SPACE; cutoffDialogButton.addActionListener(this); } /** * Returns a bar that displays information about the graph. This object * should be displayed somewhere by the container of this object. * * @return the bar as described above */ public Bar getInfoBar() { return infoBar; } /** * Lays out the components for the info bar. Layout is controlled with * constants in this class. */ private void createInfoBar() { // create the label infoLabel = new Label("Graph info..."); // create the bar, no bottom border. infoBar = new Bar(true, false); infoBar.setLayout(null); infoBar.add(infoLabel); infoLabel.setBounds(LABEL_H_INSET, LABEL_V_INSET, LABEL_WIDTH, LABEL_HEIGHT); infoBar.setBounds(0, 0, 600, FragmentMap.BAR_HEIGHT); } /** * Updates the display so that it matches the data. */ public void refresh() { repaint(); } //=================Unused methods to satisfy intefaces================= /**Unused*/ public void mouseClicked(MouseEvent e) {} /**Unused*/ public void mousePressed(MouseEvent e) {} /**Unused*/ public void mouseMoved(MouseEvent e) {} /**Unused*/ public void mouseEntered(MouseEvent e){} /**Unused*/ public void mouseExited(MouseEvent e){} }