/*
* Copyright (c) 2007-2012 The Broad Institute, Inc.
* SOFTWARE COPYRIGHT NOTICE
* This software and its documentation are the copyright of the Broad Institute, Inc. All rights are reserved.
*
* This software is supplied without any warranty or guaranteed support whatsoever. The Broad Institute is not responsible for its use, misuse, or functionality.
*
* This software is licensed under the terms of the GNU Lesser General Public License (LGPL),
* Version 2.1 which is available at http://www.opensource.org/licenses/lgpl-2.1.php.
*/
/*
* Created by JFormDesigner on Tue Jan 29 15:22:45 EST 2013
*/
package org.broad.igv.tools.motiffinder;
import htsjdk.samtools.util.SequenceUtil;
import org.broad.igv.annotations.ForTesting;
import org.broad.igv.ui.util.MessageUtils;
import org.broad.igv.ui.util.UIUtilities;
import org.broad.igv.util.ParsingUtils;
import org.broad.igv.util.StringUtils;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import javax.swing.event.CaretEvent;
import javax.swing.event.CaretListener;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
/**
* Dialog so the user can enter a pattern that can be used
* to search a nucleotide sequence.
*
* @author Jacob Silterra
*/
public class MotifFinderDialog extends JDialog {
private static Map<String, String> letterToRegex;
private static Set<String> validIUPACInputStrings;
static {
initLetterToRegex();
}
private static void initLetterToRegex() {
letterToRegex = ParsingUtils.loadIUPACMap();
validIUPACInputStrings = new HashSet<String>(letterToRegex.size());
for (String key : letterToRegex.keySet()) {
validIUPACInputStrings.add(key.toUpperCase());
}
}
/**
* Returns true if character c is a valid IUPAC Ambiguity code
*
* @param c Single character
* @return
*/
public static boolean isIUPACChar(String c) {
return validIUPACInputStrings.contains(c);
}
private String[] posTrackNames;
private String[] negTrackNames;
private String[] inputPatterns;
public MotifFinderDialog(Frame owner) {
super(owner);
initComponents();
}
public String[] getInputPattern() {
return inputPatterns;
}
public String[] getPosTrackName() {
return posTrackNames;
}
public String[] getNegTrackName() {
return negTrackNames;
}
/**
* Replace the ambiguity codes in the motif
* with regular expression equivalents
*
* @param motif
* @return
*/
static String convertMotifToRegex(String motif) {
String output = motif;
int outloc = 0;
for (int inloc = 0; inloc < motif.length(); inloc++) {
String inchar = motif.substring(inloc, inloc + 1);
String rep = letterToRegex.get(inchar);
output = output.substring(0, outloc) + rep + motif.substring(inloc + 1);
outloc += rep.length();
}
return output;
}
private void cancelButtonActionPerformed(ActionEvent e) {
this.setVisible(false);
}
void okButtonActionPerformed(ActionEvent e) {
this.inputPatterns = null;
//Split into lines, each one will be a new pair of tracks
String[] lines = patternField.getText().split("[\\r\\n]+");
String[] patterns = new String[lines.length];
boolean isMultiMatch = lines.length >= 2;
this.posTrackNames = new String[lines.length];
this.negTrackNames = new String[lines.length];
for(int ii=0; ii < lines.length; ii++){
String strPattern = lines[ii].toUpperCase();
boolean isIUPAC = checkIUPACPatternValid(strPattern);
boolean isRegex = checkNucleotideRegex(strPattern);
boolean patternIsValid = isIUPAC || isRegex;
if (!patternIsValid) {
MessageUtils.showMessage("Please enter a valid pattern.\n" +
"Patterns using IUPAC ambiguity codes should contain no special characters.\n" +
"Regular expressions should contain only 'ACTGN' in addition to special characters.\n" +
strPattern + " is invalid");
return;
}
if (isIUPAC) {
strPattern = convertMotifToRegex(strPattern);
}
if(isMultiMatch){
String posName = getPosNameFromPattern(strPattern);
this.posTrackNames[ii] = posName;
this.negTrackNames[ii] = getNegNameFromPositive(posName);
}else{
this.posTrackNames[ii] = posNameField.getText();
this.negTrackNames[ii] = negNameField.getText();
if (this.posTrackNames[ii].equalsIgnoreCase(negTrackNames[ii])) {
MessageUtils.showMessage("Track names must be different");
return;
}
}
patterns[ii] = strPattern;
}
this.inputPatterns = patterns;
this.setVisible(false);
}
/**
* Determines whether this string pattern is interpretable as
* a set of IUPAC nucleotide characters
*
* @param strPattern Upper case string pattern
* @return
*/
static boolean checkIUPACPatternValid(String strPattern) {
for (int ii = 0; ii < strPattern.length(); ii++) {
String c = strPattern.substring(ii, ii + 1);
if (!isIUPACChar(c)) {
return false;
}
}
return true;
}
/**
* Determine whether it's a valid regex.
* Also, any letters should be one of AGCTN (case insensitive)
*
* @param strPattern
* @return
*/
static boolean checkNucleotideRegex(String strPattern) {
try {
//First check if it's valid regex
Pattern pattern = Pattern.compile(strPattern);
byte[] bytes = strPattern.getBytes();
for (byte c : bytes) {
if (Character.isLetter(c)) {
boolean validBase = SequenceUtil.isValidBase(c);
validBase |= c == 'N';
if (!validBase) return false;
}
}
} catch (PatternSyntaxException e) {
return false;
}
return true;
}
/**
* Whether we are matching multiple patterns (ie whether there are newlines in the text field)
* @return
*/
private boolean isMultiMatch(){
String patternText = MotifFinderDialog.this.patternField.getText();
return patternText.contains("\n") || patternText.contains("\r");
}
private void updateNegNameFieldFromPattern() {
UIUtilities.invokeOnEventThread(new Runnable() {
@Override
public void run() {
String posText = MotifFinderDialog.this.posNameField.getText();
MotifFinderDialog.this.negNameField.setText(getNegNameFromPositive(posText));
MotifFinderDialog.this.negNameField.setEnabled(!isMultiMatch());
}
});
}
private void updatePosNameFieldFromPattern() {
UIUtilities.invokeOnEventThread(new Runnable() {
@Override
public void run() {
String posNameText = "Auto";
boolean hasNewlines = isMultiMatch();
if(!hasNewlines){
String patternText = MotifFinderDialog.this.patternField.getText();
posNameText = StringUtils.checkLength(patternText, MaxTrackNameLength);
}
MotifFinderDialog.this.posNameField.setEnabled(!isMultiMatch());
MotifFinderDialog.this.posNameField.setText(posNameText);
}
});
}
private String getPosNameFromPattern(String patternText){
return StringUtils.checkLength(patternText, MaxTrackNameLength);
}
private String getNegNameFromPositive(String posText){
return posText + " Negative";
}
static final int MaxTrackNameLength = 100;
private void patternFieldCaretUpdate(CaretEvent e) {
updatePosNameFieldFromPattern();
updateNegNameFieldFromPattern();
}
private void initComponents() {
// JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents
// Generated using JFormDesigner non-commercial license
dialogPane = new JPanel();
contentPanel = new JPanel();
label2 = new JLabel();
label4 = new JLabel();
patternField = new JTextArea();
textArea1 = new JTextArea();
vSpacer1 = new JPanel(null);
panel1 = new JPanel();
label1 = new JLabel();
posNameField = new JTextField();
panel2 = new JPanel();
label3 = new JLabel();
negNameField = new JTextField();
buttonBar = new JPanel();
okButton = new JButton();
cancelButton = new JButton();
//======== this ========
setModal(true);
Container contentPane = getContentPane();
contentPane.setLayout(new BorderLayout());
//======== dialogPane ========
{
dialogPane.setBorder(new EmptyBorder(12, 12, 12, 12));
dialogPane.setLayout(new BorderLayout());
//======== contentPanel ========
{
contentPanel.setAlignmentX(0.0F);
contentPanel.setLayout(new BoxLayout(contentPanel, BoxLayout.Y_AXIS));
//---- label2 ----
label2.setText("Search Pattern:");
label2.setLabelFor(patternField);
label2.setHorizontalTextPosition(SwingConstants.LEFT);
label2.setHorizontalAlignment(SwingConstants.LEFT);
label2.setAlignmentX(1.0F);
label2.setMaximumSize(new Dimension(374, 16));
label2.setPreferredSize(new Dimension(374, 16));
contentPanel.add(label2);
contentPanel.add(label4);
//---- patternField ----
patternField.setToolTipText("Enter multiple patterns, separated by newlines");
patternField.setRows(2);
patternField.addCaretListener(new CaretListener() {
@Override
public void caretUpdate(CaretEvent e) {
patternFieldCaretUpdate(e);
}
});
contentPanel.add(patternField);
//---- textArea1 ----
textArea1.setText("Enter nucleotide sequence (e.g. ACCGCT), or nucleotide sequence with IUPAC ambiguity codes (e.g. AAARNR), or regular expression of nucleotides (e.g. TATAAA(A){3,}). ");
textArea1.setEditable(false);
textArea1.setBackground(new Color(238, 238, 238));
textArea1.setLineWrap(true);
textArea1.setWrapStyleWord(true);
textArea1.setFont(textArea1.getFont().deriveFont(textArea1.getFont().getStyle() | Font.ITALIC, textArea1.getFont().getSize() - 2f));
textArea1.setMargin(new Insets(0, 25, 0, 0));
textArea1.setFocusable(false);
contentPanel.add(textArea1);
//---- vSpacer1 ----
vSpacer1.setMinimumSize(new Dimension(12, 20));
vSpacer1.setPreferredSize(new Dimension(10, 20));
contentPanel.add(vSpacer1);
//======== panel1 ========
{
panel1.setLayout(new BoxLayout(panel1, BoxLayout.X_AXIS));
//---- label1 ----
label1.setText("Positive Strand Track Name:");
label1.setLabelFor(posNameField);
label1.setHorizontalTextPosition(SwingConstants.LEFT);
label1.setHorizontalAlignment(SwingConstants.LEFT);
label1.setMaximumSize(new Dimension(374, 16));
label1.setPreferredSize(new Dimension(200, 16));
label1.setAlignmentX(1.0F);
panel1.add(label1);
panel1.add(posNameField);
}
contentPanel.add(panel1);
//======== panel2 ========
{
panel2.setLayout(new BoxLayout(panel2, BoxLayout.X_AXIS));
//---- label3 ----
label3.setText("Negative Strand Track Name:");
label3.setLabelFor(negNameField);
label3.setHorizontalTextPosition(SwingConstants.LEFT);
label3.setHorizontalAlignment(SwingConstants.LEFT);
label3.setMaximumSize(new Dimension(374, 16));
label3.setPreferredSize(new Dimension(200, 16));
label3.setAlignmentX(1.0F);
panel2.add(label3);
panel2.add(negNameField);
}
contentPanel.add(panel2);
}
dialogPane.add(contentPanel, BorderLayout.NORTH);
//======== buttonBar ========
{
buttonBar.setBorder(new EmptyBorder(12, 0, 0, 0));
buttonBar.setLayout(new GridBagLayout());
((GridBagLayout)buttonBar.getLayout()).columnWidths = new int[] {0, 85, 80};
((GridBagLayout)buttonBar.getLayout()).columnWeights = new double[] {1.0, 0.0, 0.0};
//---- okButton ----
okButton.setText("OK");
okButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
okButtonActionPerformed(e);
}
});
buttonBar.add(okButton, new GridBagConstraints(1, 0, 1, 1, 0.0, 0.0,
GridBagConstraints.CENTER, GridBagConstraints.BOTH,
new Insets(0, 0, 0, 5), 0, 0));
//---- cancelButton ----
cancelButton.setText("Cancel");
cancelButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
cancelButtonActionPerformed(e);
}
});
buttonBar.add(cancelButton, new GridBagConstraints(2, 0, 1, 1, 0.0, 0.0,
GridBagConstraints.CENTER, GridBagConstraints.BOTH,
new Insets(0, 0, 0, 0), 0, 0));
}
dialogPane.add(buttonBar, BorderLayout.SOUTH);
}
contentPane.add(dialogPane, BorderLayout.CENTER);
setSize(450, 300);
setLocationRelativeTo(getOwner());
// JFormDesigner - End of component initialization //GEN-END:initComponents
}
// JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables
// Generated using JFormDesigner non-commercial license
private JPanel dialogPane;
private JPanel contentPanel;
private JLabel label2;
private JLabel label4;
private JTextArea patternField;
private JTextArea textArea1;
private JPanel vSpacer1;
private JPanel panel1;
private JLabel label1;
private JTextField posNameField;
private JPanel panel2;
private JLabel label3;
private JTextField negNameField;
private JPanel buttonBar;
private JButton okButton;
private JButton cancelButton;
// JFormDesigner - End of variables declaration //GEN-END:variables
@ForTesting
void setPatternFieldText(String text) {
this.patternField.setText(text);
}
}