/*******************************************************************************
* Copyright (c) 2010, 2012 Weltevree Beheer BV, Remain Software & Industrial-TSI
*
* All rights reserved.
* This program and the accompanying materials are made available under the terms of
* the Eclipse Public License v1.0 which accompanies this distribution, and is
* available at http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Wim S. Jongman - initial API and implementation
* Jantje- split oscilloscope in plotter and oscilloscope
******************************************************************************/
package org.eclipse.nebula.widgets.oscilloscope.multichannel;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.ControlEvent;
import org.eclipse.swt.events.ControlListener;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.widgets.Canvas;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
@SuppressWarnings("synthetic-access")
public class Plotter extends Canvas {
private Color bg;
private int height = DEFAULT_HEIGHT;
private int width = DEFAULT_WIDTH;
// Blocks painting if true
private boolean paintBlock = true;
private final Data[] chan;
private int myRangeHighValue = 100;
private int myRangeLowValue = -100;
/**
* This class holds the data per channel.
*
* @author Wim Jongman
*
*/
private class Data {
private int base;
private int baseOffset = BASE_CENTER;
private boolean connect;
private int cursor = CURSOR_START_DEFAULT;
private boolean fade;
private Color fg;
private int lineWidth = LINE_WIDTH_DEFAULT;
private int originalSteadyPosition = STEADYPOSITION_75PERCENT;
/**
* This contains the actual values that where input by the user before
* scaling. If the user resized we can calculate how the tail would have
* looked with the new window dimensions.
*
* @see Plotter#tail
*/
private int originalTailSize;
private boolean percentage = false;
private boolean steady;
/**
* This contains the old or historical input and is used to paint the
* tail of the graph.
*/
private int[] tail;
private int tailFade = TAILFADE_PERCENTAGE;
private int tailSize = TAILSIZE_DEFAULT;
private boolean antiAlias = false;
private String name = new String();
}
/**
* The base of the line is positioned at the center of the widget.
*
* @see #setBaseOffset(int)
*/
public static final int BASE_CENTER = 50;
/**
* The default cursor starting position.
*/
public static final int CURSOR_START_DEFAULT = 50;
/**
* The default comfortable widget height.
*/
public static final int DEFAULT_HEIGHT = 100;
/**
* The default comfortable widget width.
*/
public static final int DEFAULT_WIDTH = 180;
/**
* The default line width.
*/
public static final int LINE_WIDTH_DEFAULT = 1;
/**
* The default tail fade percentage
*/
public static final int PROGRESSION_DEFAULT = 1;
/**
* Steady position @ 75% of graph.
*/
public static final int STEADYPOSITION_75PERCENT = -1;
/**
* The default amount of tail fading in percentages (25).
*/
public static final int TAILFADE_DEFAULT = 25;
/**
* No tailfade.
*/
public static final int TAILFADE_NONE = 0;
/**
* The default tail fade percentage
*/
public static final int TAILFADE_PERCENTAGE = 25;
/**
* The default tail size is 75% of the width.
*/
public static final int TAILSIZE_DEFAULT = -3;
/**
* Will draw a tail from the left border but is only valid if the boolean in
* {@link #setSteady(boolean, int)} was set to true, will default to
* {@link #TAILSIZE_MAX} otherwise.
*/
public static final int TAILSIZE_FILL = -2;
/**
* Will draw a maximum tail.
*/
public static final int TAILSIZE_MAX = -1;
// van Sloeber
/**
* Creates a new plotter with <code>channels</code> channels.
*
* @param channels
* @param parent
* @param style
*/
public Plotter(int channels, Composite parent, int style) {
this(channels, parent, style, null, null);
}
/**
* Creates a new plotter with <code>channels</code> channels
*
* @param channels
* @param parent
* @param style
* @param backgroundColor
* if null use default background
* @param foregroundColor
* if null use default foreground
*/
public Plotter(int channels, Composite parent, int style, Color backgroundColor, Color foregroundColor) {
super(parent, SWT.DOUBLE_BUFFERED | style);
if (backgroundColor == null) {
this.bg = Display.getDefault().getSystemColor(SWT.COLOR_BLACK);
} else {
this.bg = backgroundColor;
}
Color fg;
if (foregroundColor == null) {
fg = Display.getDefault().getSystemColor(SWT.COLOR_WHITE);
} else {
fg = foregroundColor;
}
setBackground(this.bg);
this.chan = new Data[channels];
for (int i = 0; i < this.chan.length; i++) {
this.chan[i] = new Data();
this.chan[i].fg = fg;
setTailSize(i, TAILSIZE_DEFAULT);
}
addDisposeListener(new DisposeListener() {
public void widgetDisposed(DisposeEvent e) {
Plotter.this.widgetDisposed(e);
}
});
addPaintListener(new PaintListener() {
public void paintControl(PaintEvent e) {
if (!Plotter.this.paintBlock) {
Plotter.this.paintControl(e);
}
Plotter.this.paintBlock = false;
}
});
addControlListener(new ControlListener() {
public void controlMoved(ControlEvent e) {
Plotter.this.controlMoved(e);
}
public void controlResized(ControlEvent e) {
Plotter.this.paintBlock = true;
Plotter.this.controlResized(e);
}
});
}
/**
* This method calculates the progression of the line.
*
* @return
*/
private Object[] calculate(int channel) {
int c = channel;
int[] line1 = null;
int[] line2 = null;
int splitPos = 0;
splitPos = this.chan[c].tailSize * 4;
if (!isSteady(c))
this.chan[c].cursor++;
if (this.chan[c].cursor >= this.width)
this.chan[c].cursor = 0;
line1 = new int[this.chan[c].tailSize * 4];
line2 = new int[this.chan[c].tailSize * 4];
for (int i = 0; i < this.chan[c].tailSize; i++) {
int posx = this.chan[c].cursor - this.chan[c].tailSize + i;
int pos = i * 4;
if (posx < 0) {
posx += this.width;
line1[pos] = posx - 1;
line1[pos + 1] = getBase(c) + (isSteady(c) ? 0 : this.chan[c].tail[i]);
line1[pos + 2] = posx;
line1[pos + 3] = getBase(c) + (isSteady(c) ? 0 : this.chan[c].tail[i + 1]);
}
else {
if (splitPos == this.chan[c].tailSize * 4)
splitPos = pos;
line2[pos] = posx - 1;
line2[pos + 1] = getBase(c) + this.chan[c].tail[i];
line2[pos + 2] = posx;
line2[pos + 3] = (getBase(c) + this.chan[c].tail[i + 1]);
}
// chan[c].tail[tailIndex - 1] = chan[c].tail[tailIndex++];
}
// }
int[] l1 = new int[splitPos];
System.arraycopy(line1, 0, l1, 0, l1.length);
int[] l2 = new int[(this.chan[c].tailSize * 4) - splitPos];
System.arraycopy(line2, splitPos, l2, 0, l2.length);
return new Object[] { l1, l2 };
}
/**
* calculate the base of the line
*
* @param channel
*/
private void calculateBase(int channel) {
if ((this.myRangeLowValue != 0) || (this.myRangeHighValue != 0))
this.chan[channel].base = 0;
else if (this.height > 2) {
this.chan[channel].base = (this.height * +(100 - getBaseOffset(channel))) / 100;
}
}
protected void paintControl(PaintEvent e) {
for (int c = 0; c < this.chan.length; c++) {
// Go calculate the line
Object[] result = calculate(c);
int[] l1 = (int[]) result[0];
int[] l2 = (int[]) result[1];
PositionPolyLine(l1);
PositionPolyLine(l2);
// Draw it
GC gc = e.gc;
gc.setForeground(getForeground(c));
gc.setAdvanced(true);
gc.setAntialias(this.chan[c].antiAlias ? SWT.ON : SWT.OFF);
gc.setLineWidth(getLineWidth(c));
// Fade tail
if (isFade(c)) {
gc.setAlpha(0);
double fade = 0;
double fadeOutStep = (double) 125 / (double) ((getTailSize(c) * (getTailFade(c)) / 100));
for (int i = 0; i < l1.length - 4;) {
fade += (fadeOutStep / 2);
setAlpha(gc, fade);
gc.drawLine(l1[i], l1[i + 1], l1[i + 2], l1[i + 3]);
i += 2;
}
for (int i = 0; i < l2.length - 4;) {
fade += (fadeOutStep / 2);
setAlpha(gc, fade);
gc.drawLine(l2[i], l2[i + 1], l2[i + 2], l2[i + 3]);
i += 2;
}
} else {
gc.drawPolyline(l1);
gc.drawPolyline(l2);
}
// Connects the head with the tail
if (isConnect(c) && !isFade(c) && this.chan[c].originalTailSize == TAILSIZE_MAX && l1.length > 0
&& l2.length > 0) {
gc.drawLine(l2[l2.length - 2], l2[l2.length - 1], l1[0], l1[1]);
}
}
}
/**
* Sets a value to be drawn relative to the center of the channel. Supply a
* positive or negative value. This method will only accept values if the
* width of the plotter > 0. The values will be stored in a stack and popped
* once a value is needed. The size of the stack is the width of the widget.
* If you resize the widget, the old stack will be copied into a new stack
* with the new capacity.
* <p/>
* This method can be called outside of the UI thread.
*
* @param channel
* @param value
* which is an absolute value or a percentage
*
* @see #isPercentage(int)
* @see #setBaseOffset(int, int)
*/
public void setValue(int channel, int value) {
int copysize = this.chan[channel].tail.length;
System.arraycopy(this.chan[channel].tail, 1, this.chan[channel].tail, 0, copysize - 1);
this.chan[channel].tail[this.chan[channel].tailSize] = value;
}
/**
*
* @return the value represented by the bottom of the plotter
*/
public int getRangeLowValue() {
return this.myRangeLowValue;
}
/**
*
* @return the value represented by the top of the plotter
*/
public int getRangeHighValue() {
return this.myRangeHighValue;
}
private static void setAlpha(GC gc, double fade) {
if (gc.getAlpha() == fade) {
return;
}
if (fade >= 255) {
gc.setAlpha(255);
} else {
gc.setAlpha((int) fade);
}
}
protected void PositionPolyLine(int[] l1) {
for (int i = 0; i < l1.length - 4; i += 4) {
l1[i + 1] = ConvertValueToScreenPosition(l1[i + 1], getSize().y);
l1[i + 3] = ConvertValueToScreenPosition(l1[i + 3], getSize().y);
}
}
protected int ConvertValueToScreenPosition(int Value, int ScreenHeight) {
if ((this.myRangeLowValue == 0) && (this.myRangeHighValue == 0))
return ScreenHeight - Value;
float ret = ((float) (Value - this.myRangeLowValue) / (float) (this.myRangeHighValue - this.myRangeLowValue))
* ScreenHeight;
return ScreenHeight - (int) ret;
}
private void setTailSizeInternal(int channel) {
if (this.chan[channel].originalTailSize == TAILSIZE_DEFAULT) {
this.chan[channel].tailSize = (this.width / 4) * 3;
this.chan[channel].tailSize--;
} else if (this.chan[channel].originalTailSize == TAILSIZE_FILL) {
if (isSteady(channel)) {
this.chan[channel].tailSize = this.chan[channel].originalSteadyPosition - 1;
} else {
// act as if TAILSIZE_MAX
this.chan[channel].tailSize = this.width - 2;
}
} else if (this.chan[channel].originalTailSize == TAILSIZE_MAX
|| this.chan[channel].originalTailSize > this.width) {
this.chan[channel].tailSize = this.width - 2;
} else if (this.chan[channel].tailSize != this.chan[channel].originalTailSize) {
this.chan[channel].tailSize = this.chan[channel].originalTailSize;
}
// Transform the old tail. This is we want to see sort of the same form
// after resize.
int[] oldTail = this.chan[channel].tail;
if (oldTail == null) {
this.chan[channel].tail = new int[this.chan[channel].tailSize + 1];
} else {
this.chan[channel].tail = new int[this.chan[channel].tailSize + 1];
if (this.chan[channel].tail.length >= oldTail.length) {
for (int i = 0; i < oldTail.length; i++) {
this.chan[channel].tail[this.chan[channel].tail.length - 1 - i] = oldTail[oldTail.length - 1 - i];
}
} else {
for (int i = 0; i < this.chan[channel].tail.length; i++) {
this.chan[channel].tail[this.chan[channel].tail.length - 1 - i] = oldTail[oldTail.length - 1 - i];
}
}
}
}
public void setRange(int lowValue, int highValue) {
this.myRangeLowValue = lowValue;
this.myRangeHighValue = highValue;
}
/**
* This method returns the data in csv format using semi comma as seperator.
* if addHeader is true a header is added based on the names of the
* channels. Use setChannelName to set the names
*/
public String getData(boolean addHeader) {
String ret = new String();
if (addHeader) {
for (int channel = 0; channel < this.chan.length; channel++) {
ret = ret + this.chan[channel].name + ';';
}
}
for (int curvalue = 0; curvalue < this.chan[0].tail.length; curvalue++) {
for (int channel = 0; channel < this.chan.length; channel++) {
ret += this.chan[channel].tail[curvalue] + ';';
}
ret += '\n';
}
return ret;
}
/**
* Set the descriptive name of the channel
*
* @param channel
* the channel to set the name for
* @param name
* the descriptive name
*/
public void SetChannelName(int channel, String name) {
this.chan[channel].name = name;
}
/**
* get the descriptive name of the channel
*
* @param channel
* the channel to get the name from returns the descriptive name
*/
public String getChannelName(int channel) {
return this.chan[channel].name;
}
/**
* If steady is true the graph will draw on a steady position instead of
* advancing.
* <p/>
* This method can be called outside of the UI thread.
*
* @param steady
* @param steadyPosition
*/
public void setSteady(int channel, boolean steady, int steadyPosition) {
this.chan[channel].steady = steady;
this.chan[channel].originalSteadyPosition = steadyPosition;
if (steady) {
if (steadyPosition == STEADYPOSITION_75PERCENT) {
this.chan[channel].cursor = (int) (this.width * 0.75);
} else if (steadyPosition > 0 && steadyPosition < this.width) {
this.chan[channel].cursor = steadyPosition;
}
}
}
/**
* This method can be called outside of the UI thread.
*
* @return boolean steady indicator
* @see Oscilloscope#setSteady(boolean, int)
*/
public boolean isSteady(int channel) {
return this.chan[channel].steady;
}
/**
* This method can be called outside of the UI thread.
*
* @return the base of the line.
*/
public int getBase(int channel) {
return this.chan[channel].base;
}
/**
* Gets the relative location where the line is drawn in the widget. This
* method can be called outside of the UI thread.
*
* @return baseOffset
*/
public int getBaseOffset(int channel) {
return this.chan[channel].baseOffset;
}
/**
* Returns the number of channels on the plotter This method can be called
* outside of the UI thread.
*
* @return int, number of channels.
*/
public int getChannels() {
return this.chan.length;
}
/**
* This method can be called outside of the UI thread.
*
* @param channel
* @return the foreground color associated with the supplied channel.
*/
public Color getForeground(int channel) {
return this.chan[channel].fg;
}
/**
* This method can be called outside of the UI thread.
*
* @return int, the width of the line.
* @see #setLineWidth(int)
*/
public int getLineWidth(int channel) {
return this.chan[channel].lineWidth;
}
/**
* Gets the percentage of tail that must be faded out. This method can be
* called outside of the UI thread.
*
* @return int percentage
* @see #setFade(boolean)
*/
public int getTailFade(int channel) {
return this.chan[channel].tailFade;
}
/**
* Returns the size of the tail. This method can be called outside of the UI
* thread.
*
* @return int
* @see #setTailSize(int)
* @see #TAILSIZE_DEFAULT
* @see #TAILSIZE_FILL
* @see #TAILSIZE_MAX
*
*/
public int getTailSize(int channel) {
return this.chan[channel].tailSize;
}
/**
* This method can be called outside of the UI thread.
*
* @return boolean, true if the tail and the head of the graph must be
* connected if tail size is {@link #TAILSIZE_MAX} no fading graph.
*/
public boolean isConnect(int channel) {
return this.chan[channel].connect;
}
/**
* This method can be called outside of the UI thread.
*
* @see #setFade(boolean)
* @return boolean fade
*/
public boolean isFade(int channel) {
return this.chan[channel].fade;
}
/**
* This method can be called outside of the UI thread.
*
* @return boolean
* @see #setPercentage(boolean)
*/
public boolean isPercentage(int channel) {
return this.chan[channel].percentage;
}
/**
* This method can be called outside of the UI thread.
*
* @return boolean anti-alias indicator
* @see Oscilloscope#setAntialias(int, boolean)
*/
public boolean isAntiAlias(int channel) {
return this.chan[channel].antiAlias;
}
/**
* The tail size defaults to TAILSIZE_DEFAULT which is 75% of the width.
* Setting it with TAILSIZE_MAX will leave one pixel between the tail and
* the head. All values are absolute except TAILSIZE*. If the width is
* smaller then the tail size then the tail size will behave like
* TAILSIZE_MAX.
*
* @param size
* the size of the tail
* @see #getTailSize()
* @see #TAILSIZE_DEFAULT
* @see #TAILSIZE_FILL
* @see #TAILSIZE_MAX
*/
public void setTailSize(int channel, int newSize) {
int size = newSize;
checkWidget();
if (size == TAILSIZE_FILL && !isSteady(channel)) {
size = TAILSIZE_MAX;
}
if (this.chan[channel].originalTailSize != size) {
tailSizeCheck(size);
this.chan[channel].originalTailSize = size;
setTailSizeInternal(channel);
}
}
private static void tailSizeCheck(int size) {
if (size < -3 || size == 0) {
throw new RuntimeException("Invalid tail size " + size); //$NON-NLS-1$
}
}
protected void widgetDisposed(DisposeEvent e) {
this.bg.dispose();
for (Data element : this.chan) {
element.fg.dispose();
}
}
protected void controlMoved(ControlEvent e) {
// nothing to do
}
protected void controlResized(ControlEvent e) {
this.width = getSize().x;
this.height = getSize().y;
for (int c = 0; c < this.chan.length; c++) {
calculateBase(c);
}
if (getBounds().width > 0) {
for (int channel = 0; channel < this.chan.length; channel++) {
setSteady(channel, this.chan[channel].steady, this.chan[channel].originalSteadyPosition);
setTailSizeInternal(channel);
}
}
}
/**
* Sets the percentage of tail that must be faded out. If you supply 100
* then the tail is faded out all the way to the top. The effect will become
* increasingly less obvious.
* <p/>
* This method can be called outside of the UI thread.
*
* @param tailFade
*/
public void setTailFade(int channel, int newTailFade) {
int tailFade = newTailFade;
checkWidget();
if (tailFade > 100) {
tailFade = 100;
}
if (tailFade < 1) {
tailFade = 1;
}
this.chan[channel].tailFade = tailFade;
}
/**
* Sets fade mode so that a percentage of the tail will be faded out at the
* costs of extra CPU utilization (no beauty without pain or as the Dutch
* say: "Wie mooi wil gaan moet pijn doorstaan"). The reason for this is
* that each pixel must be drawn separately with alpha faded in instead of
* the elegant {@link GC#drawPolygon(int[])} routine which does not support
* alpha blending.
* <p>
* In addition to this, set the percentage of tail that must be faded out
* {@link #setTailFade(int)}.
* <p>
* This method can be called outside of the UI thread.
*
* @param fade
* true or false
* @see #setTailFade(int)
*/
public void setFade(int channel, boolean fade) {
this.chan[channel].fade = fade;
}
/**
* Sets the foreground color for the supplied channel.
* <p/>
* This method can be called outside of the UI thread.
*
* @param channel
* @param color
*/
public void setForeground(int channel, Color color) {
this.chan[channel].fg = color;
}
/**
* Sets the line width. A value equal or below zero is ignored. The default
* width is 1. This method can be called outside of the UI thread.
*
* @param lineWidth
*/
public void setLineWidth(int channel, int lineWidth) {
if (lineWidth > 0) {
this.chan[channel].lineWidth = lineWidth;
}
}
/**
* If set to true then the values are treated as percentages of the
* available space rather than absolute values. This will scale the
* amplitudes if the control is resized. Default is false.
* <p/>
* This method can be called outside of the UI thread.
*
* @param percentage
* true if percentages
*/
public void setPercentage(int channel, boolean percentage) {
this.chan[channel].percentage = percentage;
}
/**
* Gets the relative location where the line is drawn in the widget, the
* default is <code>BASE_CENTER</code> which is in the middle of the
* plotter. This method can be called outside of the UI thread.
*
* @param baseOffset
* must be between 100 and -100, exceeding values are rounded to
* the closest allowable value.
*/
public void setBaseOffset(int channel, int newBaseOffset) {
int baseOffset = newBaseOffset;
if (baseOffset > 100) {
baseOffset = 100;
}
if (baseOffset < -100) {
baseOffset = -100;
}
this.chan[channel].baseOffset = baseOffset;
calculateBase(channel);
}
/**
* Connects head and tail only if tail size is {@link #TAILSIZE_MAX} and no
* fading. This method can be called outside of the UI thread.
*
* @param connectHeadAndTail
*/
public void setConnect(int channel, boolean connectHeadAndTail) {
this.chan[channel].connect = connectHeadAndTail;
}
/**
* Sets if the line must be anti-aliased which uses more processing power in
* return of a smoother image. The default value is false. This method can
* be called outside of the UI thread.
*
* @param channel
* @param antialias
*/
public void setAntialias(int channel, boolean antialias) {
this.chan[channel].antiAlias = antialias;
}
}