package com.github.lindenb.jvarkit.tools.sigframe;
import htsjdk.samtools.SAMSequenceDictionary;
import htsjdk.samtools.SAMSequenceRecord;
import htsjdk.samtools.util.CloserUtil;
import htsjdk.variant.utils.SAMSequenceDictionaryExtractor;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.LinearGradientPaint;
import java.awt.Paint;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.AdjustmentEvent;
import java.awt.event.AdjustmentListener;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.Vector;
import java.util.regex.Pattern;
import javax.swing.AbstractAction;
import javax.swing.ActionMap;
import javax.swing.ButtonGroup;
import javax.swing.JButton;
import javax.swing.JDesktopPane;
import javax.swing.JDialog;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JInternalFrame;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollBar;
import javax.swing.JScrollPane;
import javax.swing.JSeparator;
import javax.swing.JSpinner;
import javax.swing.JTable;
import javax.swing.JToggleButton;
import javax.swing.JToolBar;
import javax.swing.SpinnerNumberModel;
import javax.swing.SwingUtilities;
import javax.swing.border.EmptyBorder;
import javax.swing.event.InternalFrameAdapter;
import javax.swing.event.InternalFrameEvent;
import javax.swing.filechooser.FileFilter;
import com.beust.jcommander.Parameter;
import com.github.lindenb.jvarkit.util.Hershey;
import com.github.lindenb.jvarkit.util.jcommander.Launcher;
import com.github.lindenb.jvarkit.util.jcommander.Program;
import com.github.lindenb.jvarkit.util.log.Logger;
import com.github.lindenb.jvarkit.util.swing.AbstractGenericTable;
import com.github.lindenb.jvarkit.util.tabix.AbstractTabixObjectReader;
import com.github.lindenb.jvarkit.util.tabix.TabixFileReader;
/**
BEGIN_DOC
##Input file:
tab delimited text file, compressed with **tabix/bgzip** and indexed with **tabix/tabix.**
Columns are:
* chrom (string)
* start (int)
* end (int)
* value (double)
* color (optional: 3 comma-separated integers [0-255] for R,G,B )
```tsv
chrM 0 10 0.005661 120,120,120
chrM 0 10 0.114468
chrM 0 10 -0.877466
chrM 0 10 1.456670 120,220,120
chrM 0 10 -1.720711
chrM 1 11 -1.427848
chrM 1 11 -1.433984
chrM 1 11 1.891983
chrM 1 11 2.255700
```
## Synopsis
```
$ java -jar dist/sigframe.jar
```
or start from the command-line
```
$ java -jar dist/sigframe.jar -R ref.fasta tabix1.gz tabix2.gz tabix3.gz
```
END_DOC
*/
@SuppressWarnings("serial")
public class SigFrame
extends JFrame
{
private static final Logger LOG=Logger.build(SigFrame.class).make();
/** desktop */
private JDesktopPane desktop;
/** map string to action */
private ActionMap actionMap=new ActionMap();
/** reference genome */
private SAMSequenceDictionary genome=null;
/** about message */
private String aboutMessage="<h2 align='center'>Pierre Lindenbaum PhD @yokofakun</h2>";
/** tool */
private enum Tool
{
ZOOM,
SHOW_DATA_FOR_FILE,
CHANGE_HEIGHT
};
/** a chrom/start/end/value */
static private class SigData
{
String name;
double value=0;
String chrom;
int start;
int end;
String color_string;
private Color _color;
public int getStart() { return start;}
public int getEnd() { return end;}
public String getChromosome() { return chrom;}
public String getName() {return name;}
public double getValue() { return value;}
public Color getColor()
{
if(_color==null)
{
_color=Color.BLACK;
if(color_string!=null)
{
String tokens[]=color_string.split("[,]");
_color=new Color(
Integer.parseInt(tokens[0]),
Integer.parseInt(tokens[1]),
Integer.parseInt(tokens[2])
);
}
}
return _color;
}
}
/** a vector of signalfiles */
class SigFiles extends Vector<SigDataFileTabixReader>
{
}
/** A wrapper around a file indexed with tabix */
class SigDataFileTabixReader extends AbstractTabixObjectReader<SigData>
{
private File file;
SigDataFileTabixReader(final File file ) throws IOException
{
super(file.getPath());
this.file=file;
}
public File getFile()
{
return file;
}
public boolean hasDataForChrom(final SAMSequenceRecord rec)
{
return iterator(rec.getSequenceName(), 1, 1+rec.getSequenceLength()).hasNext();
}
@Override
protected Iterator<SigData> iterator(Iterator<String> delegate)
{
return new MyIterator(delegate);
}
/** convert line to SigData */
private class MyIterator
extends AbstractMyIterator
{
private final Pattern tab=Pattern.compile("[\t]");
MyIterator(final Iterator<String> delegate)
{
super(delegate);
}
@Override
public SigData next()
{
if(!hasNext()) throw new IllegalStateException();
final String tokens[]=this.tab.split(super.delegate.next());
final SigData d=new SigData();
d.chrom=tokens[0];
d.start=Integer.parseInt(tokens[1]);
d.end=Integer.parseInt(tokens[2]);
d.value=Double.parseDouble(tokens[3]);
if(tokens.length>4) d.color_string=tokens[4];
return d;
}
}
}
/**
* LoadAtStartup
*
*/
private static class LoadAtStartup
extends WindowAdapter
{
private Set<File> sources = new HashSet<File>();
private SigFrame frame;
LoadAtStartup(SigFrame frame)
{
this.frame=frame;
}
@Override
public void windowOpened(WindowEvent e)
{
frame.removeWindowListener(this);
if(sources.isEmpty()) return;
frame.openTabixFiles(sources);
}
}
/**
* IFrame
*
*/
private class IFrame extends JInternalFrame
{
protected IFrame()
{
super("iframe",true,true,true,true);
JPanel content=new JPanel(new BorderLayout(2,2));
content.setBorder(new EmptyBorder(2, 2, 2, 2));
setContentPane(content);
this.addInternalFrameListener(new InternalFrameAdapter()
{
@Override
public void internalFrameOpened(InternalFrameEvent e)
{
IFrame.this.removeInternalFrameListener(this);
onOpen();
}
});
}
void onOpen()
{
final float ratio=0.9f;
final Dimension dim= SigFrame.this.desktop.getSize();
final Dimension d2=new Dimension(dim);
d2.width=(int)(ratio*dim.width);
d2.height=(int)(ratio*dim.height);
this.setBounds(
(int)(Math.random()*(dim.width-d2.width)),
(int)(Math.random()*(dim.height-d2.height)),
d2.width, d2.height);
}
}
/**
* Browser: a browser is an iframe containing
* multiple chromView, each chromView contains a file(tabix) view
*
*/
private class Browser extends IFrame
implements MouseListener,MouseMotionListener
{
private final int TOP_DISTANCE=100;
private final int LEFT_DISTANCE=50;
private final int BORDER_SIZE=5;
private final int DEFAULT_FILE_VIEW_HEIGHT=150;
private JScrollBar hScrollBar;
private JScrollBar vScrollBar;
private JPanel drawingArea;
private BufferedImage offscreen=null;
private Point mouseStart=null;
private Point mousePrev=null;
private int max_chrom_length=0;
private int view_start=0;
private int view_length=0;
private int position_in_history=0;
private List<History> history=new ArrayList<History>();
private List<ChromView> chromViews=new ArrayList<ChromView>();
private Tool currentTool=Tool.ZOOM;
private ChromView selectedChromView=null;
private ChromView.FileView selectedFileFiew=null;
private SAMSequenceDictionary samSequenceDictionary;
/** all tabix files associated to this reader */
private SigFiles sigilesList;
/** select tool */
private class SelectToolAction
extends AbstractAction
{
SelectToolAction(Tool tool)
{
super(tool.name());
this.putValue("sigframe.tool", tool);
}
@Override
public void actionPerformed(ActionEvent e)
{
currentTool=(Tool)this.getValue("sigframe.tool");
}
}
/** browser history */
private class History
{
int view_start=0;
int view_length=0;
History(int start,int length)
{
this.view_start=start;
this.view_length=length;
}
}
/** Graphics Context */
private class GC
{
int step=0;
Graphics2D g;
int top_y;
int width;
int height;
GC()
{
top_y=vScrollBar.getValue();
width=drawingArea.getWidth();
height=drawingArea.getHeight();
}
}
/** abstract view: offsets Y for the view */
private abstract class View
{
int y_pixels=0;
int height_pixels=0;
public int getMaxY()
{
return getTopY()+getHeight();
}
public int getTopY()
{
return this.y_pixels;
}
public int getHeight()
{
return this.height_pixels;
}
public int getTopY(GC gc)
{
return this.getTopY()+topMargin()-gc.top_y;
}
}
/**
* ChromView
*/
private class ChromView
extends View
{
/** background color for this view */
private Color background=Color.WHITE;
/** chromosome for this view */
private SAMSequenceRecord chrom;
/** one file view for each tabix */
private List<FileView> fileViews=new ArrayList<FileView>();
/** inside a chromosome view: one view per BED file */
class FileView
extends View
{
/** associated tabix file */
SigDataFileTabixReader tabixFile;
/** min value Y */
double min_value;
/** max value Y */
double max_value;
public FileView(SigDataFileTabixReader sigFile)
{
this.tabixFile=sigFile;
this.height_pixels=DEFAULT_FILE_VIEW_HEIGHT;
this.min_value=-1.0;//sigChrom.getMinValue();
this.max_value=1.0;//sigChrom.getMaxValue();
}
/** return owner */
public ChromView getChromView()
{
return ChromView.this;
}
/** return owner */
public Browser getBrowser()
{
return getChromView().getBrowser();
}
/** return associated chromosome */
public SAMSequenceRecord getChromosome()
{
return getChromView().getChromosome();
}
/** returns wether that fileview contains x,y */
public boolean contains(GC gc,int x,int y)
{
int left= leftMargin();
if(y<topMargin()) return false;
if(getTopY(gc)> gc.height) return false;
if(x <left) return false;
return new Rectangle(left, getTopY(gc), gc.width-left, getHeight()).contains(x,y);
}
/** fills a popup menu invoked in that menu */
public void fillPopup(JPopupMenu popup)
{
popup.add(new JSeparator());
popup.add(new AbstractAction("Adjust File Height....")
{
@Override
public void actionPerformed(ActionEvent e)
{
Integer h=getBrowser().askHeight();
if(h==null) return;
FileView.this.height_pixels=h;
getBrowser().recalcSize();
}});
popup.add(new AbstractAction("File Min/Max value....")
{
@Override
public void actionPerformed(ActionEvent e)
{
double values[]=askMinValue(min_value,max_value);
if(values==null) return;
FileView.this.min_value=values[0];
FileView.this.max_value=values[1];
getBrowser().offscreen=null;
getBrowser().drawingArea.repaint();
}});
}
/** paint that file view */
private void paint(GC gc)
{
if(this.getMaxY()< gc.top_y) return;
if(this.getTopY()>= (gc.top_y+(gc.height-topMargin()))) return;
Graphics2D g=gc.g;
Hershey hershey=new Hershey();
Shape clip=g.getClip();
//g.setClip(new Rectangle(leftMargin()/2,getTopY()-topMargin(),leftMargin()/2,getHeight()));
g.setFont(new Font("Courier",Font.PLAIN,9));
g.setColor(Color.BLACK);
AffineTransform old=g.getTransform();
AffineTransform tr= AffineTransform.getTranslateInstance(leftMargin()/2, getTopY()-gc.top_y+topMargin());
tr.rotate(Math.PI/2);
g.setTransform(tr);
hershey.paint(g, this.tabixFile.getFile().getName(), 0,0,this.getHeight(), leftMargin()/2);
//g.drawString(this.tabixFile.getFile().getName(),0,0);
g.setTransform(old);
g.setClip(clip);
g.setColor(Color.WHITE);
g.fillRect(
leftMargin(),
getTopY(),
gc.width-leftMargin(),
getHeight());
/* paint gradient */
for(int side=0;side<2;++side)
{
float y_0=value2pixel(0f);
float y_value2=value2pixel(2.0*(side==0?1.0:-1.0));
float fractions[]=new float[]{0.0f,1.0f};
if(y_value2==y_0) continue;
Color colors[]=new Color[]{Color.WHITE,new Color(255,210,210)};
LinearGradientPaint grad=new LinearGradientPaint(0,y_0,0,y_value2,fractions,colors);
Paint oldp= g.getPaint();
g.setPaint(grad);
Rectangle r=new Rectangle();
r.x=leftMargin();
r.width=gc.width-leftMargin();
if(side==0)
{
r.y=getTopY();
r.height=(int)(y_0-getTopY());
if(r.height<=0) continue;
}
else
{
r.y=(int)y_0;
r.height=(int)(getMaxY()-y_0);
if(r.y>= getMaxY() || r.height<=0) continue;
}
g.fill(r);
g.setPaint(oldp);
}
/* paint y axis */
if(this.min_value<=0 && this.max_value>=0)
{
g.setColor(Color.RED);
int y_axis=value2pixel(0);
g.drawLine(
leftMargin(),
y_axis,
gc.width,
y_axis);
}
/* paint data */
Iterator<SigData> iter=this.tabixFile.iterator(
ChromView.this.getChromosome().getSequenceName(),
(Browser.this.view_start+1),
1+Math.min(getChromosome().getSequenceLength(),Browser.this.view_start+Browser.this.view_length)
);
/* loop over data */
while(iter.hasNext())
{
SigData data=iter.next();
if(data.getEnd()< Browser.this.view_start) continue;
if(data.getStart()>(Browser.this.view_start+Browser.this.view_length)) continue;//break ?
if(data.getValue()< this.min_value || data.getValue()> this.max_value) continue;
g.setColor(data.getColor());
int x0= getBrowser().base2pixel(data.getStart(), gc);
int x1= getBrowser().base2pixel(data.getEnd(), gc);
if(x1==x0) x1++;
g.fillRect(
x0,
this.value2pixel(data.getValue()),
(x1-x0),
5);
}
//iter.close();
/* draw frame */
g.setColor(Color.GRAY);
g.drawRect(
leftMargin(),
getTopY(),
gc.width-leftMargin(),
getHeight());
}
/** convert value to Y -pixel*/
private int value2pixel(double value)
{
return this.getTopY()+
(int)(((this.max_value-value)/(this.max_value-this.min_value))*this.getHeight());
}
}
/** constructor for this chromosome */
ChromView(SAMSequenceRecord chrom )
{
this.chrom=chrom;
}
/** get associated browser */
public Browser getBrowser()
{
return Browser.this;
}
/** get chromosome */
public SAMSequenceRecord getChromosome()
{
return chrom;
}
/** return true wether this chromosome contains (x,y) */
public boolean contains(GC gc,int x,int y)
{
if(y< topMargin()) return false;
if(getTopY(gc)> gc.height) return false;
return new Rectangle(0, getTopY(gc), gc.width, getHeight()).contains(x,y);
}
/** fills a popup for this chromosome */
public void fillPopup(JPopupMenu popup)
{
popup.add(new JSeparator());
popup.add(new AbstractAction("Adjust Chromosome Height....")
{
@Override
public void actionPerformed(ActionEvent e)
{
Integer h=getBrowser().askHeight();
if(h==null) return;
for(FileView fv:fileViews)
{
fv.height_pixels=h;
}
getBrowser().recalcSize();
}});
}
/** paint this chrom view */
private void paint(GC gc)
{
if(this.getMaxY()< gc.top_y) return;
if(this.getTopY()>= (gc.top_y+(gc.height-topMargin()))) return;
Graphics2D g=gc.g;
g.translate(0, -gc.top_y);
g.setColor(background);
g.fillRect(0, getTopY(), gc.width, this.getHeight());
Hershey hershey=new Hershey();
Shape clip=g.getClip();
//g.setClip(new Rectangle(0,getTopY()-topMargin(),leftMargin()/2,getHeight()));
//g.setFont(new Font("Courier",Font.BOLD,14));
g.setColor(Color.BLACK);
AffineTransform old=g.getTransform();
AffineTransform tr= AffineTransform.getTranslateInstance(
leftMargin(),
getTopY()-gc.top_y+topMargin()
);
tr.rotate(Math.PI/2);
g.setTransform(tr);
//hershey.paint(g, getChromosome().getSequenceName(),0,0,this.getHeight()+100,100+leftMargin()/2);
//g.drawString(getChromosome().getSequenceName(),0,0);
hershey.paint(g, getChromosome().getSequenceName(),0,0,this.getHeight(),leftMargin()/2);
g.setTransform(old);
g.setClip(clip);
// paint all file views
for(FileView fv:this.fileViews)
{
fv.paint(gc);
}
g.translate(0, gc.top_y);
}
}
/** browser constuctor dictinary and set of tabix Files */
Browser(SAMSequenceDictionary dict,SigFiles sigilesList)
{
super();
this.samSequenceDictionary=dict;
this.sigilesList=sigilesList;
this.setTitle("Browser :"+this.sigilesList.size()+" file(s)");
this.setMinimumSize(new Dimension(leftMargin()+10,topMargin()+10));
JToolBar toolbar=new JToolBar();
this.getContentPane().add(toolbar,BorderLayout.NORTH);
JPanel pane=new JPanel(new BorderLayout());
this.getContentPane().add(pane,BorderLayout.CENTER);
this.addInternalFrameListener(new InternalFrameAdapter()
{
@Override
public void internalFrameClosed(InternalFrameEvent e)
{
for(SigDataFileTabixReader f: Browser.this.sigilesList) f.close();
Browser.this.doMenuClose();
}
});
this.addComponentListener(new ComponentAdapter()
{
@Override
public void componentResized(ComponentEvent e)
{
adjustScrollBars();
drawingArea.repaint();
}
});
this.vScrollBar=new JScrollBar(JScrollBar.VERTICAL);
pane.add(this.vScrollBar,BorderLayout.EAST);
this.hScrollBar=new JScrollBar(JScrollBar.HORIZONTAL);
pane.add(this.hScrollBar,BorderLayout.SOUTH);
/* create drawing area */
this.drawingArea=new JPanel(null,true)
{
/* create a tooltip text */
@Override
public String getToolTipText(MouseEvent event)
{
/* create genome-position */
GC gc=new GC();
StringBuilder b=new StringBuilder();
int b1= pixel2base(event.getX(), gc);
if(b1!=-1)
{
b.append("position:").append(niceNumber(b1));
int b2= pixel2base(event.getX()+1, gc);
if(b2!=-1 && b2!=b1)
{
b.append("-").append(niceNumber(b2));
}
}
/* find chromview at this location */
ChromView cv=findChromView(event.getX(), event.getY());
if(cv!=null)
{
b.insert(0,cv.getChromosome().getSequenceName());
ChromView.FileView fv=findFileView(event.getX(), event.getY());
if( fv!=null)
{
b.insert(0,fv.tabixFile.getURI()+" ");
}
}
return b.length()==0?null:b.toString();
}
/* paint component */
@Override
protected void paintComponent(Graphics g1)
{
if(offscreen==null ||
offscreen.getWidth()!=drawingArea.getWidth() ||
offscreen.getHeight()!=drawingArea.getHeight()
)
{
offscreen=new BufferedImage(
drawingArea.getWidth(),
drawingArea.getHeight(),
BufferedImage.TYPE_INT_RGB
);
GC gc= new GC();
gc.top_y=vScrollBar.getValue();
gc.width=drawingArea.getWidth();
gc.height=drawingArea.getHeight();
gc.g=offscreen.createGraphics();
paintDrawingArea(gc);
gc.g.dispose();
}
Graphics2D.class.cast(g1).drawImage(offscreen, 0, 0, drawingArea);
}
};
this.drawingArea.setToolTipText("");
this.drawingArea.setOpaque(true);
this.drawingArea.setBackground(Color.WHITE);
pane.add(this.drawingArea,BorderLayout.CENTER);
/* create chrom-views */
for(SAMSequenceRecord chrom: this.samSequenceDictionary.getSequences())
{
ChromView cv=new ChromView(chrom);
for(SigDataFileTabixReader sf: sigilesList)
{
if(!sf.hasDataForChrom(chrom)) continue;
this.max_chrom_length = Math.max(this.max_chrom_length,chrom.getSequenceLength()+1);
cv.fileViews.add(cv.new FileView(sf));
}
cv.background=(chromViews.size()%2==0?Color.WHITE:Color.LIGHT_GRAY);
chromViews.add(cv);
}
this.view_length=this.max_chrom_length;
this.recalcSize();
AdjustmentListener al=new AdjustmentListener()
{
@Override
public void adjustmentValueChanged(AdjustmentEvent e)
{
if(e.getAdjustable()==hScrollBar)
{
view_start=hScrollBar.getValue();
}
if(e.getValueIsAdjusting())
{
//handle?
}
offscreen=null;
drawingArea.repaint();
}
};
vScrollBar.addAdjustmentListener(al);
hScrollBar.addAdjustmentListener(al);
drawingArea.addMouseListener(this);
drawingArea.addMouseMotionListener(this);
this.history.add(new History(this.view_start,this.view_length));
this.position_in_history=0;
/* create history buttons */
AbstractAction action=new AbstractAction("Prev")
{
@Override
public void actionPerformed(ActionEvent e)
{
goHistory(position_in_history-1);
}
};
action.setEnabled(false);
this.getActionMap().put("ACTION.PREV.HISTORY", action);
toolbar.add(new JButton(action));
action=new AbstractAction("Next")
{
@Override
public void actionPerformed(ActionEvent e)
{
goHistory(1);
}
};
action.setEnabled(false);
this.getActionMap().put("ACTION.NEXT.HISTORY", action);
toolbar.add(new JButton(action));
/* create tools buttons */
ButtonGroup butGroup=new ButtonGroup();
for(Tool tool: Tool.values())
{
JToggleButton toggle=new JToggleButton(new SelectToolAction(tool));
butGroup.add(toggle);
toolbar.add(toggle);
}
JMenuBar bar=new JMenuBar();
this.setJMenuBar(bar);
JMenu menu=new JMenu("Options");
bar.add(menu);
menu.add(new AbstractAction("Set Height")
{
@Override
public void actionPerformed(ActionEvent e)
{
Integer h=Browser.this.askHeight();
if(h==null) return;
for(ChromView cv:chromViews)
{
for(ChromView.FileView fv:cv.fileViews)
{
fv.height_pixels=h;
}
}
recalcSize();
}
});
menu.add(new AbstractAction("Set Min/Max")
{
@Override
public void actionPerformed(ActionEvent e)
{
double h[]=Browser.this.askMinValue(-100, 100);
if(h==null) return;
for(ChromView cv:chromViews)
{
for(ChromView.FileView fv:cv.fileViews)
{
fv.min_value=h[0];
fv.max_value=h[1];
}
}
offscreen=null;
drawingArea.repaint();
}
});
bar.add(new JSeparator());
bar.add(menu);
menu.add(new AbstractAction("Close")
{
@Override
public void actionPerformed(ActionEvent e)
{
Browser.this.doMenuClose();
}
});
}
void doMenuClose()
{
//this.setVisible(false);
//this.dispose();
}
int leftMargin()
{
return LEFT_DISTANCE;
}
int topMargin()
{
return TOP_DISTANCE;
}
private void goHistory(int new_position_history)
{
if(new_position_history<0 || new_position_history>=history.size() || history.isEmpty()) return;
position_in_history=new_position_history;
Browser.this.view_start=history.get(position_in_history).view_start;
Browser.this.view_length=history.get(position_in_history).view_length;
offscreen=null;
drawingArea.repaint();
this.getActionMap().get("ACTION.PREV.HISTORY").setEnabled(position_in_history>0);
this.getActionMap().get("ACTION.NEXT.HISTORY").setEnabled(position_in_history+1< history.size());
}
private void paintDrawingArea(GC gc)
{
Graphics2D g=gc.g;
g.setColor(Color.WHITE);
g.fillRect(0, 0, gc.width, gc.height);
for(gc.step=0;gc.step<1;++gc.step)
{
paintAxis(gc);
Shape oldClip=g.getClip();
g.setClip(new Rectangle(0,topMargin(),gc.width,gc.height-topMargin()));
g.translate(0, topMargin());
for(ChromView cv: this.chromViews)
{
cv.paint(gc);
}
g.translate(0, -topMargin());
g.setClip(oldClip);
}
}
private void recalcSize()
{
int y=0;
for(ChromView cv:this.chromViews)
{
cv.y_pixels=y;
for(ChromView.FileView fv:cv.fileViews)
{
y+=BORDER_SIZE;
fv.y_pixels=y;
y+= fv.height_pixels;
}
y+=BORDER_SIZE;
cv.height_pixels=(y-cv.y_pixels);
}
adjustScrollBars();
}
private void adjustScrollBars()
{
//adjust horizontal
this.hScrollBar.setValues(
this.view_start,
this.view_length,
0,
this.max_chrom_length-this.view_length
);
ChromView last=this.chromViews.get(this.chromViews.size()-1);
int top_y= this.vScrollBar.getValue();
int max_y= last.getMaxY();
int extend= drawingArea.getHeight()-topMargin();
if(extend<=0) extend=1;
if(max_y<extend) max_y=extend;
//adjust vertical
this.vScrollBar.setValues(
top_y,
extend,
0,
max_y
);
}
private void paintAxis(GC gc)
{
AffineTransform old;
Graphics2D g=gc.g;
final int num_steps=10;
int step=(int)(Math.pow(10,Math.ceil(Math.log10(this.view_length)))/num_steps);
if(step>=10 && step*2>=this.view_length)
{
step/=10;
}
if(step>0)
{
//create a scale with vertical bars
int ticks = view_start - view_start%step;
while(ticks<= view_start+view_length)
{
float x= (float)base2pixel(ticks,gc);
if(x>=leftMargin() && x< gc.width)
{
g.setColor(Color.BLACK);
g.fillRect((int)x, 0, 3, gc.height);
g.setColor(Color.GRAY);
g.fillRect((int)x, 0, 2, gc.height);
g.setColor(Color.BLACK);
old=g.getTransform();
AffineTransform tr= AffineTransform.getTranslateInstance(x+2, 10);
tr.rotate(Math.PI/2);
g.setTransform(tr);
g.drawString(niceNumber(ticks),0,0);
g.setTransform(old);
}
if(step>=10)
{
for(int t2=ticks;t2<ticks+step;t2+=step/10)
{
float x2= base2pixel(t2,gc);
if(!(x2>=leftMargin() && x2< gc.width)) continue;
g.setColor(Color.LIGHT_GRAY);
g.drawLine((int)x2, 0, (int)x2, gc.height);
/*
g.setColor(Color.BLACK);
old=g.getTransform();
AffineTransform tr= AffineTransform.getTranslateInstance(x2+2, 10);
tr.rotate(Math.PI/2);
g.setTransform(tr);
g.drawString(niceNumber(t2),0,0);
g.setTransform(old);
*/
}
}
ticks+=step;
}
}
}
private String niceNumber(final int i)
{
String s=String.valueOf(i);
StringBuilder b= new StringBuilder(s.length());
for(int j=0;j< s.length();j++)
{
if(j!=0 && j%3==0) b.insert(0, ' ');
b.insert(0,s.charAt(s.length()-1-j));
}
return b.toString();
}
private int base2pixel(final int genome_pos,GC gc)
{
int left=leftMargin();
return left+(int)(((genome_pos- this.view_start)/(double)this.view_length)*(gc.width-left));
}
private int pixel2base(final int x1,GC gc)
{
int left=leftMargin();
if(x1< left) return -1;
return this.view_start+(int)(((x1-left)/(double)(gc.width-left))*this.view_length);
}
@Override
public void mouseClicked(MouseEvent e)
{
}
@Override
public void mouseEntered(MouseEvent e)
{
}
@Override
public void mouseExited(MouseEvent e)
{
}
@Override
public void mousePressed(MouseEvent e)
{
this.mouseStart=null;
this.mousePrev=null;
this.selectedChromView=findChromView(e.getX(),e.getY());
this.selectedFileFiew=findFileView(e.getX(),e.getY());
if(e.isPopupTrigger() || e.isControlDown())
{
JPopupMenu menu=new JPopupMenu();
if(this.selectedChromView!=null)
{
this.selectedChromView.fillPopup(menu);
if(this.selectedFileFiew!=null)
{
this.selectedFileFiew.fillPopup(menu);
}
}
menu.show(drawingArea, e.getX(), e.getY());
return;
}
this.mouseStart=new Point(e.getX(),e.getY());
this.mousePrev=null;
switch(currentTool)
{
case CHANGE_HEIGHT:
case SHOW_DATA_FOR_FILE:
if(selectedFileFiew==null)
{
mouseStart=null;
}
break;
case ZOOM: break;
default:break;
}
}
@Override
public void mouseDragged(MouseEvent e)
{
if(mouseStart!=null && e.getX()> (leftMargin()) && e.getX()<this.drawingArea.getWidth())
{
int topY= topMargin();
int heightY= drawingArea.getHeight()-topMargin();
switch(currentTool)
{
case CHANGE_HEIGHT:
case SHOW_DATA_FOR_FILE:
{
GC gc=new GC();
topY=selectedFileFiew.getTopY(gc);
heightY= selectedFileFiew.getHeight();
break;
}
default:break;
}
Graphics2D g=(Graphics2D)drawingArea.getGraphics();
g.setXORMode(drawingArea.getBackground());
if(currentTool==Tool.CHANGE_HEIGHT)
{
int mid=topY+heightY/2;
int radius;
if(mousePrev!=null)
{
radius= (int) Point2D.distance(mouseStart.x, mid, mousePrev.x, mousePrev.y);
g.drawOval(mouseStart.x-radius, mid-radius, radius*2, radius*2);
}
mousePrev=new Point(e.getX(),e.getY());
radius= (int) Point2D.distance(mouseStart.x, mid, mousePrev.x, mousePrev.y);
g.drawOval(mouseStart.x-radius, mid-radius, radius*2, radius*2);
}
else
{
if(mousePrev!=null)
{
g.fillRect(
Math.min(mousePrev.x, mouseStart.x),
topY,
Math.abs(mousePrev.x - mouseStart.x),
heightY
);
}
mousePrev=new Point(e.getX(),e.getY());
g.fillRect(
Math.min(mousePrev.x, mouseStart.x),
topY,
Math.abs(mousePrev.x - mouseStart.x),
heightY
);
}
g.setPaintMode();
}
}
@Override
public void mouseReleased(MouseEvent e)
{
if(mouseStart!=null &&
mousePrev!=null &&
!mouseStart.equals(mousePrev))
{
switch(currentTool)
{
case ZOOM:
{
GC gc=new GC();
int chromStart=pixel2base(Math.max(leftMargin(),Math.min(mouseStart.x, mousePrev.x)), gc);
int chromEnd=pixel2base(Math.min(drawingArea.getWidth(),Math.max(mouseStart.x, mousePrev.x)), gc);
if(chromStart< chromEnd)
{
History h=new History(chromStart, chromEnd-chromStart);
history.add(h);
position_in_history++;
while(position_in_history+1>history.size() )
{
history.remove(history.size()-1);
}
this.view_start=h.view_start;
this.view_length=h.view_length;
this.getActionMap().get("ACTION.PREV.HISTORY").setEnabled(position_in_history>0);
this.getActionMap().get("ACTION.NEXT.HISTORY").setEnabled(false);
adjustScrollBars();
}
break;
}
case CHANGE_HEIGHT:
{
GC gc=new GC();
int mid=selectedFileFiew.getTopY(gc)+selectedFileFiew.getHeight()/2;
int radius= (int) Point2D.distance(mouseStart.x, mid, mousePrev.x, mousePrev.y);
radius*=2;
if(radius< 20 || radius>1000) break;
selectedFileFiew.height_pixels=radius;
recalcSize();
break;
}
case SHOW_DATA_FOR_FILE:
{
GC gc=new GC();
int chromStart=pixel2base(Math.max(leftMargin(),Math.min(mouseStart.x, mousePrev.x)), gc);
int chromEnd=pixel2base(Math.min(drawingArea.getWidth(),Math.max(mouseStart.x, mousePrev.x)), gc);
Iterator<SigData> iter=selectedFileFiew.tabixFile.iterator(
selectedFileFiew.getChromosome().getSequenceName(),
chromStart,
chromEnd
);
Table table=new Table(iter);
CloserUtil.close(iter);
SigFrame.this.desktop.add(table);
table.setVisible(true);
break;
}
}
}
mouseStart=null;
mousePrev=null;
offscreen=null;
selectedChromView=null;
selectedFileFiew=null;
drawingArea.repaint();
}
@Override
public void mouseMoved(MouseEvent e)
{
}
/** find chrom view at given location */
private ChromView findChromView(int x,int y)
{
GC gc=new GC();
for(ChromView cv:this.chromViews)
{
if(cv.contains(gc,x,y)) return cv;
}
return null;
}
/** find file-view at given location */
private ChromView.FileView findFileView(int x,int y)
{
GC gc=new GC();
for(ChromView cv:this.chromViews)
{
for( ChromView.FileView fv:cv.fileViews)
{
if(fv.contains(gc,x,y)) return fv;
}
}
return null;
}
private Integer askHeight()
{
JSpinner spin=new JSpinner(new SpinnerNumberModel(DEFAULT_FILE_VIEW_HEIGHT, 50, 1000, 1));
JPanel pane=new JPanel(new FlowLayout(FlowLayout.LEADING));
JLabel label=new JLabel("new Height:",JLabel.RIGHT);
label.setLabelFor(spin);
pane.add(label);
pane.add(spin);
if(JOptionPane.showConfirmDialog(
Browser.this, pane,"Height",
JOptionPane.OK_CANCEL_OPTION
)!=JOptionPane.OK_OPTION)
{
return null;
}
return SpinnerNumberModel.class.cast(spin.getModel()).getNumber().intValue();
}
private double[] askMinValue(double min,double max)
{
JSpinner spinMin=new JSpinner(new SpinnerNumberModel(min,Math.min(min, -20.0),Math.max(max, 20.0),0.001));
JPanel pane=new JPanel(new FlowLayout(FlowLayout.LEADING));
JLabel label=new JLabel("new Min Value:",JLabel.RIGHT);
label.setLabelFor(spinMin);
pane.add(label);
pane.add(spinMin);
JSpinner spinMax=new JSpinner(new SpinnerNumberModel(max,Math.min(min, -20.0),Math.max(max, 20.0),0.001));
label=new JLabel("new Max Value:",JLabel.RIGHT);
label.setLabelFor(spinMax);
pane.add(label);
pane.add(spinMax);
do
{
if(JOptionPane.showConfirmDialog(
Browser.this, pane,"Height",
JOptionPane.OK_CANCEL_OPTION
)!=JOptionPane.OK_OPTION)
{
return null;
}
min= SpinnerNumberModel.class.cast(spinMin.getModel()).getNumber().doubleValue();
max= SpinnerNumberModel.class.cast(spinMax.getModel()).getNumber().doubleValue();
} while(min>max);
return new double[]{min,max};
}
}
/**
* Table
*/
private class Table extends IFrame
{
/**
* Model for this table
*
*/
class SigTableModel
extends AbstractGenericTable<SigData>
{
public SigTableModel(Iterator<SigData> iter)
{
while(iter.hasNext() && this.getRowCount()<10000)
{
this.getRows().add(iter.next());
}
}
@Override
public int getColumnCount()
{
return 6;
}
@Override
public Class<?> getColumnClass(int columnIndex) {
switch(columnIndex)
{
case 0: return String.class;
case 1: return Integer.class;
case 2: return Integer.class;
case 3: return String.class;
case 4: return Double.class;
case 5: return String.class;
}
return null;
}
@Override
public Object getValueOf(SigData d, int columnIndex)
{
switch(columnIndex)
{
case 0: return d.getChromosome();
case 1: return d.getStart();
case 2: return d.getEnd();
case 3: return d.getName();
case 4: return d.getValue();
case 5: return d.color_string;
}
return null;
}
@Override
public String getColumnName(int columnIndex)
{
switch(columnIndex)
{
case 0: return "Chrom";
case 1: return "Start";
case 2: return "End";
case 3: return "Name";
case 4: return "Value";
case 5: return "Color";
}
return null;
}
}
private JTable table;
Table(Iterator<SigData> iter)
{
SigTableModel model=new SigTableModel(iter);
this.table=new JTable(model);
table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
getContentPane().add(new JScrollPane(this.table),BorderLayout.CENTER);
if(iter.hasNext())
{
getContentPane().add(new JLabel("Warning: maximum number of rows reached"),
BorderLayout.NORTH);
}
}
}
private SigFrame()
{
super("SigFrame");
setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
addWindowListener(new WindowAdapter()
{
@Override
public void windowClosing(WindowEvent e)
{
doMenuQuit();
}
});
JPanel contentPane=new JPanel(new BorderLayout());
contentPane.setBorder(new EmptyBorder(4,4, 4, 4));
setContentPane(contentPane);
this.desktop=new JDesktopPane();
contentPane.add(desktop);
AbstractAction action=new AbstractAction("Quit")
{
@Override
public void actionPerformed(ActionEvent e)
{
doMenuQuit();
}
};
this.actionMap.put("ACTION_QUIT", action);
action=new AbstractAction("Open Reference...")
{
@Override
public void actionPerformed(ActionEvent e)
{
doMenuOpenGenome();
}
};
action.putValue(AbstractAction.SHORT_DESCRIPTION, "Load reference");
this.actionMap.put("ACTION_OPEN_FAIDX", action);
action=new AbstractAction("Open File...")
{
@Override
public void actionPerformed(ActionEvent e)
{
doMenuOpen();
}
};
action.putValue(AbstractAction.SHORT_DESCRIPTION, "Load Files....");
action.setEnabled(false);
this.actionMap.put("ACTION_OPEN", action);
/*
action=new AbstractAction("Open URL...")
{
@Override
public void actionPerformed(ActionEvent e)
{
doMenuLoadURL();
}
};
action.putValue(AbstractAction.SHORT_DESCRIPTION, "Load from remote URL");
this.actionMap.put("ACTION_LOADURL", action);
*/
JMenuBar bar=new JMenuBar();
setJMenuBar(bar);
JMenu menu=new JMenu("File");
bar.add(menu);
menu.add(new AbstractAction("About...")
{
@Override
public void actionPerformed(ActionEvent ae)
{
JOptionPane.showMessageDialog(
SigFrame.this,
"<html><body><h1 align='center'>"+
"SigFrame"+
"</h1>"+aboutMessage+"</body></html>",
"About...",JOptionPane.PLAIN_MESSAGE);
}
});
menu.add(new JSeparator());
menu.add(this.actionMap.get("ACTION_OPEN_FAIDX"));
menu.add(this.actionMap.get("ACTION_OPEN"));
menu.add(new JSeparator());
menu.add(this.actionMap.get("ACTION_QUIT"));
}
private void doMenuQuit()
{
for(JInternalFrame ji:this.desktop.getAllFrames())
{
ji.setVisible(false);
ji.dispose();
}
this.setVisible(false);
this.dispose();
}
public SAMSequenceDictionary getSamSequenceDictionary()
{
return genome;
}
private void doMenuOpenGenome()
{
JFileChooser chooser=new JFileChooser();
chooser.setFileFilter(new FileFilter()
{
@Override
public String getDescription()
{
return "faidx file";
}
@Override
public boolean accept(File f)
{
return f.isDirectory() || (f.isFile() && f.getName().endsWith(".dict") || f.getName().endsWith(".fai"));
}
});
if(chooser.showOpenDialog(this)!=JFileChooser.APPROVE_OPTION) return;
try {
doMenuOpenGenome(chooser.getSelectedFile());
}
catch (Exception e)
{
this.actionMap.get("ACTION_OPEN").setEnabled(false);
showError(e);
}
}
private void doMenuOpenGenome(File f) throws IOException
{
LOG.info("open faidx "+f);
this.actionMap.get("ACTION_OPEN").setEnabled(false);
this.genome= SAMSequenceDictionaryExtractor.extractDictionary(f);
this.actionMap.get("ACTION_OPEN").setEnabled(true);
}
private void doMenuOpen()
{
if(this.genome==null) return ;
JFileChooser chooser=new JFileChooser();
chooser.setMultiSelectionEnabled(true);
chooser.setFileFilter(new FileFilter()
{
@Override
public String getDescription()
{
return "VCF file indexed with tabix";
}
@Override
public boolean accept(File f)
{
return f.isDirectory() || TabixFileReader.isValidTabixFile(f);
}
});
if(chooser.showOpenDialog(this)!=JFileChooser.APPROVE_OPTION) return;
File files[]=chooser.getSelectedFiles();
if(files==null || files.length==0) return;
openTabixFiles(Arrays.asList(files));
}
private void openTabixFiles(Collection<File> inputs)
{
SigFiles storage=new SigFiles();
try
{
for(File f:inputs)
{
LOG.info("opening "+f);
storage.add(new SigDataFileTabixReader(f));
}
Browser browser=new Browser(this.genome,storage);
SigFrame.this.desktop.add(browser);
browser.setVisible(true);
}
catch(Exception err)
{
err.printStackTrace();
showError(err);
return;
}
}
private void showError(Object o)
{
}
@Program(name="sigframe",
description="SigFrame displays CGH/ position+values in a GUI",
keywords={"cgh","gui","visualization"}
)
public static class Main extends Launcher
{
@Parameter(names="-R",description="Reference indexed fasta file",required=true)
private File referenceFile;
private SigFrame app=new SigFrame();
private Main()
{
}
@Override
public String getProgramName()
{
return "SigFrame";
}
@Override
public int doWork(List<String> args) {
if(this.referenceFile==null)
{
LOG.error("ref missing");
return -1;
}
try
{
app.doMenuOpenGenome(this.referenceFile);
app.aboutMessage="Author: Pierre Lindenbaum "+
"<br>Version:"+getVersion();
JFrame.setDefaultLookAndFeelDecorated(true);
JDialog.setDefaultLookAndFeelDecorated(true);
/** loop over tabix file */
if(!args.isEmpty())
{
if(app.getSamSequenceDictionary()==null)
{
LOG.error("No indexed genome defined");
return -1;
}
LoadAtStartup startup= new LoadAtStartup(app);
for(final String fname:args)
{
final File f=new File(fname);
if(!TabixFileReader.isValidTabixFile(f))
{
LOG.error("Not a valid tabix file:"+f);
return -1;
}
startup.sources.add(f);
}
app.addWindowListener(startup);
}
Dimension screen= Toolkit.getDefaultToolkit().getScreenSize();
app.setBounds(
50,50,
screen.width-100,
screen.height-100
);
SwingUtilities.invokeAndWait(new Runnable()
{
@Override
public void run()
{
app.setVisible(true);
}
});
return 0;
}
catch (Exception e)
{
e.printStackTrace();
return -1;
}
}
private void startInstance(final String[] args)
{
instanceMain(args);
}
}
public static void main(String[] args)
{
new Main().startInstance(args);
}
}