/* SourceMontage.java created 2007-11-22
*
*/
package org.signalml.domain.montage;
import static org.signalml.app.util.i18n.SvarogI18n._;
import static org.signalml.app.util.i18n.SvarogI18n._R;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import javax.swing.event.EventListenerList;
import org.apache.log4j.Logger;
import org.signalml.app.document.signal.SignalDocument;
import org.signalml.domain.montage.system.ChannelFunction;
import org.signalml.domain.montage.system.EegElectrode;
import org.signalml.domain.montage.system.EegSystem;
import org.signalml.domain.montage.system.EegSystemName;
import org.signalml.domain.montage.system.IChannelFunction;
import org.signalml.domain.signal.samplesource.MultichannelSampleSource;
import org.signalml.exception.SanityCheckException;
import org.signalml.util.Util;
import com.thoughtworks.xstream.annotations.XStreamAlias;
/**
* This class represents a source montage.
* Source montage consists of a list of {@link SourceChannel source channels},
* out of which every one has an assigned {@link Channel function (location)}.
* This class has also assigned listeners informing about changes in it.
*
* @author Michal Dobaczewski © 2007-2008 CC Otwarte Systemy Komputerowe Sp. z o.o.
*/
@XStreamAlias("sourcemontage")
public class SourceMontage {
public static final String CHANGED_PROPERTY = "changed";
protected static final Logger logger = Logger.getLogger(SourceMontage.class);
/**
* The {@link EegSystem} used by this {@link SourceMontage}.
*/
private transient EegSystem eegSystem;
/**
* The name of the {@link EegSystem} used by this SourceMontage.
* It is used only for reading/storing the SourceMontage in files.
*/
private EegSystemName eegSystemName;
/**
* a list of SourceChannels in this SourceMontage
*/
protected ArrayList<SourceChannel> sourceChannels = new ArrayList<SourceChannel>();;
/**
* {@link SignalTypeConfigurer configurer} for a signal type
*/
private transient SignalConfigurer signalConfigurer = new SignalConfigurer();
/**
* HashMap associating {@link SourceChannel source channels}
* with their labels
*/
private transient HashMap<String,SourceChannel> sourceChannelsByLabel;
/**
* list of EventListeners associated with this source montage
*/
protected transient EventListenerList listenerList = new EventListenerList();
/**
* PropertyChangeSupport associated with this source montage
*/
protected transient PropertyChangeSupport pcSupport = new PropertyChangeSupport(this);
/**
* informs whether this source montage has been changed
*/
private transient boolean changed = false;
public SourceMontage() {
}
/**
* Constructor. Creates a SourceMontage for a given
* {@link SignalType type} of a signal with channelCount empty channels.
* @param signalType a type of a signal
* @param channelCount a number of channels to be created
* @throws SanityCheckException thrown when addSourceChannel fails
* because of duplicate labels/functions.
* Means there is an error in code
*/
//TODO moim zdaniem ten wyjątek nie ma prawa być wyrzucony
public SourceMontage(int channelCount) {
try {
for (int i=0; i<channelCount; i++) {
addSourceChannel("L" + (i+1), signalConfigurer.genericChannel());
}
} catch (MontageException ex) {
throw new SanityCheckException(_("Failed to build default source montage"), ex);
}
}
/**
* Constructor. Creates a SourceMontage from a given
* {@link SignalDocument document} with a signal.
* @param document a document with a signal
* @throws SanityCheckException if {@link #addSourceChannel} fails
* because of duplicate labels/functions.
* Means there is error in code.
*/
public SourceMontage(SignalDocument document) {
MultichannelSampleSource mss = document.getSampleSource();
for (int i=0; i<mss.getChannelCount(); i++) {
String label = mss.getLabel(i);
if (getSourceChannelsByLabel().containsKey(label)) {
logger.warn("WARNING! Duplicate label [" + label + "]");
label = getNewSourceChannelLabel(label);
logger.debug("Changed to [" + label + "]");
}
try {
IChannelFunction channelFunction = ChannelFunction.UNKNOWN;
if (document.isMontageCreated())
channelFunction = document.getMontage().getSourceChannelAt(i).getFunction();
addSourceChannel(label, channelFunction);
} catch (MontageException ex) {
throw new SanityCheckException(_("addSourceChannel still failed"));
}
}
if (document.isMontageCreated())
this.setEegSystem(document.getMontage().getEegSystem());
}
/**
* Copy constructor.
* @param montage a SourceMontage to be copied
*/
public SourceMontage(SourceMontage montage) {
super();
copyFrom(montage);
}
/**
* Copies the given SourceMontage parameters to this source montage.
* {@link #sourceChannels Source channels} are also copied.
* {@link #listenerList Listeners} are not copied.
* @param montage a SourceMontage which parameters are to be copied
*/
protected void copyFrom(SourceMontage montage) {
setChanged(montage.changed);
sourceChannels = new ArrayList<SourceChannel>(montage.sourceChannels.size());
for (SourceChannel channel : montage.sourceChannels) {
SourceChannel newChannel = new SourceChannel(channel);
sourceChannels.add(newChannel);
}
this.eegSystemName = montage.eegSystemName;
this.eegSystem = montage.eegSystem;
}
/**
* Checks if this montage has the same number of source channels
* as signal in a given {@link SignalDocument document}.
* @param document a document with signal to be compared with
* this source montage
* @return true if a number of {@link SourceChannel source channels}
* is the same, false otherwise
*/
public boolean isCompatible(SignalDocument document) {
return(document.getChannelCount() == getSourceChannelCount());
}
/**
* Checks if this source montage has the same number of
* {@link SourceChannel source channels} as given SourceMontage object.
* @param montage a SourceMontage to be compared with this source
* montage
* @return true if number of source channels is the same, false otherwise
*/
public boolean isCompatible(SourceMontage montage) {
return(montage.getSourceChannelCount() == getSourceChannelCount());
}
/**
* Adapts this source montage to a given {@link SignalDocument document}
* with a signal.
* Changes the number of {@link SourceChannel source channels}
* (adds from file or removes excessive) to make it equal with the
* number of channels in a document.
* @param document a document with a signal to which this source montage
* is to be adapted
*/
public void adapt(SignalDocument document) {
int dCnt = document.getChannelCount();
int mCnt = getSourceChannelCount();
if (dCnt > mCnt) {
MultichannelSampleSource mss = document.getSampleSource();
for (int i=mCnt; i<dCnt; i++) {
try {
addSourceChannel(getNewSourceChannelLabel(mss.getLabel(i)), null);
} catch (MontageException ex) {
throw new SanityCheckException(ex);
}
}
} else if (dCnt < mCnt) {
for (int i=mCnt-1; i>=dCnt; i--) {
IChannelFunction function = this.getSourceChannelAt(i).getFunction();
if (function == ChannelFunction.ONE || function == ChannelFunction.ZERO)
continue;
removeLastSourceChannel();
}
}
}
/**
* Returns the {@link EegSystem} used by this Montage.
* @return the {@link EegSystem} used by this Montage
*/
public EegSystem getEegSystem() {
return eegSystem;
}
/**
* Sets the {@link EegSystem} to be used by this Montage.
* @param eegSystem the {@link EegSystem} to be used by this Montage
*/
public void setEegSystem(EegSystem eegSystem) {
if (this.eegSystem == eegSystem)
return;
this.eegSystem = eegSystem;
if (eegSystem == null) {
eegSystemName = null;
return;
}
else {
eegSystemName = eegSystem.getEegSystemName();
}
for (SourceChannel sourceChannel: sourceChannels) {
refreshElectrodeAndFunctionForSourceChannel(sourceChannel);
}
fireSourceMontageEegSystemChanged(this);
}
/**
* Checks if an {@link EegElectrode} having the same name is available
* in the current {@link EegSystem}. If so, it sets the channel function
* to {@link ChannelFunction#EEG} and associates the electrode with the channel.
* Otherwise the channel function is set to {@link ChannelFunction#UNKNOWN}.
* @param sourceChannel the {@link SourceChannel} to be refreshed
*/
protected void refreshElectrodeAndFunctionForSourceChannel(SourceChannel sourceChannel) {
if (sourceChannel == null)
return;
if (eegSystem == null)
return;
EegElectrode electrodeForChannel = eegSystem.getElectrode(sourceChannel.getLabel());
if (electrodeForChannel != null) {
sourceChannel.setEegElectrode(electrodeForChannel);
sourceChannel.setFunction(ChannelFunction.EEG);
}
else {
sourceChannel.setEegElectrode(null);
if (sourceChannel.getFunction() == ChannelFunction.EEG) {
sourceChannel.setFunction(ChannelFunction.UNKNOWN);
}
}
}
/**
* Informs whether this SourceMontage has been changed.
* @return true if this SourceMontage has been changed, false otherwise
*/
public boolean isChanged() {
return changed;
}
/**
* Sets {@link #changed <code>changed</code>} parameter.
* @param changed a changed parameter to be set
*/
public void setChanged(boolean changed) {
if (this.changed != changed) {
this.changed = changed;
pcSupport.firePropertyChange(CHANGED_PROPERTY, !changed, changed);
}
}
/**
* Returns the {@link SignalTypeConfigurer configurer} for
* this source montage.
* @return the configurer for this source montage
*/
public SignalConfigurer getSignalTypeConfigurer() {
return signalConfigurer;
}
/**
* Returns list of {@link SourceChannel source channels} with a
* given {@link Channel function}.
* @param function a function that source channels should fulfil
* @return list of source channels with a given function
*/
protected LinkedList<SourceChannel> getSourceChannelsByFunctionList(IChannelFunction function) {
LinkedList<SourceChannel> list = new LinkedList<SourceChannel>();
for (SourceChannel channel: sourceChannels) {
if (channel.getFunction() == function)
list.add(channel);
}
return list;
}
/**
* Returns a HashMap associating {@link SourceChannel source channels}
* with their labels.
* If doesn't exist it is created.
* @return a HashMap associating source channels with their labels.
*/
protected HashMap<String,SourceChannel> getSourceChannelsByLabel() {
if (sourceChannelsByLabel == null) {
sourceChannelsByLabel = new HashMap<String, SourceChannel>();
for (SourceChannel channel : sourceChannels) {
sourceChannelsByLabel.put(channel.getLabel(), channel);
}
}
return sourceChannelsByLabel;
}
/**
* Returns a {@link SourceChannel source channel} of a given label.
* @param label a label of source channel to be found
* @return the found source channel
*/
public SourceChannel getSourceChannelByLabel(String label) {
return getSourceChannelsByLabel().get(label);
}
/**
* Returns the number of {@link SourceChannel source channels}
* @return the number of source channels
*/
public int getSourceChannelCount() {
return sourceChannels.size();
}
/**
* Finds the label of a {@link SourceChannel source channel} of a
* given index.
* @param index an index of source channel to be found
* @return the label of a source channel of a given index
*/
public String getSourceChannelLabelAt(int index) {
return sourceChannels.get(index).getLabel();
}
public SourceChannel getSourceChannelAt(int index) {
return sourceChannels.get(index);
}
/**
* Returns the function of a {@link SourceChannel source channel} of
* a given index.
* @param index an index of a source channel to be found
* @return the function of a source channel of a given index
*/
public IChannelFunction getSourceChannelFunctionAt(int index) {
return sourceChannels.get(index).getFunction();
}
/**
* Finds a {@link SourceChannel source channel} of a given index and
* changes its label to a given value
* @param index an index of source channel
* @param label a String with a unique label to be set
* @return old a label of found source channel
* @throws MontageException thrown when the label empty, not unique
* or containing invalid characters
*/
public String setSourceChannelLabelAt(int index, String label) throws MontageException {
if (label == null || label.isEmpty()) {
throw new MontageException(_("Source channel label cannot be empty!"));
}
if (Util.hasSpecialChars(label)) {
throw new MontageException(_("Source channel labels contains bad characters!"));
}
SourceChannel channel = sourceChannels.get(index);
String oldLabel = channel.getLabel();
HashMap<String, SourceChannel> map = getSourceChannelsByLabel();
if (!oldLabel.equals(label)) {
SourceChannel namedChannel = map.get(label);
if (namedChannel != null && namedChannel != channel) {
throw new MontageException(_("Source channel label cannot be duplicated!"));
}
channel.setLabel(label);
map.remove(oldLabel);
map.put(label, channel);
refreshElectrodeAndFunctionForSourceChannel(channel);
fireSourceMontageChannelChanged(this, channel.getChannel());
setChanged(true);
}
return oldLabel;
}
/**
* Finds a {@link SourceChannel source channel} of a given index and
* changes its {@link Channel function} to a given value.
* @param index an index of source channel
* @param function a Channel object with a new function for a SourceChannel
* @return old a function of found source channel
* @throws MontageException if the function is not unique
*/
public IChannelFunction setSourceChannelFunctionAt(int index, IChannelFunction function) throws MontageException {
SourceChannel channel = sourceChannels.get(index);
IChannelFunction oldFunction = channel.getFunction();
if (oldFunction != function) {
LinkedList<SourceChannel> list = getSourceChannelsByFunctionList(function);
if (function.isUnique() && !list.isEmpty()) {
throw new MontageException(_("Channels with this function cannot be duplicated."));
}
if (!oldFunction.isMutable()) {
throw new MontageException(_("Channel with this function are immutable - their function cannot be changed"));
}
LinkedList<SourceChannel> oldList = getSourceChannelsByFunctionList(oldFunction);
oldList.remove(channel);
channel.setFunction(function);
list.add(channel);
fireSourceMontageChannelChanged(this, channel.getChannel());
setChanged(true);
}
return oldFunction;
}
/**
* Returns an array of indexes of {@link SourceChannel source channels}
* with a given {@link Channel function}.
* @param function a function that source channels should fulfil
* @return an array of indexes of source channels with a given function
*/
public int[] getSourceChannelsByFunction(IChannelFunction function) {
LinkedList<SourceChannel> list = getSourceChannelsByFunctionList(function);
int[] indices = new int[list.size()];
int i = 0;
for (SourceChannel channel : list) {
indices[i] = channel.getChannel();
i++;
}
return indices;
}
/**
* Adds a new {@link SourceChannel source channel} with a given label
* and {@link Channel function}.
* @param label a unique label for new source channel
* @param function a unique function for new source channel
* @throws MontageException thrown when label or function not unique
*/
public void addSourceChannel(String label, IChannelFunction function) throws MontageException {
IChannelFunction nonNullChannel = (function != null ? function : getSignalTypeConfigurer().genericChannel());
HashMap<String, SourceChannel> map = getSourceChannelsByLabel();
if (map.containsKey(label)) {
throw new MontageException(_("Source channels labels cannot be duplicated!"));
}
LinkedList<SourceChannel> list = getSourceChannelsByFunctionList(nonNullChannel);
if (nonNullChannel.isUnique() && !list.isEmpty()) {
throw new MontageException(_R("The function {0} cannot be duplicated!", nonNullChannel.getName()));
}
SourceChannel channel = new SourceChannel(sourceChannels.size(), label, nonNullChannel);
sourceChannels.add(channel);
map.put(label, channel);
list.add(channel);
fireSourceMontageChannelAdded(this, channel.getChannel());
setChanged(true);
}
/**
* Removes source channel from of a given index from this SourceMontage.
* @param index the index of the source channel to be removed
* @return true if the channel was removed, false otherwise
* (the channel cannot be removed, if it is in use in the target montage;
* see {@link Montage#removeSourceChannel(int)}).
*/
public boolean removeSourceChannel(int index) {
SourceChannel channel = sourceChannels.get(index);
getSourceChannelsByLabel().remove(channel.getLabel());
getSourceChannelsByFunctionList(channel.getFunction()).remove(channel);
sourceChannels.remove(index);
for (int i = index; i < sourceChannels.size(); i++) {
SourceChannel sourceChannel = sourceChannels.get(i);
sourceChannel.setChannel(sourceChannel.getChannel()-1);
}
setChanged(true);
fireSourceMontageChannelRemoved(this, index);
return true;
}
/**
* Removes the last {@link SourceChannel source channel} on the
* {@link #sourceChannels sourceChannels} list from this SourceMontage
* @return the removed source channel
*/
protected SourceChannel removeLastSourceChannel() {
if (sourceChannels.isEmpty()) {
return null;
}
int index = sourceChannels.size() - 1;
SourceChannel channel = sourceChannels.get(index);
getSourceChannelsByLabel().remove(channel.getLabel());
getSourceChannelsByFunctionList(channel.getFunction()).remove(channel);
sourceChannels.remove(index);
fireSourceMontageChannelRemoved(this, index);
setChanged(true);
return channel;
}
/**
* Finds a unique label for a {@link SourceChannel source channel}.
* @param stem a String on which new label is to be based
* @return a unique label for a source channel
*/
public String getNewSourceChannelLabel(String stem) {
int cnt = 2;
String candidate = stem;
HashMap<String, SourceChannel> map = getSourceChannelsByLabel();
while (map.containsKey(candidate)) {
candidate = stem + " (" + cnt + ")";
cnt++;
}
return candidate;
}
/**
* Add a given {@link PropertyChangeListener listener} to the
* list of listeners. The listener is registered for all properties.
* @param listener The PropertyChangeListener to be added
*/
public void addPropertyChangeListener(PropertyChangeListener listener) {
pcSupport.addPropertyChangeListener(listener);
}
/**
* Add a {@link PropertyChangeListener listener} for a specific property.
* The listener will be invoked only when a call on firePropertyChange
* names that specific property.
* @param propertyName The name of the property to listen on.
* @param listener the PropertyChangeListener to be added
*/
public void addPropertyChangeListener(String propertyName, PropertyChangeListener listener) {
pcSupport.addPropertyChangeListener(propertyName, listener);
}
/**
* Remove a {@link PropertyChangeListener listener} from the listener
* list. This removes the lister that was registered for all properties.
* @param listener the PropertyChangeListener to be removed
*/
public void removePropertyChangeListener(PropertyChangeListener listener) {
pcSupport.removePropertyChangeListener(listener);
}
/**
* Remove a {@link PropertyChangeListener listener} for a specific
* property.
* @param propertyName the name of the property that was listened on.
* @param listener the PropertyChangeListener to be removed
*/
public void removePropertyChangeListener(String propertyName, PropertyChangeListener listener) {
pcSupport.removePropertyChangeListener(propertyName, listener);
}
/**
* Adds the listener as a listener of a type
* {@link SourceMontageListener}.
* @param l the listener to be added
*/
public void addSourceMontageListener(SourceMontageListener l) {
listenerList.add(SourceMontageListener.class, l);
}
/**
* Removes the listener as a listener of a type
* {@link SourceMontageListener}.
* @param l the listener to be removed
*/
public void removeSourceMontageListener(SourceMontageListener l) {
listenerList.remove(SourceMontageListener.class, l);
}
/**
* Fires an event of adding a {@link SourceChannel channel}.
* @param source the object on which the Event initially occurred.
* @param channel an index of an added channel
*/
protected void fireSourceMontageChannelAdded(Object source, int channel) {
Object[] listeners = listenerList.getListenerList();
SourceMontageEvent e = null;
for (int i = listeners.length-2; i>=0; i-=2) {
if (listeners[i]==SourceMontageListener.class) {
if (e == null) {
e = new SourceMontageEvent(source, channel);
}
((SourceMontageListener)listeners[i+1]).sourceMontageChannelAdded(e);
}
}
}
/**
* Fires an event of removing a {@link SourceChannel channel}.
* @param source the object on which the Event initially occurred.
* @param channel an index of removed channel
*/
protected void fireSourceMontageChannelRemoved(Object source, int channel) {
Object[] listeners = listenerList.getListenerList();
SourceMontageEvent e = null;
for (int i = listeners.length-2; i>=0; i-=2) {
if (listeners[i]==SourceMontageListener.class) {
if (e == null) {
e = new SourceMontageEvent(source, channel);
}
((SourceMontageListener)listeners[i+1]).sourceMontageChannelRemoved(e);
}
}
}
/**
* Fires an event of changing a {@link SourceChannel channel}.
* @param source the object on which the Event initially occurred.
* @param channel an index of a changed channel
*/
protected void fireSourceMontageChannelChanged(Object source, int channel) {
Object[] listeners = listenerList.getListenerList();
SourceMontageEvent e = null;
for (int i = listeners.length-2; i>=0; i-=2) {
if (listeners[i]==SourceMontageListener.class) {
if (e == null) {
e = new SourceMontageEvent(source, channel);
}
((SourceMontageListener)listeners[i+1]).sourceMontageChannelChanged(e);
}
}
}
/**
* Fires an event informing all listeners that the Montage {@link EegSystem}
* has been changed.
* @param source the object on which the Event initially occurred.
*/
protected void fireSourceMontageEegSystemChanged(Object source) {
Object[] listeners = listenerList.getListenerList();
SourceMontageEvent e = null;
for (int i = listeners.length-2; i>=0; i-=2) {
if (listeners[i]==SourceMontageListener.class) {
if (e == null) {
e = new SourceMontageEvent(source);
}
((SourceMontageListener)listeners[i+1]).sourceMontageEegSystemChanged(e);
}
}
}
/**
* Returns the name of the {@link EegSystem} used by this Montage.
* @return the name of the {@link EegSystem} used by this Montage
*/
public EegSystemName getEegSystemName() {
return eegSystemName;
}
public String getEegSystemFullName() {
if (eegSystemName != null)
return eegSystemName.getFullName();
return null;
}
}