package org.signalml.plugin.exampleplugin;
import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.InvalidClassException;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.swing.BoxLayout;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JPanel;
import javax.swing.JSpinner;
import javax.swing.SpinnerNumberModel;
import javax.swing.border.CompoundBorder;
import javax.swing.border.TitledBorder;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import org.apache.log4j.Logger;
import org.signalml.plugin.export.NoActiveObjectException;
import org.signalml.plugin.export.SignalMLException;
import org.signalml.plugin.export.signal.ExportedSignalDocument;
import org.signalml.plugin.export.signal.ExportedSignalSelectionType;
import org.signalml.plugin.export.signal.ExportedTag;
import org.signalml.plugin.export.signal.ExportedTagDocument;
import org.signalml.plugin.export.signal.ExportedTagStyle;
import org.signalml.plugin.export.signal.SvarogAccessSignal;
import org.signalml.plugin.export.signal.Tag;
import org.signalml.plugin.export.signal.TagStyle;
import org.signalml.plugin.export.view.AbstractDialog;
/**
* This dialog allows the user to create a custom (precise) tag.
* Contains fields that allow to specify parameters of the tag:
* <ul>
* <li>the {@link ExportedSignalSelectionType type} of the
* {@link ExportedTag tag},</li>
* <li>the {@link ExportedTagStyle style} of the tag,</li>
* <li>the number of the channel (which is active only if the type
* of the tag is {@code CHANNEL},</li>
* <li>the position where the tag starts (seconds),</li>
* <li>the length of the tag.</li>
* </ul>
*
* @author Marcin Szumski
*/
public class PreciseTagDialog extends AbstractDialog implements ActionListener, ChangeListener {
private static final long serialVersionUID = 1L;
/**
* the logger that is consistent with logging framework of Svarog;
* requires {@code log4j.jar} as the library
*/
private static final Logger logger = Logger.getLogger(PreciseTagDialog.class);
/**
* the {@link SvarogAccessSignal access} to signal options
*/
private SvarogAccessSignal signalAccess;
/**
* the combo-box in which the possible {@link ExportedSignalSelectionType
* types} of the tag are displayed (BLOCK, PAGE, CHANNEL)
*/
private JComboBox typesBox;
/**
* the combo-box in which the {@link ExportedTagStyle styles} of the
* currently active {@link ExportedSignalSelectionType type} are displayed
*/
private JComboBox stylesBox = new JComboBox();
/**
* the combo-box in which the names of channels of the signal are displayed
* if the currently selected {@link ExportedSignalSelectionType type}
* is a CHANNEL
*/
private JComboBox channelBox = new JComboBox();
/**
* the spinner with the position (in time - seconds) where the tag should
* start;
* <p>
* value of this spinner must be within {@code [0,length]}, where
* {@code length} is the length of the shortest channel of the signal;
* <p>
* step of this spinner depends on the selected
* {@link ExportedSignalSelectionType type}: for {@code BLOCK} it is the
* size of the block, for {@code PAGE} it is the size of the page, and for
* {@code CHANNEL} it is 1
*/
private SpinnerNumberModel startSpinnerModel;
/**
* the spinner with the length (seconds) of the {@link ExportedTag tag};
* <p>
* value of this spinner must be within {@code [0,length-start]}, where
* {@code length} is the length of the shortest channel of the signal and
* {@code start} is the {@link #startSpinnerModel position} where tag
* starts
* <p>
* step of this spinner depends on the selected
* {@link ExportedSignalSelectionType type}: for {@code BLOCK} it is the
* size of the block, for {@code PAGE} it is the size of the page, and for
* {@code CHANNEL} it is 1
*/
private SpinnerNumberModel lengthSpinnerModel;
/**
* the {@link ExportedTagDocument document} to which the created
* {@link ExportedTag tag} is to be added
*/
private ExportedTagDocument tagDocument = null;
/**
* the {@link ExportedSignalDocument document} with the signal
*/
private ExportedSignalDocument signalDocument = null;
/**
* Constructor. Sets {@link SvarogAccessSignal signal access}.
* @param signalAccess access to set
*/
public PreciseTagDialog(SvarogAccessSignal signalAccess) {
this.signalAccess = signalAccess;
}
/**
* Creates the {@link #typesBox}.
* @return the created types box
*/
private JComboBox selectionTypesBox() {
String[] types = new String[] {
ExportedSignalSelectionType.BLOCK,
ExportedSignalSelectionType.CHANNEL,
ExportedSignalSelectionType.PAGE
};
typesBox = new JComboBox(types);
typesBox.addActionListener(this);
return typesBox;
}
/**
* Creates a panel with BorderLayout and the CompoundBorder with the given
* text.
* @param name the text to be set on the border
* @return the created panel
*/
private JPanel createNamedPanel(String name) {
JPanel panel = new JPanel(new BorderLayout());
CompoundBorder cb = new CompoundBorder(
new TitledBorder(name),
null
);
panel.setBorder(cb);
return panel;
}
/**
* Creates a panel with the {@link #typesBox} and the border with
* text {@code "type"}.
* @return the created panel
*/
private JPanel createTypesPanel() {
JPanel panel = createNamedPanel("type");
panel.add(selectionTypesBox());
return panel;
}
/**
* Creates a panel with the {@link #stylesBox} and the border with
* text {@code "style"}.
* @return the created panel
*/
private JPanel createStylesPanel() {
JPanel panel = createNamedPanel("style");
panel.add(stylesBox);
return panel;
}
/**
* Creates a panel with the {@link #channelBox} and the border with
* text {@code "channel"}.
* @return the created panel
*/
private JPanel createChannelPanel() {
JPanel panel = createNamedPanel("channel");
panel.add(channelBox);
return panel;
}
/**
* Creates a panel with the spinner with model {@link #startSpinnerModel}
* and the border with text {@code "position"}.
* @return the created panel
*/
private JPanel createStartPanel() {
JPanel panel = createNamedPanel("position");
startSpinnerModel = new SpinnerNumberModel(0.0, 0.0, getSignalDocument().getMinSignalLength(), 1);
startSpinnerModel.addChangeListener(this);
JSpinner startSpinner = new JSpinner(startSpinnerModel);
panel.add(startSpinner);
return panel;
}
/**
* Creates a panel with the spinner with model {@link #lengthSpinnerModel}
* and the border with text {@code "length"}.
* @return the created panel
*/
private JPanel createLengthPanel() {
JPanel panel = createNamedPanel("length");
lengthSpinnerModel = new SpinnerNumberModel(1.0, 0.0, getSignalDocument().getMinSignalLength(), 1);
lengthSpinnerModel.addChangeListener(this);
JSpinner lenghtSpinner = new JSpinner(lengthSpinnerModel);
panel.add(lenghtSpinner);
return panel;
}
/**
* Creates a panel with box layout that contains sub-panels which allow to
* select:
* <ul>
* <li>the {@link ExportedSignalSelectionType type} of the
* {@link ExportedTag tag}</li>
* <li>the {@link ExportedTagStyle style} of the tag</li>
* <li>the number of the channel (which is active only if the type
* of the tag is {@code CHANNEL}</li>
* <li>the position where the tag starts (seconds)</li>
* <li>the length of the tag</li>
* </ul>
*/
@Override
protected JComponent createInterface() {
JPanel panel = new JPanel();
panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS));
panel.add(createTypesPanel());
JPanel styleAndChannelPanel = new JPanel();
styleAndChannelPanel.setLayout(new BoxLayout(styleAndChannelPanel, BoxLayout.LINE_AXIS));
styleAndChannelPanel.add(createStylesPanel());
styleAndChannelPanel.add(createChannelPanel());
panel.add(styleAndChannelPanel);
JPanel positionAndLengthPanel = new JPanel();
positionAndLengthPanel.setLayout(new BoxLayout(positionAndLengthPanel, BoxLayout.LINE_AXIS));
positionAndLengthPanel.add(createStartPanel());
positionAndLengthPanel.add(createLengthPanel());
panel.add(positionAndLengthPanel);
updateStylesBox();
updateChannelBox();
updateStartSpinner();
updateLenghtSpinner();
return panel;
}
/**
* There is no model for this dialog, so always true is returned.
*/
@Override
public boolean supportsModelClass(Class<?> clazz) {
return true;
}
/* (non-Javadoc)
* @see org.signalml.plugin.export.view.AbstractDialog#fillDialogFromModel(java.lang.Object)
*/
@Override
public void fillDialogFromModel(Object model) throws SignalMLException {
//nothing to do
}
/**
* Reads the parameters of the {@link Tag tag} and based on them creates a tag.
* Adds the created style to the {@link ExportedSignalDocument signal document}.
* No model is used.
*/
@Override
public void fillModelFromDialog(Object model) throws SignalMLException {
Set<ExportedTagStyle> styles = getTagDocument().getTagStyles();
String selectedStyleName = (String) stylesBox.getSelectedItem();
ExportedTagStyle selectedStyle = null;
for (ExportedTagStyle style : styles) {
if (style.getName().equals(selectedStyleName)) {
selectedStyle = style;
}
}
double length = (Double) lengthSpinnerModel.getValue();
double position = (Double) startSpinnerModel.getValue();
int channel = channelBox.getSelectedIndex();
ExportedTag tag = new Tag(new TagStyle(selectedStyle), (float) position, (float) length, channel);
try {
signalAccess.addTagToDocument(tagDocument, tag);
} catch (InvalidClassException e) {
logger.error("Tag document has not valid type. Shouldn't occur");
} catch (IllegalArgumentException e) {
logger.error("There is no such style in document. Shouldn't occur");
}
}
/**
* Updates the list of {@link ExportedTagStyle styles} in
* the {@link #stylesBox}.
* Removes all styles and adds those that have the currently selected
* {@link ExportedSignalSelectionType type}.
*/
private void updateStylesBox() {
stylesBox.removeAllItems();
String type = (String) typesBox.getSelectedItem();
Set<ExportedTagStyle> styles = getTagDocument().getTagStyles();
Set<ExportedTagStyle> selectedStyles = new HashSet<ExportedTagStyle>();
for (ExportedTagStyle style : styles) {
if (style.getType().getName().equals(type))
selectedStyles.add(style);
}
for (ExportedTagStyle style : selectedStyles) {
stylesBox.addItem(style.getName());
}
}
/**
* If the currently selected {@link ExportedSignalSelectionType type}
* is a {@code CHANNEL} adds the names of channels to {@link #channelBox}
* and sets it to be enabled.
* If the currently selected type is not a {@code CHANNEL} sets
* {@link #channelBox} to be disabled.
*/
private void updateChannelBox() {
channelBox.removeAllItems();
String type = (String) typesBox.getSelectedItem();
if (type.equals(ExportedSignalSelectionType.CHANNEL)) {
channelBox.setEnabled(true);
int channelCount = getSignalDocument().getChannelCount();
List<String> labels = getSignalDocument().getSourceChannelLabels();
for (int i = 0; i < channelCount; ++i) {
channelBox.addItem(labels.get(i));
}
} else
channelBox.setEnabled(false);
}
/**
* Called when the selected value of the {@code typesBox}
* has changed.
* Updates the styles box, the channel box, the start spinner and
* the length spinner.
*/
@Override
public void actionPerformed(ActionEvent e) {
updateStylesBox();
updateChannelBox();
updateStartSpinner();
updateLenghtSpinner();
}
/**
* Updates the {@link #lengthSpinnerModel}.
* Sets:
* <ul>
* <li>the maximal value of the spinner to be {@code maximum - position},
* where {@code maximum} is the length of the signal and {@code position}
* is the position where the tag starts,</li>
* <li>the value of the spinner, if the current value is larger then
* the maximal value of the spinner (after the change)</li>
* <li>the value of the spinner to the multiple of block/page size
* (the largest multiple that is less then the current value) if the
* currently selected type is {@code PAGE/BLOCK}</li>
* <li>the step of the spinner to the size of block/page if the
* currently selected type is {@code PAGE/BLOCK} or to 1 if
* the type is {@code CHANNEL}</li>
* </ul>
*/
private void updateLenghtSpinner() {
double position = (Double) startSpinnerModel.getValue();
double length = (Double) lengthSpinnerModel.getValue();
double maximum = (Double) startSpinnerModel.getMaximum();
String type = (String) typesBox.getSelectedItem();
if (length + position > maximum) {
length = maximum - position;
}
if (type.equals(ExportedSignalSelectionType.PAGE)) {
float pageSize = getSignalDocument().getPageSize();
int numberOfPages = (int)(length / pageSize);
length = numberOfPages * pageSize;
lengthSpinnerModel.setStepSize(pageSize);
} else if (type.equals(ExportedSignalSelectionType.BLOCK)) {
float blockSize = getSignalDocument().getBlockSize();
int numberOfPages = (int)(length / blockSize);
length = numberOfPages * blockSize;
lengthSpinnerModel.setStepSize(blockSize);
} else {
lengthSpinnerModel.setStepSize(1);
}
lengthSpinnerModel.setValue(length);
lengthSpinnerModel.setMaximum(maximum - position);
}
/**
* Updates the {@link #startSpinnerModel}.
* Sets:
* <ul>
* <li>the value of the spinner to the multiple of block/page size
* (the largest multiple that is less then the current value) if the
* currently selected type is {@code PAGE/BLOCK}</li>
* <li>the step of the spinner to the size of block/page if the
* currently selected type is {@code PAGE/BLOCK} or to 1 if
* the type is {@code CHANNEL}</li>
* </ul>
*/
private void updateStartSpinner() {
double position = (Double) startSpinnerModel.getValue();
String type = (String) typesBox.getSelectedItem();
if (type.equals(ExportedSignalSelectionType.PAGE)) {
float pageSize = getSignalDocument().getPageSize();
int numberOfAPage = (int)(position / pageSize);
position = pageSize*numberOfAPage;
startSpinnerModel.setStepSize(pageSize);
} else if (type.equals(ExportedSignalSelectionType.BLOCK)) {
float blockSize = getSignalDocument().getBlockSize();
int numberOfABlock = (int)(position / blockSize);
position = blockSize*numberOfABlock;
startSpinnerModel.setStepSize(blockSize);
} else {
startSpinnerModel.setStepSize(1);
}
startSpinnerModel.setValue(position);
}
/**
* Called when the value of {@code startSpinnerModel} changes.
* Updates the length spinner.
*/
@Override
public void stateChanged(ChangeEvent e) {
updateLenghtSpinner();
}
/**
* Getter for {@link #signalDocument}.
* If the document is {@code null} the active document is set
* as a value.
* @return the signalDocument
*/
private ExportedSignalDocument getSignalDocument() {
if (signalDocument == null) {
try {
signalDocument = signalAccess.getActiveSignalDocument();
} catch (NoActiveObjectException e) {
logger.error("no active tag or signal document while there should be one");
}
}
return signalDocument;
}
/**
* Getter for {@link #tagDocument}.
* If the document is {@code null} the active tag document is set
* as a value.
* @return the tagDocument
*/
private ExportedTagDocument getTagDocument() {
if (tagDocument == null) {
try {
tagDocument = signalAccess.getActiveTagDocument();
} catch (NoActiveObjectException e) {
logger.error("no active tag or signal document while there should be one");
}
}
return tagDocument;
}
}