/*
* Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
* Copyright (C) $$Year-Rel$$ The Jalview Authors
*
* This file is part of Jalview.
*
* Jalview 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.
*
* Jalview 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 Jalview. If not, see .
* The Jalview Authors are detailed in the 'AUTHORS' file.
*/
package jalview.gui;
import jalview.analysis.AlignSeq;
import jalview.datamodel.AlignmentAnnotation;
import jalview.datamodel.ColumnSelection;
import jalview.datamodel.HiddenColumns;
import jalview.datamodel.RnaViewerModel;
import jalview.datamodel.SequenceGroup;
import jalview.datamodel.SequenceI;
import jalview.ext.varna.RnaModel;
import jalview.structure.SecondaryStructureListener;
import jalview.structure.SelectionListener;
import jalview.structure.SelectionSource;
import jalview.structure.StructureSelectionManager;
import jalview.structure.VamsasSource;
import jalview.util.Comparison;
import jalview.util.MessageManager;
import jalview.util.ShiftList;
import java.awt.BorderLayout;
import java.awt.Color;
import java.util.Collection;
import java.util.Hashtable;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javax.swing.JInternalFrame;
import javax.swing.JSplitPane;
import javax.swing.event.InternalFrameAdapter;
import javax.swing.event.InternalFrameEvent;
import fr.orsay.lri.varna.VARNAPanel;
import fr.orsay.lri.varna.exceptions.ExceptionFileFormatOrSyntax;
import fr.orsay.lri.varna.exceptions.ExceptionLoadingFailed;
import fr.orsay.lri.varna.exceptions.ExceptionUnmatchedClosingParentheses;
import fr.orsay.lri.varna.interfaces.InterfaceVARNASelectionListener;
import fr.orsay.lri.varna.models.BaseList;
import fr.orsay.lri.varna.models.FullBackup;
import fr.orsay.lri.varna.models.annotations.HighlightRegionAnnotation;
import fr.orsay.lri.varna.models.rna.ModeleBase;
import fr.orsay.lri.varna.models.rna.RNA;
public class AppVarna extends JInternalFrame
implements SelectionListener, SecondaryStructureListener,
InterfaceVARNASelectionListener, VamsasSource
{
private static final byte[] PAIRS = new byte[] { '(', ')', '[', ']', '{',
'}', '<', '>' };
private AppVarnaBinding vab;
private AlignmentPanel ap;
private String viewId;
private StructureSelectionManager ssm;
/*
* Lookup for sequence and annotation mapped to each RNA in the viewer. Using
* a linked hashmap means that order is preserved when saved to the project.
*/
private Map models = new LinkedHashMap();
private Map offsets = new Hashtable();
private Map offsetsInv = new Hashtable();
private JSplitPane split;
private VarnaHighlighter mouseOverHighlighter = new VarnaHighlighter();
private VarnaHighlighter selectionHighlighter = new VarnaHighlighter();
private class VarnaHighlighter
{
private HighlightRegionAnnotation _lastHighlight;
private RNA _lastRNAhighlighted = null;
public VarnaHighlighter()
{
}
/**
* Constructor when restoring from Varna session, including any highlight
* state
*
* @param rna
*/
public VarnaHighlighter(RNA rna)
{
// TODO nice try but doesn't work; do we need a highlighter per model?
_lastRNAhighlighted = rna;
List highlights = rna.getHighlightRegion();
if (highlights != null && !highlights.isEmpty())
{
_lastHighlight = highlights.get(0);
}
}
/**
* highlight a region from start to end (inclusive) on rna
*
* @param rna
* @param start
* - first base pair index (from 0)
* @param end
* - last base pair index (from 0)
*/
public void highlightRegion(RNA rna, int start, int end)
{
clearLastSelection();
HighlightRegionAnnotation highlight = new HighlightRegionAnnotation(
rna.getBasesBetween(start, end));
rna.addHighlightRegion(highlight);
_lastHighlight = highlight;
_lastRNAhighlighted = rna;
}
public HighlightRegionAnnotation getLastHighlight()
{
return _lastHighlight;
}
/**
* Clears all structure selection and refreshes the display
*/
public void clearSelection()
{
if (_lastRNAhighlighted != null)
{
_lastRNAhighlighted.getHighlightRegion().clear();
vab.updateSelectedRNA(_lastRNAhighlighted);
_lastRNAhighlighted = null;
_lastHighlight = null;
}
}
/**
* Clear the last structure selection
*/
public void clearLastSelection()
{
if (_lastRNAhighlighted != null)
{
_lastRNAhighlighted.removeHighlightRegion(_lastHighlight);
_lastRNAhighlighted = null;
_lastHighlight = null;
}
}
}
/**
* Constructor
*
* @param seq
* the RNA sequence
* @param aa
* the annotation with the secondary structure string
* @param ap
* the AlignmentPanel creating this object
*/
public AppVarna(SequenceI seq, AlignmentAnnotation aa, AlignmentPanel ap)
{
this(ap);
String sname = aa.sequenceRef == null
? "secondary structure (alignment)"
: seq.getName() + " structure";
String theTitle = sname
+ (aa.sequenceRef == null ? " trimmed to " + seq.getName()
: "");
theTitle = MessageManager.formatMessage("label.varna_params",
new String[]
{ theTitle });
setTitle(theTitle);
String gappedTitle = sname + " (with gaps)";
RnaModel gappedModel = new RnaModel(gappedTitle, aa, seq, null, true);
addModel(gappedModel, gappedTitle);
String trimmedTitle = "trimmed " + sname;
RnaModel trimmedModel = new RnaModel(trimmedTitle, aa, seq, null,
false);
addModel(trimmedModel, trimmedTitle);
vab.setSelectedIndex(0);
}
/**
* Constructor that links the viewer to a parent panel (but has no structures
* yet - use addModel to add them)
*
* @param ap
*/
protected AppVarna(AlignmentPanel ap)
{
this.ap = ap;
this.viewId = System.currentTimeMillis() + "." + this.hashCode();
vab = new AppVarnaBinding();
initVarna();
this.ssm = ap.getStructureSelectionManager();
ssm.addStructureViewerListener(this);
ssm.addSelectionListener(this);
addInternalFrameListener(new InternalFrameAdapter()
{
@Override
public void internalFrameClosed(InternalFrameEvent evt)
{
close();
}
});
}
/**
* Constructor given viewer data read from a saved project file
*
* @param model
* @param ap
* the (or a) parent alignment panel
*/
public AppVarna(RnaViewerModel model, AlignmentPanel ap)
{
this(ap);
setTitle(model.title);
this.viewId = model.viewId;
setBounds(model.x, model.y, model.width, model.height);
this.split.setDividerLocation(model.dividerLocation);
}
/**
* Constructs a split pane with an empty selection list and display panel, and
* adds it to the desktop
*/
public void initVarna()
{
VARNAPanel varnaPanel = vab.get_varnaPanel();
setBackground(Color.white);
split = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, true,
vab.getListPanel(), varnaPanel);
getContentPane().setLayout(new BorderLayout());
getContentPane().add(split, BorderLayout.CENTER);
varnaPanel.addSelectionListener(this);
jalview.gui.Desktop.addInternalFrame(this, "", getBounds().width,
getBounds().height);
this.pack();
showPanel(true);
}
/**
* Constructs a new RNA model from the given one, without gaps. Also
* calculates and saves a 'shift list'
*
* @param rna
* @param name
* @return
*/
public RNA trimRNA(RNA rna, String name)
{
ShiftList offset = new ShiftList();
RNA rnaTrim = new RNA(name);
try
{
String structDBN = rna.getStructDBN(true);
rnaTrim.setRNA(rna.getSeq(), replaceOddGaps(structDBN));
} catch (ExceptionUnmatchedClosingParentheses e2)
{
e2.printStackTrace();
} catch (ExceptionFileFormatOrSyntax e3)
{
e3.printStackTrace();
}
String seq = rnaTrim.getSeq();
StringBuilder struc = new StringBuilder(256);
struc.append(rnaTrim.getStructDBN(true));
int ofstart = -1;
int sleng = seq.length();
for (int i = 0; i < sleng; i++)
{
if (Comparison.isGap(seq.charAt(i)))
{
if (ofstart == -1)
{
ofstart = i;
}
/*
* mark base or base & pair in the structure with *
*/
if (!rnaTrim.findPair(i).isEmpty())
{
int m = rnaTrim.findPair(i).get(1);
int l = rnaTrim.findPair(i).get(0);
struc.replace(m, m + 1, "*");
struc.replace(l, l + 1, "*");
}
else
{
struc.replace(i, i + 1, "*");
}
}
else
{
if (ofstart > -1)
{
offset.addShift(offset.shift(ofstart), ofstart - i);
ofstart = -1;
}
}
}
// final gap
if (ofstart > -1)
{
offset.addShift(offset.shift(ofstart), ofstart - sleng);
ofstart = -1;
}
/*
* remove the marked gaps from the structure
*/
String newStruc = struc.toString().replace("*", "");
/*
* remove gaps from the sequence
*/
String newSeq = AlignSeq.extractGaps(Comparison.GapChars, seq);
try
{
rnaTrim.setRNA(newSeq, newStruc);
registerOffset(rnaTrim, offset);
} catch (ExceptionUnmatchedClosingParentheses e)
{
e.printStackTrace();
} catch (ExceptionFileFormatOrSyntax e)
{
e.printStackTrace();
}
return rnaTrim;
}
/**
* Save the sequence to structure mapping, and also its inverse.
*
* @param rnaTrim
* @param offset
*/
private void registerOffset(RNA rnaTrim, ShiftList offset)
{
offsets.put(rnaTrim, offset);
offsetsInv.put(rnaTrim, offset.getInverse());
}
public void showPanel(boolean show)
{
this.setVisible(show);
}
/**
* If a mouseOver event from the AlignmentPanel is noticed the currently
* selected RNA in the VARNA window is highlighted at the specific position.
* To be able to remove it before the next highlight it is saved in
* _lastHighlight
*
* @param sequence
* @param index
* the aligned sequence position (base 0)
* @param position
* the dataset sequence position (base 1)
*/
@Override
public void mouseOverSequence(SequenceI sequence, final int index,
final int position)
{
RNA rna = vab.getSelectedRNA();
if (rna == null)
{
return;
}
RnaModel rnaModel = models.get(rna);
if (rnaModel.seq == sequence)
{
int highlightPos = rnaModel.gapped ? index
: position - sequence.getStart();
mouseOverHighlighter.highlightRegion(rna, highlightPos, highlightPos);
vab.updateSelectedRNA(rna);
}
}
@Override
public void selection(SequenceGroup seqsel, ColumnSelection colsel,
HiddenColumns hidden, SelectionSource source)
{
if (source != ap.av)
{
// ignore events from anything but our parent alignpanel
// TODO - reuse many-one panel-view system in jmol viewer
return;
}
RNA rna = vab.getSelectedRNA();
if (rna == null)
{
return;
}
RnaModel rnaModel = models.get(rna);
if (seqsel != null && seqsel.getSize() > 0
&& seqsel.contains(rnaModel.seq))
{
int start = seqsel.getStartRes(), end = seqsel.getEndRes();
if (rnaModel.gapped)
{
ShiftList shift = offsets.get(rna);
if (shift != null)
{
start = shift.shift(start);
end = shift.shift(end);
}
}
else
{
start = rnaModel.seq.findPosition(start) - rnaModel.seq.getStart();
end = rnaModel.seq.findPosition(end) - rnaModel.seq.getStart();
}
selectionHighlighter.highlightRegion(rna, start, end);
selectionHighlighter.getLastHighlight()
.setOutlineColor(seqsel.getOutlineColour());
// TODO - translate column markings to positions on structure if present.
vab.updateSelectedRNA(rna);
}
else
{
selectionHighlighter.clearSelection();
}
}
/**
* Respond to a change of the base hovered over in the Varna viewer
*/
@Override
public void onHoverChanged(ModeleBase previousBase, ModeleBase newBase)
{
RNA rna = vab.getSelectedRNA();
ShiftList shift = offsetsInv.get(rna);
SequenceI seq = models.get(rna).seq;
if (newBase != null && seq != null)
{
if (shift != null)
{
int i = shift.shift(newBase.getIndex());
// System.err.println("shifted "+(arg1.getIndex())+" to "+i);
ssm.mouseOverVamsasSequence(seq, i, this);
}
else
{
ssm.mouseOverVamsasSequence(seq, newBase.getIndex(), this);
}
}
}
@Override
public void onSelectionChanged(BaseList arg0, BaseList arg1,
BaseList arg2)
{
// TODO translate selected regions in VARNA to a selection on the
// alignpanel.
}
/**
* Returns the path to a temporary file containing a representation of the
* state of one Varna display
*
* @param rna
*
* @return
*/
public String getStateInfo(RNA rna)
{
return vab.getStateInfo(rna);
}
public AlignmentPanel getAlignmentPanel()
{
return ap;
}
public String getViewId()
{
return viewId;
}
/**
* Returns true if any of the viewer's models (not necessarily the one
* currently displayed) is for the given sequence
*
* @param seq
* @return
*/
public boolean isListeningFor(SequenceI seq)
{
for (RnaModel model : models.values())
{
if (model.seq == seq)
{
return true;
}
}
return false;
}
/**
* Returns a value representing the horizontal split divider location
*
* @return
*/
public int getDividerLocation()
{
return split == null ? 0 : split.getDividerLocation();
}
/**
* Tidy up as necessary when the viewer panel is closed
*/
protected void close()
{
/*
* Deregister as a listener, to release references to this object
*/
if (ssm != null)
{
ssm.removeStructureViewerListener(AppVarna.this, null);
ssm.removeSelectionListener(AppVarna.this);
}
}
/**
* Returns the secondary structure annotation that this viewer displays for
* the given sequence
*
* @return
*/
public AlignmentAnnotation getAnnotation(SequenceI seq)
{
for (RnaModel model : models.values())
{
if (model.seq == seq)
{
return model.ann;
}
}
return null;
}
public int getSelectedIndex()
{
return this.vab.getSelectedIndex();
}
/**
* Returns the set of models shown by the viewer
*
* @return
*/
public Collection getModels()
{
return models.values();
}
/**
* Add a model (e.g. loaded from project file)
*
* @param rna
* @param modelName
*/
public RNA addModel(RnaModel model, String modelName)
{
if (!model.ann.isValidStruc())
{
throw new IllegalArgumentException(
"Invalid RNA structure annotation");
}
/*
* opened on request in Jalview session
*/
RNA rna = new RNA(modelName);
String struc = model.ann.getRNAStruc();
struc = replaceOddGaps(struc);
String strucseq = model.seq.getSequenceAsString();
try
{
rna.setRNA(strucseq, struc);
} catch (ExceptionUnmatchedClosingParentheses e2)
{
e2.printStackTrace();
} catch (ExceptionFileFormatOrSyntax e3)
{
e3.printStackTrace();
}
if (!model.gapped)
{
rna = trimRNA(rna, modelName);
}
models.put(rna, new RnaModel(modelName, model.ann, model.seq, rna,
model.gapped));
vab.addStructure(rna);
return rna;
}
/**
* Constructs a shift list that describes the gaps in the sequence
*
* @param seq
* @return
*/
protected ShiftList buildOffset(SequenceI seq)
{
// TODO refactor to avoid duplication with trimRNA()
// TODO JAL-1789 bugs in use of ShiftList here
ShiftList offset = new ShiftList();
int ofstart = -1;
int sleng = seq.getLength();
for (int i = 0; i < sleng; i++)
{
if (Comparison.isGap(seq.getCharAt(i)))
{
if (ofstart == -1)
{
ofstart = i;
}
}
else
{
if (ofstart > -1)
{
offset.addShift(offset.shift(ofstart), ofstart - i);
ofstart = -1;
}
}
}
// final gap
if (ofstart > -1)
{
offset.addShift(offset.shift(ofstart), ofstart - sleng);
ofstart = -1;
}
return offset;
}
/**
* Set the selected index in the model selection list
*
* @param selectedIndex
*/
public void setInitialSelection(final int selectedIndex)
{
/*
* empirically it needs a second for Varna/AWT to finish loading/drawing
* models for this to work; SwingUtilities.invokeLater _not_ a solution;
* explanation and/or better solution welcome!
*/
synchronized (this)
{
try
{
wait(1000);
} catch (InterruptedException e)
{
// meh
}
}
vab.setSelectedIndex(selectedIndex);
}
/**
* Add a model with associated Varna session file
*
* @param rna
* @param modelName
*/
public RNA addModelSession(RnaModel model, String modelName,
String sessionFile)
{
if (!model.ann.isValidStruc())
{
throw new IllegalArgumentException(
"Invalid RNA structure annotation");
}
try
{
FullBackup fromSession = vab.vp.loadSession(sessionFile);
vab.addStructure(fromSession.rna, fromSession.config);
RNA rna = fromSession.rna;
// copy the model, but now including the RNA object
RnaModel newModel = new RnaModel(model.title, model.ann, model.seq,
rna, model.gapped);
if (!model.gapped)
{
registerOffset(rna, buildOffset(model.seq));
}
models.put(rna, newModel);
// capture rna selection state when saved
selectionHighlighter = new VarnaHighlighter(rna);
return fromSession.rna;
} catch (ExceptionLoadingFailed e)
{
System.err
.println("Error restoring Varna session: " + e.getMessage());
return null;
}
}
/**
* Replace everything except RNA secondary structure characters with a period
*
* @param s
* @return
*/
public static String replaceOddGaps(String s)
{
if (s == null)
{
return null;
}
// this is measured to be 10 times faster than a regex replace
boolean changed = false;
byte[] bytes = s.getBytes();
for (int i = 0; i < bytes.length; i++)
{
boolean ok = false;
// todo check for ((b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z')) if
// wanted also
for (int j = 0; !ok && (j < PAIRS.length); j++)
{
if (bytes[i] == PAIRS[j])
{
ok = true;
}
}
if (!ok)
{
bytes[i] = '.';
changed = true;
}
}
return changed ? new String(bytes) : s;
}
}