package beast.app.beauti;
import java.awt.*;
import java.text.DateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.EventObject;
import java.util.GregorianCalendar;
import java.util.List;
import javax.swing.Box;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.event.CellEditorListener;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableCellRenderer;
import beast.app.draw.BEASTObjectInputEditor;
import beast.core.BEASTInterface;
import beast.core.Input;
import beast.core.util.Log;
import beast.evolution.alignment.Taxon;
import beast.evolution.tree.TraitSet;
import beast.evolution.tree.Tree;
public class TipDatesInputEditor extends BEASTObjectInputEditor {
public TipDatesInputEditor(BeautiDoc doc) {
super(doc);
}
private static final long serialVersionUID = 1L;
DateFormat dateFormat = DateFormat.getDateInstance();
@Override
public Class<?> type() {
return Tree.class;
}
Tree tree;
TraitSet traitSet;
JComboBox<TraitSet.Units> unitsComboBox;
JComboBox<String> relativeToComboBox;
List<String> taxa;
Object[][] tableData;
JTable table;
String m_sPattern = ".*(\\d\\d\\d\\d).*";
JScrollPane scrollPane;
List<Taxon> taxonsets;
@Override
public void init(Input<?> input, BEASTInterface beastObject, int itemNr, ExpandOption isExpandOption, boolean addButtons) {
m_bAddButtons = addButtons;
this.itemNr = itemNr;
if (itemNr >= 0) {
tree = (Tree) ((List<?>) input.get()).get(itemNr);
} else {
tree = (Tree) input.get();
}
if (tree != null) {
try {
m_input = ((BEASTInterface) tree).getInput("trait");
} catch (Exception e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
m_beastObject = tree;
traitSet = tree.getDateTrait();
Box box = Box.createVerticalBox();
JCheckBox useTipDates = new JCheckBox("Use tip dates", traitSet != null);
useTipDates.addActionListener(e -> {
JCheckBox checkBox = (JCheckBox) e.getSource();
try {
if (checkBox.isSelected()) {
if (traitSet == null) {
traitSet = new TraitSet();
traitSet.initByName("traitname", "date",
"taxa", tree.getTaxonset(),
"value", "");
traitSet.setID("dateTrait.t:" + BeautiDoc.parsePartition(tree.getID()));
}
tree.setDateTrait(traitSet);
} else {
tree.setDateTrait(null);
}
refreshPanel();
} catch (Exception ex) {
ex.printStackTrace();
}
});
Box box2 = Box.createHorizontalBox();
box2.add(useTipDates);
box2.add(Box.createHorizontalGlue());
box.add(box2);
if (traitSet != null) {
box.add(createButtonBox());
box.add(createListBox());
}
add(box);
}
} // init
private Component createListBox() {
taxa = traitSet.taxaInput.get().asStringList();
String[] columnData = new String[]{"Name", "Date", "Height"};
tableData = new Object[taxa.size()][3];
convertTraitToTableData();
// set up table.
// special features: background shading of rows
// custom editor allowing only Date column to be edited.
table = new JTable(tableData, columnData) {
private static final long serialVersionUID = 1L;
// method that induces table row shading
@Override
public Component prepareRenderer(TableCellRenderer renderer, int Index_row, int Index_col) {
Component comp = super.prepareRenderer(renderer, Index_row, Index_col);
//even index, selected or not selected
if (isCellSelected(Index_row, Index_col)) {
comp.setBackground(Color.lightGray);
} else if (Index_row % 2 == 0 && !isCellSelected(Index_row, Index_col)) {
comp.setBackground(new Color(237, 243, 255));
} else {
comp.setBackground(Color.white);
}
return comp;
}
};
// set up editor that makes sure only doubles are accepted as entry
// and only the Date column is editable.
table.setDefaultEditor(Object.class, new TableCellEditor() {
JTextField m_textField = new JTextField();
int m_iRow,
m_iCol;
@Override
public boolean stopCellEditing() {
table.removeEditor();
String text = m_textField.getText();
// try {
// Double.parseDouble(text);
// } catch (Exception e) {
// try {
// Date.parse(text);
// } catch (Exception e2) {
// return false;
// }
// }
tableData[m_iRow][m_iCol] = text;
convertTableDataToTrait();
convertTraitToTableData();
return true;
}
@Override
public boolean isCellEditable(EventObject anEvent) {
return table.getSelectedColumn() == 1;
}
@Override
public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int rowNr, int colNr) {
if (!isSelected) {
return null;
}
m_iRow = rowNr;
m_iCol = colNr;
m_textField.setText((String) value);
return m_textField;
}
@Override
public boolean shouldSelectCell(EventObject anEvent) {
return false;
}
@Override
public void removeCellEditorListener(CellEditorListener l) {
}
@Override
public Object getCellEditorValue() {
return null;
}
@Override
public void cancelCellEditing() {
}
@Override
public void addCellEditorListener(CellEditorListener l) {
}
});
int fontsize = table.getFont().getSize();
table.setRowHeight(24 * fontsize / 13);
scrollPane = new JScrollPane(table);
// AJD: This ComponentListener breaks the resizing of the tip dates table, so I have removed it.
// scrollPane.addComponentListener(new ComponentListener() {
// @Override
// public void componentShown(ComponentEvent e) {}
//
// @Override
// public void componentResized(ComponentEvent e) {
// Component c = (Component) e.getSource();
// while (c.getParent() != null && !(c.getParent() instanceof JSplitPane)) {
// c = c.getParent();
// }
// if (c.getParent() != null) {
// Dimension preferredSize = c.getSize();
// preferredSize.height = Math.max(preferredSize.height - 170, 0);
// preferredSize.width = Math.max(preferredSize.width - 25, 0);
// scrollPane.setPreferredSize(preferredSize);
// } else if (doc.getFrame() != null) {
// Dimension preferredSize = doc.getFrame().getSize();
// preferredSize.height = Math.max(preferredSize.height - 170, 0);
// preferredSize.width = Math.max(preferredSize.width - 25, 0);
// scrollPane.setPreferredSize(preferredSize);
// }
// }
//
// @Override
// public void componentMoved(ComponentEvent e) {}
//
// @Override
// public void componentHidden(ComponentEvent e) {}
// });
return scrollPane;
} // createListBox
/* synchronise table with data from traitSet BEASTObject */
private void convertTraitToTableData() {
for (int i = 0; i < tableData.length; i++) {
tableData[i][0] = taxa.get(i);
tableData[i][1] = "0";
tableData[i][2] = "0";
}
String[] traits = traitSet.traitsInput.get().split(",");
for (String trait : traits) {
trait = trait.replaceAll("\\s+", " ");
String[] strs = trait.split("=");
if (strs.length != 2) {
break;
//throw new Exception("could not parse trait: " + trait);
}
String taxonID = normalize(strs[0]);
int taxonIndex = taxa.indexOf(taxonID);
// if (taxonIndex < 0) {
// throw new Exception("Trait (" + taxonID + ") is not a known taxon. Spelling error perhaps?");
// }
if (taxonIndex >= 0) {
tableData[taxonIndex][1] = normalize(strs[1]);
tableData[taxonIndex][0] = taxonID;
} else {
Log.warning.println("WARNING: File contains taxon " + taxonID + " that cannot be found in alignment");
}
}
if (traitSet.traitNameInput.get().equals(TraitSet.DATE_BACKWARD_TRAIT)) {
Double minDate = Double.MAX_VALUE;
for (int i = 0; i < tableData.length; i++) {
minDate = Math.min(minDate, parseDate((String) tableData[i][1]));
}
for (int i = 0; i < tableData.length; i++) {
tableData[i][2] = parseDate((String) tableData[i][1]) - minDate;
}
} else {
Double maxDate = 0.0;
for (int i = 0; i < tableData.length; i++) {
maxDate = Math.max(maxDate, parseDate((String) tableData[i][1]));
}
for (int i = 0; i < tableData.length; i++) {
tableData[i][2] = maxDate - parseDate((String) tableData[i][1]);
}
}
if (table != null) {
for (int i = 0; i < tableData.length; i++) {
table.setValueAt(tableData[i][1], i, 1);
table.setValueAt(tableData[i][2], i, 2);
}
}
} // convertTraitToTableData
private double parseDate(String str) {
// default, try to interpret the string as a number
try {
return Double.parseDouble(str);
} catch (NumberFormatException e) {
// does not look like a number, try parsing it as a date
if (str.matches(".*[a-zA-Z].*")) {
str = str.replace('/', '-');
}
//try {
// unfortunately this deprecated date parser is the most flexible around at the moment...
long time = Date.parse(str);
Date date = new Date(time);
// AJD
// Ideally we would use a non-deprecated method like this one instead but it seems to have
// far less support for different date formats.
// for example it fails on "12-Oct-2014"
//dateFormat.setLenient(true);
//Date date = dateFormat.parse(str);
Calendar calendar = dateFormat.getCalendar();
calendar.setTime(date);
// full year (e.g 2015)
int year = calendar.get(Calendar.YEAR);
double days = calendar.get(Calendar.DAY_OF_YEAR);
double daysInYear = 365.0;
if (calendar instanceof GregorianCalendar &&(((GregorianCalendar) calendar).isLeapYear(year))) {
daysInYear = 366.0;
}
double dateAsDecimal = year + days/daysInYear;
return dateAsDecimal;
//}
//catch (ParseException e1) {
// System.err.println("*** WARNING: Failed to parse '" + str + "' as date using dateFormat " + dateFormat);
//}
}
//return 0;
} // parseStrings
private String normalize(String str) {
if (str.charAt(0) == ' ') {
str = str.substring(1);
}
if (str.endsWith(" ")) {
str = str.substring(0, str.length() - 1);
}
return str;
}
/**
* synchronise traitSet BEAST object with table data
*/
private void convertTableDataToTrait() {
String trait = "";
for (int i = 0; i < tableData.length; i++) {
trait += taxa.get(i) + "=" + tableData[i][1];
if (i < tableData.length - 1) {
trait += ",\n";
}
}
try {
traitSet.traitsInput.setValue(trait, traitSet);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* create box with comboboxes for selectin units and trait name *
*/
private Box createButtonBox() {
Box buttonBox = Box.createHorizontalBox();
JLabel label = new JLabel("Dates specified as: ");
label.setMaximumSize(label.getPreferredSize());
buttonBox.add(label);
unitsComboBox = new JComboBox<>(TraitSet.Units.values());
unitsComboBox.setSelectedItem(traitSet.unitsInput.get());
unitsComboBox.addActionListener(e -> {
String selected = unitsComboBox.getSelectedItem().toString();
try {
traitSet.unitsInput.setValue(selected, traitSet);
//System.err.println("Traitset is now: " + m_traitSet.m_sUnits.get());
} catch (Exception ex) {
ex.printStackTrace();
}
});
Dimension d = unitsComboBox.getPreferredSize();
unitsComboBox.setMaximumSize(new Dimension(Integer.MAX_VALUE, unitsComboBox.getPreferredSize().height));
unitsComboBox.setSize(d);
buttonBox.add(unitsComboBox);
relativeToComboBox = new JComboBox<>(new String[]{"Since some time in the past", "Before the present"});
if (traitSet.traitNameInput.get().equals(TraitSet.DATE_BACKWARD_TRAIT)) {
relativeToComboBox.setSelectedIndex(1);
} else {
relativeToComboBox.setSelectedIndex(0);
}
relativeToComboBox.addActionListener(e -> {
String selected = TraitSet.DATE_BACKWARD_TRAIT;
if (relativeToComboBox.getSelectedIndex() == 0) {
selected = TraitSet.DATE_FORWARD_TRAIT;
}
try {
traitSet.traitNameInput.setValue(selected, traitSet);
Log.warning.println("Relative position is now: " + traitSet.traitNameInput.get());
} catch (Exception ex) {
ex.printStackTrace();
}
convertTraitToTableData();
});
relativeToComboBox.setMaximumSize(new Dimension(Integer.MAX_VALUE, relativeToComboBox.getPreferredSize().height));
buttonBox.add(relativeToComboBox);
buttonBox.add(Box.createHorizontalGlue());
JButton guessButton = new JButton("Guess");
guessButton.setName("Guess");
guessButton.addActionListener(e -> {
GuessPatternDialog dlg = new GuessPatternDialog(null, m_sPattern);
dlg.allowAddingValues();
String trait = "";
switch (dlg.showDialog("Guess dates")) {
case canceled:
return;
case trait:
trait = dlg.getTrait();
break;
case pattern:
for (String taxon : taxa) {
String match = dlg.match(taxon);
if (match == null) {
return;
}
double date = parseDate(match);
if (trait.length() > 0) {
trait += ",";
}
trait += taxon + "=" + date;
}
break;
}
try {
traitSet.traitsInput.setValue(trait, traitSet);
convertTraitToTableData();
convertTableDataToTrait();
} catch (Exception ex) {
// TODO: handle exception
}
refreshPanel();
});
buttonBox.add(guessButton);
JButton clearButton = new JButton("Clear");
clearButton.addActionListener(e -> {
try {
traitSet.traitsInput.setValue("", traitSet);
} catch (Exception ex) {
// TODO: handle exception
}
refreshPanel();
});
buttonBox.add(clearButton);
return buttonBox;
} // createButtonBox
}