package beast.app.beauti; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.util.ArrayList; import java.util.Collections; import java.util.EventObject; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.Vector; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; import javax.swing.Box; import javax.swing.JButton; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JScrollPane; import javax.swing.JTable; import javax.swing.JTextArea; import javax.swing.JTextField; import javax.swing.event.CellEditorListener; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import javax.swing.table.DefaultTableModel; import javax.swing.table.JTableHeader; import javax.swing.table.TableCellEditor; import javax.swing.table.TableCellRenderer; import beast.app.draw.InputEditor; import beast.core.BEASTInterface; import beast.core.Input; import beast.evolution.alignment.Alignment; import beast.evolution.alignment.FilteredAlignment; import beast.evolution.alignment.Sequence; import beast.evolution.alignment.Taxon; import beast.evolution.alignment.TaxonSet; public class TaxonSetInputEditor extends InputEditor.Base { private static final long serialVersionUID = 1L; List<Taxon> m_taxonset; List<Taxon> m_lineageset; Map<String, String> m_taxonMap; JTable m_table; DefaultTableModel m_model = new DefaultTableModel(); JTextField filterEntry; String m_sFilter = ".*"; int m_sortByColumn = 0; boolean m_bIsAscending = true; public TaxonSetInputEditor(BeautiDoc doc) { super(doc); } @Override public Class<?> type() { return TaxonSet.class; } @Override public void init(Input<?> input, BEASTInterface beastObject, int itemNr, ExpandOption isExpandOption, boolean addButtons) { m_input = input; m_beastObject = beastObject; this.itemNr = itemNr; TaxonSet taxonset = (TaxonSet) m_input.get(); if (taxonset == null) { return; } List<Taxon> taxonsets = new ArrayList<>(); List<Taxon> taxa = taxonset.taxonsetInput.get(); for (Taxon taxon : taxa) { taxonsets.add(taxon); } add(getContent(taxonsets)); if (taxa.size() == 1 && taxa.get(0).getID().equals("Beauti2DummyTaxonSet") || taxa.size() == 0) { taxa.clear(); try { // species is first character of taxon guessTaxonSets("(.).*", 0); for (Taxon taxonset2 : m_taxonset) { for (Taxon taxon : ((TaxonSet) taxonset2).taxonsetInput.get()) { m_lineageset.add(taxon); m_taxonMap.put(taxon.getID(), taxonset2.getID()); } } } catch (Exception e) { e.printStackTrace(); } taxonSetToModel(); modelToTaxonset(); } } private Component getContent(List<Taxon> taxonset) { m_taxonset = taxonset; m_taxonMap = new HashMap<>(); m_lineageset = new ArrayList<>(); for (Taxon taxonset2 : m_taxonset) { if (taxonset2 instanceof TaxonSet) { for (Taxon taxon : ((TaxonSet) taxonset2).taxonsetInput.get()) { m_lineageset.add(taxon); m_taxonMap.put(taxon.getID(), taxonset2.getID()); } } } // set up table. // special features: background shading of rows // custom editor allowing only Date column to be edited. m_model = new DefaultTableModel(); m_model.addColumn("Taxon"); m_model.addColumn("Species/Population"); taxonSetToModel(); m_table = new JTable(m_model) { 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.gray); } else if (Index_row % 2 == 0) { 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. m_table.setDefaultEditor(Object.class, new TableCellEditor() { JTextField m_textField = new JTextField(); int m_iRow , m_iCol; @Override public boolean stopCellEditing() { m_table.removeEditor(); String text = m_textField.getText(); //Log.warning.println(text); m_model.setValueAt(text, m_iRow, m_iCol); // try { // Double.parseDouble(text); // } catch (Exception e) { // return false; // } modelToTaxonset(); return true; } @Override public boolean isCellEditable(EventObject anEvent) { return m_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) { } }); m_table.putClientProperty("terminateEditOnFocusLost", Boolean.TRUE); m_table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF); int size = m_table.getFont().getSize(); m_table.setRowHeight(20 * size/13); m_table.getColumnModel().getColumn(0).setPreferredWidth(250 * size/13); m_table.getColumnModel().getColumn(1).setPreferredWidth(250 * size/13); JTableHeader header = m_table.getTableHeader(); header.addMouseListener(new ColumnHeaderListener()); JScrollPane pane = new JScrollPane(m_table); Box tableBox = Box.createHorizontalBox(); tableBox.add(Box.createHorizontalGlue()); tableBox.add(pane); tableBox.add(Box.createHorizontalGlue()); Box box = Box.createVerticalBox(); box.add(createFilterBox()); box.add(tableBox); box.add(createButtonBox()); return box; } private Component createButtonBox() { Box buttonBox = Box.createHorizontalBox(); JButton fillDownButton = new JButton("Fill down"); fillDownButton.setName("Fill down"); fillDownButton.setToolTipText("replaces all taxons in selection with the one that is selected at the top"); fillDownButton.addActionListener(e -> { int[] rows = m_table.getSelectedRows(); if (rows.length < 2) { return; } String taxon = (String) ((Vector<?>) m_model.getDataVector().elementAt(rows[0])).elementAt(1); for (int i = 1; i < rows.length; i++) { m_model.setValueAt(taxon, rows[i], 1); } modelToTaxonset(); }); JButton guessButton = new JButton("Guess"); guessButton.setName("Guess"); guessButton.addActionListener(e -> { guess(); }); buttonBox.add(Box.createHorizontalGlue()); buttonBox.add(fillDownButton); buttonBox.add(Box.createHorizontalGlue()); buttonBox.add(guessButton); buttonBox.add(Box.createHorizontalGlue()); return buttonBox; } public class ColumnHeaderListener extends MouseAdapter { @Override public void mouseClicked(MouseEvent evt) { // The index of the column whose header was clicked int vColIndex = m_table.getColumnModel().getColumnIndexAtX(evt.getX()); if (vColIndex == -1) { return; } if (vColIndex != m_sortByColumn) { m_sortByColumn = vColIndex; m_bIsAscending = true; } else { m_bIsAscending = !m_bIsAscending; } taxonSetToModel(); } } private void guess() { GuessPatternDialog dlg = new GuessPatternDialog(this, m_sPattern); switch(dlg.showDialog("Guess taxon sets")) { case canceled: return; case pattern: String pattern = dlg.getPattern(); try { guessTaxonSets(pattern, 0); m_sPattern = pattern; break; } catch (Exception e) { e.printStackTrace(); } case trait: String trait = dlg.getTrait(); parseTrait(trait); break; } m_lineageset.clear(); for (Taxon taxonset2 : m_taxonset) { for (Taxon taxon : ((TaxonSet) taxonset2).taxonsetInput.get()) { m_lineageset.add(taxon); m_taxonMap.put(taxon.getID(), taxonset2.getID()); } } taxonSetToModel(); modelToTaxonset(); } /** * guesses taxon sets based on pattern in regExp based on the taxa in * m_rawData */ public int guessTaxonSets(String regexp, int minSize) { m_taxonset.clear(); HashMap<String, TaxonSet> map = new HashMap<>(); Pattern m_pattern = Pattern.compile(regexp); Set<Taxon> taxa = new HashSet<>(); Set<String> taxonIDs = new HashSet<>(); for (Alignment alignment : getDoc().alignments) { for (String id : alignment.getTaxaNames()) { if (!taxonIDs.contains(id)) { Taxon taxon = getDoc().getTaxon(id); taxa.add(taxon); taxonIDs.add(id); } } for (Sequence sequence : alignment.sequenceInput.get()) { String id = sequence.taxonInput.get(); if (!taxonIDs.contains(id)) { Taxon taxon = getDoc().getTaxon(sequence.taxonInput.get()); // ensure sequence and taxon do not get same ID if (sequence.getID().equals(sequence.taxonInput.get())) { sequence.setID("_" + sequence.getID()); } taxa.add(taxon); taxonIDs.add(id); } } } List<String> unknowns = new ArrayList<>(); for (Taxon taxon : taxa) { if (!(taxon instanceof TaxonSet)) { Matcher matcher = m_pattern.matcher(taxon.getID()); String match; if (matcher.find()) { match = matcher.group(1); } else { match = "UNKNOWN"; unknowns.add(taxon.getID()); } try { if (map.containsKey(match)) { TaxonSet set = map.get(match); set.taxonsetInput.setValue(taxon, set); } else { TaxonSet set = newTaxonSet(match); set.taxonsetInput.setValue(taxon, set); map.put(match, set); } } catch (Exception ex) { ex.printStackTrace(); } } } if (unknowns.size() > 0) { showMisMatchMessage(unknowns); } // add taxon sets int ignored = 0; for (TaxonSet set : map.values()) { if (set.taxonsetInput.get().size() > minSize) { m_taxonset.add(set); } else { ignored += set.taxonsetInput.get().size(); } } return ignored; } private TaxonSet newTaxonSet(String match) { if (getDoc().taxaset.containsKey(match)) { Taxon t = doc.taxaset.get(match); if (t instanceof TaxonSet) { TaxonSet set = (TaxonSet) t; set.taxonsetInput.get().clear(); return set; } else { // TODO handle situation where taxon and set have same name (issue #135) } } TaxonSet set = new TaxonSet(); set.setID(match); return set; } void parseTrait(String trait) { Map<String,String> traitmap = new HashMap<>(); for (String line : trait.split(",")) { String [] strs = line.split("="); if (strs.length == 2) { traitmap.put(strs[0].trim(), strs[1].trim()); } } m_taxonset.clear(); Set<Taxon> taxa = new HashSet<>(); Set<String> taxonIDs = new HashSet<>(); for (Alignment alignment : getDoc().alignments) { if (alignment instanceof FilteredAlignment) { alignment = ((FilteredAlignment)alignment).alignmentInput.get(); } for (String id : alignment.getTaxaNames()) { if (!taxonIDs.contains(id)) { Taxon taxon = getDoc().getTaxon(id); taxa.add(taxon); taxonIDs.add(id); } } } HashMap<String, TaxonSet> map = new HashMap<>(); List<String> unknowns = new ArrayList<>(); for (Taxon taxon : taxa) { if (!(taxon instanceof TaxonSet)) { String match = traitmap.get(taxon.getID()); if (match == null) { match = "UNKNOWN"; unknowns.add(taxon.getID()); } try { if (map.containsKey(match)) { TaxonSet set = map.get(match); set.taxonsetInput.setValue(taxon, set); } else { TaxonSet set = newTaxonSet(match); set.taxonsetInput.setValue(taxon, set); map.put(match, set); } } catch (Exception ex) { ex.printStackTrace(); } } } // add taxon sets for (TaxonSet set : map.values()) { m_taxonset.add(set); } if (unknowns.size() > 0) { showMisMatchMessage(unknowns); } } private void showMisMatchMessage(List<String> unknowns) { JOptionPane.showMessageDialog( this, "Some taxa did not have a match and are set to UNKNOWN:\n" + unknowns.toString().replaceAll(",", "\n"), "Warning", JOptionPane.INFORMATION_MESSAGE); } String m_sPattern = "^(.+)[-_\\. ](.*)$"; private Component createFilterBox() { Box filterBox = Box.createHorizontalBox(); filterBox.add(new JLabel("filter: ")); // Dimension size = new Dimension(100,20); filterEntry = new JTextField(); filterEntry.setColumns(20); // filterEntry.setMinimumSize(size); // filterEntry.setPreferredSize(size); // filterEntry.setSize(size); filterEntry.setToolTipText("Enter regular expression to match taxa"); int size = filterEntry.getFont().getSize(); filterEntry.setMaximumSize(new Dimension(1024, 20 * size/13)); filterBox.add(filterEntry); filterBox.add(Box.createHorizontalGlue()); filterEntry.getDocument().addDocumentListener(new DocumentListener() { @Override public void removeUpdate(DocumentEvent e) { processFilter(); } @Override public void insertUpdate(DocumentEvent e) { processFilter(); } @Override public void changedUpdate(DocumentEvent e) { processFilter(); } private void processFilter() { String filter = ".*" + filterEntry.getText() + ".*"; try { // sanity check: make sure the filter is legit filter.matches(filter); m_sFilter = filter; taxonSetToModel(); m_table.repaint(); } catch (PatternSyntaxException e) { // ignore } } }); return filterBox; } /** * for convert taxon sets to table model * */ @SuppressWarnings("unchecked") private void taxonSetToModel() { // clear table model while (m_model.getRowCount() > 0) { m_model.removeRow(0); } // fill table model with lineages matching the filter for (String lineageID : m_taxonMap.keySet()) { if (lineageID.matches(m_sFilter)) { Object[] rowData = new Object[2]; rowData[0] = lineageID; rowData[1] = m_taxonMap.get(lineageID); m_model.addRow(rowData); } } @SuppressWarnings("rawtypes") Vector data = m_model.getDataVector(); Collections.sort(data, (Vector<?> v1, Vector<?> v2) -> { String o1 = (String) v1.get(m_sortByColumn); String o2 = (String) v2.get(m_sortByColumn); if (o1.equals(o2)) { o1 = (String) v1.get(1 - m_sortByColumn); o2 = (String) v2.get(1 - m_sortByColumn); } if (m_bIsAscending) { return o1.compareTo(o2); } else { return o2.compareTo(o1); } } ); m_model.fireTableRowsInserted(0, m_model.getRowCount()); } /** * for convert table model to taxon sets * */ private void modelToTaxonset() { // update map for (int i = 0; i < m_model.getRowCount(); i++) { String lineageID = (String) ((Vector<?>) m_model.getDataVector().elementAt(i)).elementAt(0); String taxonSetID = (String) ((Vector<?>) m_model.getDataVector().elementAt(i)).elementAt(1); // new taxon set? if (!m_taxonMap.containsValue(taxonSetID)) { // create new taxon set TaxonSet taxonset = newTaxonSet(taxonSetID); m_taxonset.add(taxonset); } m_taxonMap.put(lineageID, taxonSetID); } // clear old taxon sets for (Taxon taxon : m_taxonset) { TaxonSet set = (TaxonSet) taxon; set.taxonsetInput.get().clear(); doc.registerPlugin(set); } // group lineages with their taxon sets for (String lineageID : m_taxonMap.keySet()) { for (Taxon taxon : m_lineageset) { if (taxon.getID().equals(lineageID)) { String taxonSet = m_taxonMap.get(lineageID); for (Taxon taxon2 : m_taxonset) { TaxonSet set = (TaxonSet) taxon2; if (set.getID().equals(taxonSet)) { try { set.taxonsetInput.setValue(taxon, set); } catch (Exception e) { e.printStackTrace(); } } } } } } // remove unused taxon sets for (int i = m_taxonset.size() - 1; i >= 0; i--) { if (((TaxonSet) m_taxonset.get(i)).taxonsetInput.get().size() == 0) { doc.unregisterPlugin(m_taxonset.get(i)); m_taxonset.remove(i); } } TaxonSet taxonset = (TaxonSet) m_input.get(); taxonset.taxonsetInput.get().clear(); for (Taxon taxon : m_taxonset) { try { taxonset.taxonsetInput.setValue(taxon, taxonset); } catch (Exception e) { e.printStackTrace(); } } } }