/* SignalPlotRowHeader.java created 2007-10-15
*
*/
package org.signalml.app.view.signal;
import static org.signalml.app.util.i18n.SvarogI18n._;
import java.awt.Color;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import javax.swing.AbstractAction;
import javax.swing.ImageIcon;
import javax.swing.JComponent;
import javax.swing.SwingUtilities;
import org.signalml.app.model.components.ChannelPlotOptionsModel;
import org.signalml.app.util.IconUtils;
import org.signalml.app.view.common.components.CompactButton;
import org.signalml.app.view.signal.popup.ChannelOptionsPopupDialog;
import org.signalml.domain.montage.SourceChannel;
import org.signalml.domain.montage.system.ChannelFunction;
import org.signalml.domain.signal.samplesource.MultichannelSampleSource;
/** SignalPlotRowHeader
*
*
* @author Michal Dobaczewski © 2007-2008 CC Otwarte Systemy Komputerowe Sp. z o.o.
*/
public class SignalPlotRowHeader extends JComponent {
private static final long serialVersionUID = 1L;
private static final Dimension MINIMUM_SIZE = new Dimension(0,0);
/*
* width of channel's options button (for every channel) in pixels
*/
private static final int CHANNEL_BUTTON_WIDTH = 10;
/*
* width of value scale's tick (horizontal line) in pixels
*/
private static final int SCALE_HORIZONTAL_LINE_WIDTH = 4;
/*
* Infinity value for pixel per row unit
*/
private static final double PIXEL_PER_ROW_UNIT_INF = -1.0;
/*
* distance (in pixels) from value scale to its label
*/
private static final int LABEL_LINE_DISTANCE = 5;
private boolean calculated = false;
private Font normalFont;
private Font verticalFont;
private int channelCount;
private double pixelPerValue;
private int pixelPerChannel;
private int[] channelLevel;
private double pixelPerRowUnit;
private double[] pixelPerRowUnitForChannels;
private String rowUnitLabel;
private String[] rowUnitLabelForChannels;
private Rectangle2D unitLabelBounds;
private Rectangle2D[] channelLabelBounds;
private CompactButton[] channelOptionsButtons;
private int maxChannelLabelWidth = 0;
private SignalPlot plot;
private ImageIcon channelOptionsVisibleIcon;
private ImageIcon channelOptionsInvisibleIcon;
private MultichannelSampleSource labelSource;
private ChannelOptionsPopupDialog channelOptionsPopupDialog;
private boolean active = true;
public SignalPlotRowHeader(SignalPlot plot) {
super();
this.plot = plot;
Image iconImage;
ImageIcon ic;
iconImage = IconUtils.loadClassPathImage("org/signalml/app/icon/channelOptionsVisible.png");
ic = new ImageIcon(iconImage);
this.channelOptionsVisibleIcon = ic;
iconImage = IconUtils.loadClassPathImage("org/signalml/app/icon/channelOptionsInvisible.png");
ic = new ImageIcon(iconImage);
this.channelOptionsInvisibleIcon = ic;
}
/*
* Sets calculated to false and creates ChannelOptions Buttons.
*/
public void reset() {
calculated = false;
if (channelOptionsButtons != null) {
for (int i = 0; i < channelOptionsButtons.length; i++)
if (channelOptionsButtons[i] != null)
this.remove(channelOptionsButtons[i]);
}
channelOptionsButtons = new CompactButton[this.plot.getChannelCount()];
for (int i = 0; i < this.plot.getChannelCount(); i++) {
CompactButton b = new CompactButton(
new ChannelOptionsAction(this.channelOptionsVisibleIcon,
this.channelOptionsInvisibleIcon,
_("Edit channel's options"),
_("Show channel"),
i));
channelOptionsButtons[i] = b;
this.add(b);
}
revalidate();
repaint();
}
/*
* Determine if there is some channel with its own value scale
* @returns true if there exists a channel with its own value scale, false otherwise
*/
private boolean hasSpecialChannels() {
for (int i = 0; i < channelCount; i++)
if (pixelPerRowUnitForChannels[i] >= 0)
return true;
return false;
}
/*
* Fill channels`es data structures: channelLabelBounds,
* pixelPerRowUnitForChannels, rowUnitLabelForChannels.
*/
private void calculateChannelsData(Graphics2D g) {
StringBuilder sb;
ChannelPlotOptionsModel m;
int globalScale = this.plot.getValueScaleRangeModel().getValue();
double max = 0;
for (int i = 0; i < channelCount; i++) {
//calculate label bound
channelLabelBounds[i] = normalFont.getStringBounds(labelSource.getLabel(i), g.getFontRenderContext());
if (max < channelLabelBounds[i].getWidth()) {
max = channelLabelBounds[i].getWidth();
}
//calculate scaling
m = this.plot.getChannelsPlotOptionsModel().getModelAt(i);
double localPixelsPerValue = this.plot.getChannelsPlotOptionsModel().getPixelsPerValue(i);
double globalPixelPerValue = this.plot.getPixelPerValue();
if ((localPixelsPerValue != globalPixelPerValue) && (m.getVisible())) {
pixelPerRowUnitForChannels[i] = this.plot.getPixelPerChannel() * m.getVoltageScale() * this.plot.getVoltageZoomFactorRatioFor(i);
sb = new StringBuilder("1");
if (pixelPerRowUnitForChannels[i] <= 0.0) {
sb.append("000...");
pixelPerRowUnitForChannels[i] = 0.0;
} else {
while (pixelPerRowUnitForChannels[i] <= 5) {
pixelPerRowUnitForChannels[i] *= 10;
sb.append("0");
}
};
sb.append(" "+plot.getSourceChannelFor(i).getFunction().getUnitOfMeasurementSymbol());
rowUnitLabelForChannels[i] = sb.toString();
} else
pixelPerRowUnitForChannels[i] = PIXEL_PER_ROW_UNIT_INF;
}
maxChannelLabelWidth = (int) Math.ceil(max);
}
private void calculate(Graphics2D g) {
if (calculated) {
return;
}
channelCount = plot.getChannelCount();
pixelPerValue = plot.getPixelPerValue();
pixelPerChannel = plot.getPixelPerChannel();
labelSource = plot.getSignalOutput();
channelLevel = plot.getChannelLevel();
pixelPerRowUnit = pixelPerValue;
StringBuilder sb = new StringBuilder("1");
if (pixelPerRowUnit <= 0.0) {
pixelPerRowUnit = 0.0;
sb.append("000...");
} else {
while (pixelPerRowUnit <= 5) {
pixelPerRowUnit *= 10;
sb.append("0");
}
};
sb.append(" uV");
rowUnitLabel = sb.toString();
normalFont = g.getFont();
verticalFont = normalFont.deriveFont(AffineTransform.getQuadrantRotateInstance(1));
unitLabelBounds = verticalFont.getStringBounds(rowUnitLabel, g.getFontRenderContext());
rowUnitLabelForChannels = new String[channelCount];
pixelPerRowUnitForChannels = new double[channelCount];
channelLabelBounds = new Rectangle2D[channelCount];
this.calculateChannelsData(g);
calculated = true;
}
@Override
protected void paintComponent(Graphics gOrig) {
Graphics2D g = (Graphics2D)gOrig;
calculate(g);
Point viewportPoint = plot.getViewport().getViewPosition();
Dimension viewportSize = plot.getViewport().getExtentSize();
Dimension size = getSize();
Rectangle clip = g.getClipBounds();
g.setColor(getBackground());
g.fillRect(clip.x,clip.y,clip.width,clip.height);
size.width -= SignalPlot.SCALE_TO_SIGNAL_GAP;
int i;
int y;
// this draws value ticks
g.setColor(Color.GRAY);
g.drawLine(size.width-SCALE_HORIZONTAL_LINE_WIDTH, viewportPoint.y,
size.width-SCALE_HORIZONTAL_LINE_WIDTH, viewportPoint.y + viewportSize.height);
int tickCnt = 1 + ((int)(((float)(viewportSize.height+1)) / pixelPerRowUnit));
for (i=0; i<tickCnt; i++) {
y = viewportPoint.y + ((int)(i*pixelPerRowUnit));
g.drawLine(size.width-SCALE_HORIZONTAL_LINE_WIDTH+1, y, size.width-1, y);
}
this.drawChannelsValueTicks(g, size);
//determine start channel number and number of channels to draw (to be precise - theirs labels)
int startChannel = this.plot.computePaintStartChannel(clip.y);
//take care of invisible labels above first visible drawable channel...
for (i=startChannel-1; i>=0; i--)
if (!this.plot.getChannelsPlotOptionsModel().getModelAt(i).getVisible())
startChannel--;
else
break;
int maxNumberOfChannels = (int) Math.min(channelCount, Math.ceil(((double)clip.height-1) / pixelPerChannel));
//initialise canvas
if (active) {
g.setColor(Color.BLUE);
} else {
g.setColor(Color.GRAY);
}
g.setFont(normalFont);
//draw visible labels and channel's buttons
boolean visible;
int visibleCount=0;
i=startChannel;
while (visibleCount<= maxNumberOfChannels && i<=channelCount-1) {
visible = this.plot.getChannelsPlotOptionsModel().getModelAt(i).getVisible();
String channelLabelAndUnitString = getChannelLabelAndUnitString(i);
if (visible) {
visibleCount ++;
if (active)
g.setColor(Color.BLUE);
g.drawString(channelLabelAndUnitString, CHANNEL_BUTTON_WIDTH+2, channelLevel[i] + ((int) -channelLabelBounds[i].getY()/2));
} else {
g.setColor(Color.GRAY);//todo - make font smaller
g.drawString(channelLabelAndUnitString, CHANNEL_BUTTON_WIDTH+2, channelLevel[i] + ((int) -channelLabelBounds[i].getY()/2));
}
channelOptionsButtons[i].setBounds(1, channelLevel[i]-2, CHANNEL_BUTTON_WIDTH, CHANNEL_BUTTON_WIDTH);
((ChannelOptionsAction) channelOptionsButtons[i].getAction()).setButtonVisible(visible);
i++;
}
g.setColor(Color.GRAY);
g.setFont(verticalFont);
g.drawString(rowUnitLabel, size.width+((float)unitLabelBounds.getY())-LABEL_LINE_DISTANCE, viewportPoint.y+3);
}
/**
* Returns a String containing the channel label and a unit of measurement
* for the channel (e.g. "SD3 [uV]). If the channel function is EEG,
* then the unit of measurement is not shown.
* @param montageChannelNumber the channel number in the current montage.
* @return a String describing the current channel
*/
protected String getChannelLabelAndUnitString(int montageChannelNumber) {
String channelLabelAndUnitString = labelSource.getLabel(montageChannelNumber);
SourceChannel sourceChannel = this.plot.getDocument().getMontage().getSourceChannelForMontageChannel(montageChannelNumber);
String units = sourceChannel.getFunction().getUnitOfMeasurementSymbol();
if (units != null && !units.isEmpty() && !(sourceChannel.getFunction() == ChannelFunction.EEG))
channelLabelAndUnitString += " [" + units + "]";
return channelLabelAndUnitString;
}
/*
* For every channel that have its individual value scale draws it.
* @param g graphics on which scales will be drawn
* @param size panel's size needed to determine value scale X position
*/
private void drawChannelsValueTicks(Graphics2D g, Dimension size) {
//prepare canvas to draw labels
g.setColor(Color.GRAY);
g.setFont(verticalFont);
int i=0, tp=0, bt=0, x=0, y=0;
for (int j = 0; j < channelCount; j++) {
if (pixelPerRowUnitForChannels[j] >= 0) { // if channel has its own value scale
tp = channelLevel[j] - pixelPerChannel/2 + 3; //scale's top position
bt = channelLevel[j] + pixelPerChannel/2 - 3; //scale's bottom position
x = size.width - this.getScaleWidth(); // scale's x position
//draw scale's line...
g.drawLine(x-SCALE_HORIZONTAL_LINE_WIDTH, tp, x-SCALE_HORIZONTAL_LINE_WIDTH, bt);
//determine number of ticks (scales horizontal lines)
int tickCnt = 1 + ((int)(((double)(bt - tp)) / pixelPerRowUnitForChannels[j]));
//draw half of tick obove the channel, half below
for (i=0; i<tickCnt/2; i++) {
y = channelLevel[j] + ((int)(i*pixelPerRowUnitForChannels[j]));
g.drawLine(x-SCALE_HORIZONTAL_LINE_WIDTH+1, y, x-1, y);
}
for (i=0; i<tickCnt/2; i++) {
y = channelLevel[j] - ((int)(i*pixelPerRowUnitForChannels[j]));
g.drawLine(x-SCALE_HORIZONTAL_LINE_WIDTH+1, y, x-1, y);
}
//draw informative label, eg. 100uV
g.drawString(rowUnitLabelForChannels[j], x-SCALE_HORIZONTAL_LINE_WIDTH + ((float)unitLabelBounds.getY()), tp+3);
}
}
}
/*
* Returns width (in pixels) of single channel value scale (often presented in uV).
* @returns width (in pixels) of single channel value scale (often presented in uV)
*/
private int getScaleWidth() {
return (int) Math.ceil(unitLabelBounds.getHeight() + SignalPlot.SCALE_TO_SIGNAL_GAP + LABEL_LINE_DISTANCE + SCALE_HORIZONTAL_LINE_WIDTH);
}
public int getPreferredWidth() {
calculate((Graphics2D) getGraphics());
if (this.hasSpecialChannels()) //we have individual scales for some channels
return maxChannelLabelWidth + this.getScaleWidth()*2 + CHANNEL_BUTTON_WIDTH;
else
return maxChannelLabelWidth + this.getScaleWidth() + CHANNEL_BUTTON_WIDTH;
}
@Override
public Dimension getPreferredSize() {
// preferred widths must be coordinated!
calculate((Graphics2D) getGraphics());
return new Dimension(plot.getView().getSynchronizedRowHeaderWidth(),channelCount*pixelPerChannel);
}
@Override
public Dimension getMaximumSize() {
return getPreferredSize();
}
@Override
public Dimension getMinimumSize() {
return MINIMUM_SIZE;
}
@Override
public boolean isOpaque() {
return true;
}
public SignalPlot getPlot() {
return plot;
}
public boolean isActive() {
return active;
}
public void setActive(boolean active) {
if (this.active != active) {
this.active = active;
repaint();
}
}
protected class ChannelOptionsAction extends AbstractAction {
/*
* An action performed on ChannelOptionsButton clicked.
*/
private static final long serialVersionUID = 1L;
private int channel;
private String visibleTooltip;
private String invisibleTooltip;
private ImageIcon visibleIcon;
private ImageIcon invisibleIcon;
/*
* Creates an action for ChannelOptions button.
* @param visibleIcon button's visible icon
* @param invisibleIcon button's invisible icon
* @param visibleTooltip button's visible tooltip
* @param invisible button's invisible tooltip
* @param channel index of channel the action is connected to
*/
public ChannelOptionsAction(ImageIcon visibleIcon, ImageIcon invisibleIcon, String visibleTooltip, String invisibleTooltip, int channel) {
super();
this.channel = channel;
this.visibleTooltip = visibleTooltip;
this.invisibleTooltip = invisibleTooltip;
this.visibleIcon = visibleIcon;
this.invisibleIcon = invisibleIcon;
putValue(AbstractAction.SMALL_ICON, visibleIcon);
putValue(AbstractAction.SHORT_DESCRIPTION, visibleTooltip);
}
/*
* Sets button's visibility attributes - icon and tooltip.
* @param visible ...
*/
public void setButtonVisible(boolean visible) {
if (visible) {
putValue(AbstractAction.SHORT_DESCRIPTION, this.visibleTooltip);
putValue(AbstractAction.SMALL_ICON, this.visibleIcon);
} else {
putValue(AbstractAction.SHORT_DESCRIPTION, this.invisibleTooltip);
putValue(AbstractAction.SMALL_ICON, this.invisibleIcon);
};
}
/*
* Initializes channelOptions dialog and set it to appropriate channel.
* @see java.awt.event.ActionListener#actionPerformed(java.awt.event.ActionEvent)
*/
@Override
public void actionPerformed(ActionEvent ev) {
Container ancestor = getTopLevelAncestor();
Point containerLocation = ancestor.getLocation();
CompactButton b = channelOptionsButtons[channel];
Point location = SwingUtilities.convertPoint(b, new Point(0,0), ancestor);
channelOptionsPopupDialog.setChannel(this.channel);
channelOptionsPopupDialog.setCurrentPlot(plot);
channelOptionsPopupDialog.initializeNow();
if (location.y < ancestor.getHeight()/2) {
location.translate(containerLocation.x, containerLocation.y);
} else {
location.translate(containerLocation.x, containerLocation.y + channelOptionsPopupDialog.getHeight() - channelOptionsPopupDialog.getHeight());
}
channelOptionsPopupDialog.setLocation(location);
channelOptionsPopupDialog.showDialog(plot);
}
}
/*
* Sets pop-up dialog for channelDisplay options.
* @param channelOptionsPopupDialog channelDisplay options dialog
*/
public void setChannelOptionsPopupDialog(
ChannelOptionsPopupDialog channelOptionsPopupDialog) {
this.channelOptionsPopupDialog = channelOptionsPopupDialog;
}
}