/*
* Copyright (c) 2009 The Jackson Laboratory
*
* This software was developed by Gary Churchill's Lab at The Jackson
* Laboratory (see http://research.jax.org/faculty/churchill).
*
* This 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, either version 3 of the License, or
* (at your option) any later version.
*
* This software 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 software. If not, see <http://www.gnu.org/licenses/>.
*/
package org.jax.qtl.graph;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.font.FontRenderContext;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.util.HashSet;
import java.util.Set;
import javax.swing.JPanel;
import org.jax.qtl.Constants;
import org.jax.qtl.action.GenoDataSelectionChangeEvent;
import org.jax.qtl.action.GenoDataSelectionChangeListener;
import org.jax.qtl.cross.GeneticMarker;
import org.jax.qtl.util.NonRepeatVector;
import org.jax.util.ObjectUtil;
/**
* <p>Title: OneDemensionalPlot</p>
*
* <p>Description: This is a base class for all one dimensional plots. It has
* the selection box, basic dot class, xLabel, yLabel, title </p>
*
* <p>Company: The Jackson Laboratory</p>
*
* @author Lei Wu
* @version 1.0
*/
@SuppressWarnings("all")
public abstract class OneDimensionPlot extends JPanel implements MouseMotionListener, MouseListener, GenoDataSelectionChangeListener, Constants{
/**
* every {@link java.io.Serializable} is supposed to have one of these
*/
private static final long serialVersionUID = 2975206803854712333L;
private String title="", xLabel="", yLabel="";
Graphics2D big;
NonRepeatVector selectedDots = new NonRepeatVector(); // vector of all selected Dots on this plot
Set<Dot> allDots = new HashSet<Dot>(); // vector of all Dots with lod score and related GeneticMap on this plot
BufferedImage bi;
int x = -1, y = -1, width = 0, height = 0, leftx = -1, lefty = -1; // selection box location and size
final int DEFAULT_DOT_SIZE = 1;
int distToBorder = 60, distToAxis = 5, tickHeight = 5, dotSize = this.DEFAULT_DOT_SIZE;
int yTickLength = 4, labelToTick = 2;
Insets inset = new Insets(this.distToBorder - 10, this.distToBorder + 10, this.distToBorder, this.distToBorder - 20);
Insets plotInset = new Insets(this.distToAxis, this.distToAxis, this.distToAxis + this.tickHeight, this.distToAxis);
int plotWidth, plotHeight;
Font labelFont = new Font("SansSerif", Font.BOLD, 12);
Font titleFont = new Font("SansSerif", Font.BOLD, 14);
Font tickLabelFont = new Font("SansSerif", Font.PLAIN, 11);
Color normalColor = Color.black, highlightColor = Color.red, lineColor = Color.black;
Color selectionBoxColor = new Color(0,0,0,5);
Stroke lodLinetype = new BasicStroke(2f), normalLinetype = new BasicStroke(1f);
// cursor types
Cursor plotCursor = new Cursor(Cursor.CROSSHAIR_CURSOR);
Cursor normalCursor = new Cursor(Cursor.DEFAULT_CURSOR);
Cursor handCursor = new Cursor(Cursor.HAND_CURSOR);
boolean drawOutlineBox = true;
FontRenderContext context;
protected volatile boolean graphRasterNeedsRepaint = true;
OneDimensionPlot() {
// ToolTipManager.sharedInstance().setInitialDelay(0);
// add mouse listeners
this.addMouseListener(this);
this.addMouseMotionListener(this);
this.addComponentListener(new ComponentListener()
{
public void componentHidden(ComponentEvent e)
{
}
public void componentMoved(ComponentEvent e)
{
}
public void componentResized(ComponentEvent e)
{
OneDimensionPlot.this.graphRasterNeedsRepaint = true;
repaint();
}
public void componentShown(ComponentEvent e)
{
}
});
}
/**
* Getter for the x label
* @return the xLabel
*/
public String getXLabel()
{
return this.xLabel;
}
/**
* Setter for the x label
* @param label
* the new label
*/
public void setXlabel(String label)
{
this.xLabel = label;
this.graphRasterNeedsRepaint = true;
this.repaint();
}
/**
* Getter for the y label
* @return the yLabel
*/
public String getYLabel()
{
return this.yLabel;
}
/**
* Setter for the Y label
* @param label
* the new y label
*/
public void setYlabel(String label)
{
this.yLabel = label;
this.graphRasterNeedsRepaint = true;
this.repaint();
}
/**
* Getter for the title
* @return the title
*/
public String getTitle()
{
return this.title;
}
/**
* Setter for the title
* @param title
* the new title
*/
public void setTitle(String title)
{
this.title = title;
this.graphRasterNeedsRepaint = true;
this.repaint();
}
public void paintComponent(Graphics g) {
// plot the data
if(this.graphRasterNeedsRepaint)
{
setupPlot();
this.context = this.big.getFontRenderContext();
this.graphRasterNeedsRepaint = false;
plot();
drawTitle();
drawXlabel();
drawYlabel();
drawSelectionBox();
}
// draw to screen
Graphics2D g2 = (Graphics2D) g;
g2.drawImage(this.bi, 0, 0, getWidth(), getHeight(), null);
}
// this function need to be override, otherwise, nothing will be drawn
abstract void plot();
public void setSize(Dimension size) {
super.setSize(size);
}
void drawXlabel() {
this.big.setFont(this.labelFont);
this.big.setColor(this.normalColor);
FontRenderContext context = this.big.getFontRenderContext();
Rectangle2D labelBounds = this.labelFont.getStringBounds(this.xLabel, context);
// draw x label
int labelWidth = (int)labelBounds.getWidth();
int labelHeight = (int)labelBounds.getHeight();
int labelStartX = (this.plotWidth - labelWidth) / 2 + this.inset.left;
int labelStartY = this.inset.top + this.plotHeight + (this.inset.bottom + labelHeight)/2;
this.big.drawString(this.xLabel, labelStartX, labelStartY);
}
void drawTitle() {
this.big.setFont(this.titleFont);
this.big.setColor(this.normalColor);
FontRenderContext context = this.big.getFontRenderContext();
Rectangle2D labelBounds = this.titleFont.getStringBounds(this.title, context);
// draw x label
int labelWidth = (int)labelBounds.getWidth();
int labelHeight = (int)labelBounds.getHeight();
int labelStartX = (this.plotWidth - labelWidth)/2 + this.inset.left;
int labelStartY = (this.inset.top + labelHeight)/2;
this.big.drawString(this.title, labelStartX, labelStartY);
}
void drawYlabel() {
// set the font and size
this.big.setFont(this.labelFont);
this.big.setColor(this.normalColor);
// center the label
FontRenderContext context = this.big.getFontRenderContext();
Rectangle2D labelBounds = this.labelFont.getStringBounds(this.yLabel, context);
double labelHeight = labelBounds.getHeight();
double labelWidth = labelBounds.getWidth();
int labelStartX = (int) (this.inset.left + labelHeight) / 3;
int labelStartY = this.inset.top + (int) ((this.plotHeight + labelWidth) / 2);
// clockwise 270 degrees
this.big.rotate(-Math.PI / 2, labelStartX, labelStartY);
// draw label
this.big.drawString(this.yLabel, labelStartX, labelStartY);
this.big.rotate(Math.PI / 2, labelStartX, labelStartY);
}
void drawYtickAndLabel(double yscaler, double loc, double startingPos) {
drawYtickAndLabel(yscaler, loc, loc+"", startingPos);
}
// loc should be larger than startingPos
void drawYtickAndLabel(double yscaler, double loc, String label, double startingPos){
double yaxisy = this.inset.top + this.plotHeight - this.plotInset.bottom - (loc - startingPos) * yscaler;
//System.out.println("yaxisy="+yaxisy + " inset.top+plotHeight-plotInset.bottom=" + (inset.top + plotHeight - plotInset.bottom));
this.big.drawLine(this.inset.left - this.yTickLength, (int)yaxisy, this.inset.left, (int)yaxisy);
// find the right place to start to draw labels on x axis
FontRenderContext context = this.big.getFontRenderContext();
Rectangle2D labelBounds = this.tickLabelFont.getStringBounds(label, context);
double labelWidth = labelBounds.getWidth();
double labelHeight = labelBounds.getHeight();
float yaxisLabelStartX = this.inset.left - (float) labelWidth - this.yTickLength - this.labelToTick;
float yaxisLabelStartY = (float)(yaxisy + labelHeight/2 - 1); // -1 is only for looks better
this.big.setColor(this.lineColor);
this.big.setFont(this.tickLabelFont);
this.big.drawString(label, yaxisLabelStartX, yaxisLabelStartY);
}
// setup the plot with initial outline and ticks, labels
private void setupPlot() {
this.bi = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_4BYTE_ABGR);
this.big = this.bi.createGraphics();
// fill the background with white
this.big.setColor(Color.white);
this.big.fill(new Rectangle2D.Double(0,0,getWidth(), getHeight()));
this.big.setColor(this.lineColor);
int oldPlotWidth = this.plotWidth, oldPlotHeight = this.plotHeight;
this.plotWidth = getWidth() - (this.inset.right + this.inset.left);
this.plotHeight = getHeight() - (this.inset.bottom + this.inset.top);
// remove all selection box if the size of plot changed
if ( (oldPlotWidth != this.plotWidth) || (oldPlotHeight != this.plotHeight)) {
removeSelectionBox();
deSelectPoints();
}
// draw outline box
if (this.drawOutlineBox)
this.big.drawRect(this.inset.left, this.inset.top, this.plotWidth, this.plotHeight);
//
this.allDots = new HashSet<Dot>();
}
/**
* draw the user selection box
*/
private void drawSelectionBox() {
// draw selection box
Rectangle2D selectionBox = new Rectangle2D.Double(this.leftx, this.lefty, this.width, this.height);
this.big.setColor(Color.gray);
this.big.draw(selectionBox);
this.big.setColor(this.selectionBoxColor);
this.big.fill(selectionBox);
}
/**
* {@inheritDoc}
*/
public void mousePressed(MouseEvent e) {
this.x = e.getX();
this.y = e.getY();
removeSelectionBox();
repaint();
}
/**
* {@inheritDoc}
*/
public void mouseReleased(MouseEvent e) {
// remove the tiny spot on the graph if nothing selected
if (this.width == 0 && this.height == 0) {
deSelectPoints();
}
else {
this.selectedDots = new NonRepeatVector();
Rectangle2D selectionBox = new Rectangle2D.Double(this.leftx, this.lefty, this.width, this.height);
for(Dot dot: this.allDots)
{
if (selectionBox.contains(dot.shape.getBounds2D())) {
dot.setSelected(true);
this.selectedDots.add(dot);
System.out.println("selectedDot width=" + dot.shape.getBounds2D().getWidth());
}
else {
dot.setSelected(false);
}
}
System.out.println("numSelectedDots=" + this.selectedDots.size());
}
repaint();
}
private void removeSelectionBox() {
this.width = 0;
this.height = 0;
}
private void deSelectPoints() {
this.leftx = -1;
this.lefty = -1;
int numSelectedDots = this.selectedDots.size();
for (int i = 0; i < numSelectedDots; i++)
( (Dot) this.selectedDots.elementAt(i)).setSelected(false);
}
/**
* {@inheritDoc}
*/
public void mouseExited(MouseEvent e) {}
/**
* {@inheritDoc}
*/
public void mouseEntered(MouseEvent e) {}
/**
* {@inheritDoc}
*/
public void mouseClicked(MouseEvent e) {}
/**
* {@inheritDoc}
*/
public void mouseMoved(MouseEvent e) {}
/**
* {@inheritDoc}
*/
public void mouseDragged(MouseEvent e) {
// for selection box
int curX = e.getX();
int curY = e.getY();
this.width = Math.abs(this.x - curX);
this.height = Math.abs(this.y - curY);
if (curX < this.x) this.leftx = curX;
else this.leftx = this.x;
if (curY < this.y) this.lefty = curY;
else this.lefty = this.y;
repaint();
}
/**
* {@inheritDoc}
*/
public void GenoDataSelectionChanged(GenoDataSelectionChangeEvent e) {
repaint();
}
class Dot
{
private final GeneticMarker marker1;
private final GeneticMarker marker2;
private Shape shape;
/**
* Constructor for 1 marker dot (the second marker is set to null)
* @param marker1
* the 1st marker
* @param shape
* the shape
*/
public Dot(
GeneticMarker marker1,
Shape shape)
{
this.marker1 = marker1;
this.marker2 = null;
this.shape = shape;
}
/**
* Constructor for 2 marker dot
* @param marker1
* the 1st marker
* @param marker2
* the 2nd marker
* @param shape
* the shape
*/
public Dot(
GeneticMarker marker1,
GeneticMarker marker2,
Shape shape)
{
this.marker1 = marker1;
this.marker2 = marker2;
this.shape = shape;
}
void setSelected(boolean selected) {
// TODO add back selection
// int numMaps = map.length;
// for (int i=0; i<numMaps; i++) {
// map[i].setSelected(selected);
// }
}
/**
* The 1st marker
* @return
* the 1st marker
*/
public GeneticMarker getMarker1()
{
return this.marker1;
}
/**
* The 2nd marker
* @return
* the 2nd marker
*/
public GeneticMarker getMarker2()
{
return this.marker2;
}
/**
* Getter for the shape of this dot
* @return
* the shape
*/
public Shape getShape()
{
return this.shape;
}
/**
* {@inheritDoc}
*/
@Override
public int hashCode()
{
return ObjectUtil.hashObject(this.shape) +
ObjectUtil.hashObject(this.marker1);
}
/**
* {@inheritDoc}
*/
@Override
public boolean equals(Object obj)
{
if(obj instanceof Dot)
{
Dot otherDot = (Dot)obj;
return ObjectUtil.areEqual(this.marker1, otherDot.marker1) &&
ObjectUtil.areEqual(this.marker2, otherDot.marker2) &&
ObjectUtil.areEqual(this.shape, otherDot.shape);
}
else
{
return false;
}
}
}
}