ObservationListPane.java
/**
* VStar: a statistical analysis tool for variable star data.
* Copyright (C) 2009 AAVSO (http://www.aavso.org/)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.aavso.tools.vstar.ui.pane.list;
import java.awt.BorderLayout;
import java.awt.CardLayout;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import javax.swing.BorderFactory;
import javax.swing.BoxLayout;
import javax.swing.ButtonGroup;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTable;
import javax.swing.ListSelectionModel;
import javax.swing.RowFilter;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.table.TableColumnModel;
import javax.swing.table.TableRowSorter;
import org.aavso.tools.vstar.data.IOrderedObservationSource;
import org.aavso.tools.vstar.data.SeriesType;
import org.aavso.tools.vstar.data.ValidObservation;
import org.aavso.tools.vstar.data.filter.IFilterDescription;
import org.aavso.tools.vstar.ui.dialog.ITextComponent;
import org.aavso.tools.vstar.ui.dialog.MessageBox;
import org.aavso.tools.vstar.ui.dialog.TextDialog;
import org.aavso.tools.vstar.ui.dialog.TextField;
import org.aavso.tools.vstar.ui.mediator.AnalysisType;
import org.aavso.tools.vstar.ui.mediator.Mediator;
import org.aavso.tools.vstar.ui.mediator.message.FilteredObservationMessage;
import org.aavso.tools.vstar.ui.mediator.message.MultipleObservationSelectionMessage;
import org.aavso.tools.vstar.ui.mediator.message.ObservationSelectionMessage;
import org.aavso.tools.vstar.ui.model.list.InvalidObservationTableModel;
import org.aavso.tools.vstar.ui.model.list.ValidObservationTableModel;
import org.aavso.tools.vstar.util.comparator.DoubleAsStringComparator;
import org.aavso.tools.vstar.util.locale.LocaleProps;
import org.aavso.tools.vstar.util.notification.Listener;
import org.aavso.tools.vstar.util.prefs.NumericPrecisionPrefs;
/**
* This class represents a GUI component that renders information about
* observation data, including one or both of valid and invalid observation
* data. If both are present, they are rendered as tables in a vertical split
* pane. Otherwise, a single table will appear.
*
* CLKotnik perhaps add JD/HJD/BJD column?
*/
@SuppressWarnings("serial")
public class ObservationListPane extends JPanel implements
ListSelectionListener {
private JTable validDataTable;
private JTable invalidDataTable;
private ValidObservationTableModel validDataModel;
private TableRowSorter<ValidObservationTableModel> rowSorter;
private VisibleSeriesRowFilter rowFilter;
private RowFilter<IOrderedObservationSource, Integer> currFilter;
private JButton selectAllButton;
private JButton createFilterButton;
private ListSearchPane<ValidObservationTableModel> searchPanel;
private VeLaListSearchPane<ValidObservationTableModel> velaSearchPanel;
private Set<ValidObservation> selectedObs;
private ValidObservation lastObSelected = null;
/**
* Constructor
*
* @param title
* The title for the table.
* @param validDataModel
* A table data model that encapsulates valid observations.
* @param invalidDataModel
* A table data model that encapsulates invalid observations.
* @param enableAutoResize
* Enable auto-resize of columns? If true, we won't get a
* horizontal scrollbar for valid observation table. The source
* of column information for the table.
* @param analysisType
* The analysis type (raw, phase) under which this table was
* created.
*/
public ObservationListPane(String title,
ValidObservationTableModel validDataModel,
InvalidObservationTableModel invalidDataModel,
boolean enableAutoResize, Set<SeriesType> initialVisibleSeries,
AnalysisType analysisType) {
super(new BorderLayout());
selectedObs = new LinkedHashSet<ValidObservation>();
JScrollPane validDataScrollPane = null;
if (validDataModel != null) {
this.validDataModel = validDataModel;
validDataTable = new JTable(validDataModel);
if (!enableAutoResize) {
// Ensure we get a horizontal scrollbar if necessary rather than
// trying to cram all the columns into the visible pane.
validDataTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
}
// Set selection mode to be row-only.
// These appear to be the defaults anyway.
validDataTable.setColumnSelectionAllowed(false);
validDataTable.setRowSelectionAllowed(true);
// Enable table sorting by clicking on a column.
// We need to treat JD, magnitude, and uncertainty as doubles.
rowSorter = new TableRowSorter<ValidObservationTableModel>(
validDataModel);
int jdColIndex = validDataModel.getColumnInfoSource()
.getColumnIndexByName(LocaleProps.get("TIME"));
rowSorter.setComparator(jdColIndex, new DoubleAsStringComparator());
int magColIndex = validDataModel.getColumnInfoSource()
.getColumnIndexByName("Magnitude");
rowSorter
.setComparator(magColIndex, new DoubleAsStringComparator());
int uncertaintyColIndex = validDataModel.getColumnInfoSource()
.getColumnIndexByName("Uncertainty");
rowSorter.setComparator(uncertaintyColIndex,
new DoubleAsStringComparator());
validDataTable.setRowSorter(rowSorter);
// Add a row filter that shows data from series that are visible in
// the main plot.
rowFilter = new VisibleSeriesRowFilter(validDataModel,
initialVisibleSeries, analysisType);
rowSorter.setRowFilter(rowFilter);
validDataScrollPane = new JScrollPane(validDataTable);
}
JScrollPane invalidDataScrollPane = null;
if (invalidDataModel != null) {
invalidDataTable = new JTable(invalidDataModel);
// Ensure we get a horizontal scrollbar if necessary rather than
// trying to cram all the columns into the visible pane. This is
// particularly pertinent to the invalid data table since one of
// its columns contains the original line in the case of a file
// source.
invalidDataTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
// Set the columns containing the observation and error to be
// greater than the default width of the others.
TableColumnModel colModel = invalidDataTable.getColumnModel();
int totalWidth = colModel.getTotalColumnWidth();
colModel.getColumn(1).setPreferredWidth((int) (totalWidth * 2.5));
colModel.getColumn(2).setPreferredWidth((int) (totalWidth * 2));
// Enable table sorting by clicking on a column.
invalidDataTable.setAutoCreateRowSorter(true);
invalidDataScrollPane = new JScrollPane(invalidDataTable);
}
// In the presence of both valid and invalid data, we put
// them into a split pane.
if (validDataScrollPane != null && invalidDataScrollPane != null) {
JSplitPane splitter = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
splitter.setToolTipText("Valid and invalid data");
splitter.setTopComponent(validDataScrollPane);
splitter.setBottomComponent(invalidDataScrollPane);
splitter.setResizeWeight(0.5);
// splitter.setBorder(BorderFactory.createTitledBorder(title));
this.add(splitter, BorderLayout.CENTER);
} else if (validDataScrollPane != null) {
// Just valid data.
// validDataScrollPane.setBorder(BorderFactory.createTitledBorder(title));
this.add(validDataScrollPane, BorderLayout.CENTER);
} else if (invalidDataScrollPane != null) {
// Just invalid data.
// invalidDataScrollPane.setBorder(BorderFactory.createTitledBorder(title));
this.add(invalidDataScrollPane, BorderLayout.CENTER);
} else {
// We have no data at all. Let's say so.
JLabel label = new JLabel("There is no data to be displayed");
label.setHorizontalAlignment(JLabel.CENTER);
this.setLayout(new BorderLayout());
this.add(label, BorderLayout.CENTER);
}
this.add(createControlPanel(), BorderLayout.NORTH);
// Listen for observation selection events. Notice that this class
// also generates these, but ignores them if sent by itself.
Mediator.getInstance().getObservationSelectionNotifier()
.addListener(createObservationSelectionListener());
// List row selection handling.
this.validDataTable.getSelectionModel().addListSelectionListener(this);
}
/**
* Create a control panel for the table.
*/
private JPanel createControlPanel() {
JPanel panel = new JPanel();
panel.setLayout(new BoxLayout(panel, BoxLayout.LINE_AXIS));
panel.setBorder(BorderFactory.createEtchedBorder());
JPanel controlPane = new JPanel();
controlPane.setLayout(new BoxLayout(controlPane, BoxLayout.PAGE_AXIS));
// A checkbox to determine whether to display all the data in the table.
JCheckBox allDataCheckBox = new JCheckBox("Show all data?");
allDataCheckBox.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
JCheckBox checkBox = (JCheckBox) e.getSource();
if (checkBox.isSelected()) {
// Show all data, no filtering, no search permitted.
currFilter = null;
searchPanel.disable();
velaSearchPanel.disable();
} else {
// Filter the data displayed, permit search.
currFilter = rowFilter;
searchPanel.enable();
velaSearchPanel.enable();
}
rowSorter.setRowFilter(currFilter);
}
});
allDataCheckBox.setSelected(false);
controlPane.add(allDataCheckBox);
// Selectable search pane: pattern and VeLa search
JPanel selectableSearchPanes = new JPanel(new CardLayout());
searchPanel = new ListSearchPane<ValidObservationTableModel>(
validDataModel, rowSorter);
selectableSearchPanes.add(searchPanel, "Regex");
velaSearchPanel = new VeLaListSearchPane<ValidObservationTableModel>(
validDataModel, rowSorter);
selectableSearchPanes.add(velaSearchPanel, "VeLa");
JRadioButton patternSearchSelector = new JRadioButton("Regular Expression");
patternSearchSelector.setSelected(true);
patternSearchSelector.addActionListener(e -> {
CardLayout cl = (CardLayout) selectableSearchPanes.getLayout();
cl.show(selectableSearchPanes, "Regex");
});
JRadioButton velaSearchSelector = new JRadioButton("VeLa Expression");
velaSearchSelector.addActionListener(e -> {
CardLayout cl = (CardLayout) selectableSearchPanes.getLayout();
cl.show(selectableSearchPanes, "VeLa");
});
ButtonGroup searchSelectionRadioButtons = new ButtonGroup();
searchSelectionRadioButtons.add(patternSearchSelector);
searchSelectionRadioButtons.add(velaSearchSelector);
controlPane.add(patternSearchSelector);
controlPane.add(velaSearchSelector);
panel.add(controlPane);
JPanel searchPane = new JPanel();
searchPane.setLayout(new BoxLayout(searchPane, BoxLayout.PAGE_AXIS));
searchPane.setBorder(BorderFactory.createTitledBorder("Search"));
searchPane.add(selectableSearchPanes);
panel.add(searchPane);
final JPanel parent = this;
JPanel selectPane = new JPanel();
selectPane.setLayout(new BoxLayout(selectPane, BoxLayout.PAGE_AXIS));
selectAllButton = new JButton(LocaleProps.get("SELECT_ALL"));
selectAllButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
selectAll();
}
});
selectPane.add(selectAllButton);
createFilterButton = new JButton(
LocaleProps.get("CREATE_SELECTION_FILTER"));
createFilterButton.setEnabled(false);
createFilterButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
// Request a name for the filter.
String defaultName = Mediator.getInstance()
.getDocumentManager().getNextUntitledFilterName();
List<ITextComponent<String>> fields = new ArrayList<ITextComponent<String>>();
fields.add(new TextField("Name", defaultName, false, false));
final TextDialog nameDlg = new TextDialog("Filter Name", fields);
// Create an observation filter message and notify listeners.
IFilterDescription desc = new IFilterDescription() {
@Override
public boolean isParsable() {
return false;
}
@Override
public String getFilterName() {
return nameDlg.getTextStrings().get(0);
}
@Override
public String getFilterDescription() {
// Return a machine-readable (able to be parsed)
// representation.
StringBuffer buf = new StringBuffer();
int i = 0;
for (ValidObservation ob : selectedObs) {
String jdStr = NumericPrecisionPrefs.formatTime(ob
.getJD());
buf.append("JD = " + jdStr);
if (i < selectedObs.size() - 1) {
buf.append(" OR\n");
i++;
}
}
return buf.toString();
}
};
FilteredObservationMessage message = new FilteredObservationMessage(
parent, desc, new LinkedHashSet<ValidObservation>(
selectedObs));
Mediator.getInstance().getFilteredObservationNotifier()
.notifyListeners(message);
}
});
selectPane.add(createFilterButton);
panel.add(selectPane);
return panel;
}
/**
* Select all observations visible in the list.
*/
public void selectAll() {
validDataTable.selectAll();
ListSelectionModel selModel = validDataTable.getSelectionModel();
valueChanged(new ListSelectionEvent(selModel,
selModel.getMinSelectionIndex(),
selModel.getMaxSelectionIndex(), false));
}
/**
* @return the validDataTable
*/
public JTable getValidDataTable() {
return validDataTable;
}
/**
* @return the invalidDataTable
*/
public JTable getInvalidDataTable() {
return invalidDataTable;
}
/**
* @return the lastObSelected
*/
public ValidObservation getLastObSelected() {
return lastObSelected;
}
/**
* Retrieve the valid observations that are currently in the table's view.
*
* @return The observation list.
*/
public List<ValidObservation> getObservationsInView() {
List<ValidObservation> obs = new ArrayList<ValidObservation>();
// TODO: Is there some other way of getting the in-view objects? Ask the
// rowfilter for or to keep a list of the current indices?
validDataTable.selectAll();
for (int row : validDataTable.getSelectedRows()) {
row = validDataTable.convertRowIndexToModel(row);
obs.add(validDataModel.getObservations().get(row));
}
return obs;
}
// Returns an observation selection listener.
protected Listener<ObservationSelectionMessage> createObservationSelectionListener() {
final JPanel parent = this;
return new Listener<ObservationSelectionMessage>() {
public void update(ObservationSelectionMessage message) {
if (message.getSource() != parent) {
ValidObservation ob = message.getObservation();
Integer rowIndex = validDataModel
.getRowIndexFromObservation(ob);
if (rowIndex != null && rowIndex >= 0) {
try {
// Convert to view index!
rowIndex = validDataTable
.convertRowIndexToView(rowIndex);
if (rowIndex >= 0
&& rowIndex < validDataTable.getRowCount()) {
// Scroll to an arbitrary column (zeroth) within
// the selected row, then select that row.
// Assumption: we are specifying the zeroth cell
// within row i as an x,y coordinate relative to
// the top of the table pane.
// Note that we could call this on the scroll
// pane, which would then forward the request to
// the table pane anyway.
int colWidth = (int) validDataTable
.getCellRect(rowIndex, 0, true)
.getWidth();
int rowHeight = validDataTable
.getRowHeight(rowIndex);
validDataTable
.scrollRectToVisible(new Rectangle(
colWidth, rowHeight * rowIndex,
colWidth, rowHeight));
try {
validDataTable.setRowSelectionInterval(
rowIndex, rowIndex);
} catch (IllegalArgumentException e) {
// We ignore this since this is entirely
// possible when filtering is enabled.
}
lastObSelected = ob;
}
} catch (ArrayIndexOutOfBoundsException e) {
String msg = "Could not select row with index "
+ rowIndex
+ " (table model: "
+ validDataModel.getColumnInfoSource()
.getClass().getSimpleName() + ")";
MessageBox.showMessageDialog(Mediator.getUI()
.getComponent(),
"Observation List Index Error", msg);
}
}
}
}
public boolean canBeRemoved() {
return true;
}
};
}
/**
* We send an observation selection message when the value or values have
* "settled". This event could be consumed by other views such as plots or
* undo managers.
*
* @param e
* The list selection event.
*/
public void valueChanged(ListSelectionEvent e) {
if (e.getSource() == validDataTable.getSelectionModel()
&& validDataTable.getRowSelectionAllowed()
&& !e.getValueIsAdjusting()) {
int[] rows = validDataTable.getSelectedRows();
if (rows.length > 1) {
// This is a multiple observation selection.
List<ValidObservation> obs = new ArrayList<ValidObservation>();
for (int row : rows) {
row = validDataTable.convertRowIndexToModel(row);
ValidObservation ob = validDataModel.getObservations().get(
row);
obs.add(ob);
}
MultipleObservationSelectionMessage message = new MultipleObservationSelectionMessage(
obs, this);
Mediator.getInstance()
.getMultipleObservationSelectionNotifier()
.notifyListeners(message);
selectedObs.clear();
selectedObs.addAll(obs);
createFilterButton.setEnabled(true);
} else {
// This is a single observation selection.
int row = validDataTable.getSelectedRow();
if (row >= 0) {
row = validDataTable.convertRowIndexToModel(row);
ValidObservation ob = validDataModel.getObservations().get(
row);
lastObSelected = ob;
ObservationSelectionMessage message = new ObservationSelectionMessage(
ob, this);
Mediator.getInstance().getObservationSelectionNotifier()
.notifyListeners(message);
selectedObs.clear();
selectedObs.add(ob);
createFilterButton.setEnabled(true);
} else {
createFilterButton.setEnabled(false);
}
}
}
}
}