Java tutorial
/* * #%L * SLIM Curve plugin for combined spectral-lifetime image analysis. * %% * Copyright (C) 2010 - 2015 Board of Regents of the University of * Wisconsin-Madison. * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU 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 General Public License for more details. * * You should have received a copy of the GNU General Public * License along with this program. If not, see * <http://www.gnu.org/licenses/gpl-3.0.html>. * #L% */ package loci.slim2.process.interactive.ui; import java.awt.BasicStroke; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Container; import java.awt.Dimension; import java.awt.FlowLayout; import java.awt.Graphics2D; import java.awt.event.ComponentEvent; import java.awt.event.ComponentListener; import java.awt.event.ItemEvent; import java.awt.event.ItemListener; import java.awt.event.MouseEvent; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.awt.geom.Ellipse2D; import java.awt.geom.Rectangle2D; import java.util.prefs.Preferences; import javax.swing.JCheckBox; import javax.swing.JComponent; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JTextField; import javax.swing.SwingUtilities; import loci.slim2.fitting.FitResults; import loci.slim2.process.interactive.cursor.FittingCursor; import loci.slim2.process.interactive.cursor.FittingCursorListener; import org.jdesktop.jxlayer.JXLayer; import org.jdesktop.jxlayer.plaf.AbstractLayerUI; import org.jfree.chart.ChartPanel; import org.jfree.chart.JFreeChart; import org.jfree.chart.axis.LogarithmicAxis; import org.jfree.chart.axis.NumberAxis; import org.jfree.chart.plot.CombinedDomainXYPlot; import org.jfree.chart.plot.XYPlot; import org.jfree.chart.renderer.xy.XYSplineRenderer; import org.jfree.chart.ui.RectangleEdge; import org.jfree.data.xy.XYSeries; import org.jfree.data.xy.XYSeriesCollection; /** * This is the chart that shows the transient decay data, the fitted model, and * the residuals. It also has a user interface to set the start and stop of the * fit. * * @author Aivar Grislis */ public class DefaultDecayGraph implements DecayGraph, IStartStopProportionListener { // Unicode special characters private static final Character CHI = '\u03c7', SQUARE = '\u00b2', TAU = '\u03c4', SUB_1 = '\u2081', SUB_2 = '\u2082', SUB_3 = '\u2083', SUB_R = '\u1d63'; static final String WIDTH_KEY = "width"; static final String HEIGHT_KEY = "height"; static final Dimension SIZE = new Dimension(500, 270); static final Dimension FRAME_SIZE = new Dimension(450, 450); static final Dimension MAX_SIZE = new Dimension(961, 724); // see getDataArea( // ) below static final Dimension MIN_SIZE = new Dimension(313, 266); static final String PHOTON_AXIS_LABEL = "Photons"; static final String TIME_AXIS_LABEL = "Time"; static final String UNITS_LABEL = "nanoseconds"; static final String RESIDUAL_AXIS_LABEL = "Residual"; static final String CHI_SQUARE = "" + CHI + SQUARE + SUB_R; static final String PHOTON_COUNT = "Photons"; static final String LOGARITHMIC = "Logarithmic"; static final int DECAY_WEIGHT = 4; static final int RESIDUAL_WEIGHT = 1; static final int HORZ_TWEAK = 1; static final Color PROMPT_COLOR = Color.GRAY.brighter(); static final Color DECAY_COLOR = Color.GRAY.darker(); static final Color FITTED_COLOR = Color.RED; static final Color BACK_COLOR = Color.WHITE; static final Color TRANS_START_COLOR = Color.BLUE.darker(); static final Color DATA_START_COLOR = Color.GREEN.darker(); static final Color TRANS_STOP_COLOR = Color.RED.darker(); static final Color BASE_COLOR = Color.GREEN.darker(); static final Color RESIDUAL_COLOR = Color.GRAY; private static final Object synchObject = new Object(); private JFrame frame; FittingCursor fittingCursor; FittingCursorListenerImpl fittingCursorListener; PixelPicker picker; private StartStopDraggingUI<JComponent> startStopDraggingUI; private double timeInc; private int bins; private double maxValue; private Double transStart; private Double dataStart; private Double transStop; private boolean logarithmic = true; XYPlot decaySubPlot; XYSeriesCollection decayDataset; XYSeriesCollection residualDataset; JTextField tau1TextField; JTextField tau2TextField; JTextField tau3TextField; JTextField chiSqTextField; JTextField photonTextField; JCheckBox logCheckBox; @Override public JFrame init(final JFrame parentFrame, final int bins, final double timeInc, final PixelPicker pixelPicker) { if (null == frame || !frame.isVisible() || this.bins != bins || this.timeInc != timeInc) { // save incoming parameters this.bins = bins; this.timeInc = timeInc; maxValue = timeInc * bins; this.picker = pixelPicker; if (null != frame) { // delete existing frame frame.setVisible(false); frame.dispose(); } // create the combined chart final JFreeChart chart = createCombinedChart(bins, timeInc); final ChartPanel panel = new ChartPanel(chart, true, true, true, false, true); panel.setDomainZoomable(false); panel.setRangeZoomable(false); panel.setPreferredSize(SIZE); // add start/stop vertical bar handling dataStart = transStop = null; final JXLayer<JComponent> layer = new JXLayer<JComponent>(panel); startStopDraggingUI = new StartStopDraggingUI<JComponent>(panel, decaySubPlot, this, maxValue); layer.setUI(startStopDraggingUI); // create a frame for the chart frame = new JFrame(); final Container container = frame.getContentPane(); container.setLayout(new BorderLayout()); container.add(layer, BorderLayout.CENTER); final JPanel miscPane = new JPanel(); miscPane.setLayout(new FlowLayout()); final JLabel label1 = new JLabel(CHI_SQUARE); miscPane.add(label1); chiSqTextField = new JTextField(7); chiSqTextField.setEditable(false); miscPane.add(chiSqTextField); final JLabel label2 = new JLabel(PHOTON_COUNT); miscPane.add(label2); photonTextField = new JTextField(7); photonTextField.setEditable(false); miscPane.add(photonTextField); logCheckBox = new JCheckBox(LOGARITHMIC); logCheckBox.setSelected(logarithmic); logCheckBox.addItemListener(new ItemListener() { @Override public void itemStateChanged(final ItemEvent e) { logarithmic = logCheckBox.isSelected(); NumberAxis photonAxis; if (logarithmic) { photonAxis = new LogarithmicAxis(PHOTON_AXIS_LABEL); } else { photonAxis = new NumberAxis(PHOTON_AXIS_LABEL); } decaySubPlot.setRangeAxis(photonAxis); } }); miscPane.add(logCheckBox); container.add(miscPane, BorderLayout.SOUTH); System.out.println("size from prefs " + getSizeFromPreferences()); // _frame.setSize(getSizeFromPreferences()); // _frame.setMaximumSize(MAX_SIZE); // doesn't work; bug in Java frame.pack(); frame.setLocationRelativeTo(parentFrame); frame.setVisible(true); frame.addComponentListener(new ComponentListener() { @Override public void componentHidden(final ComponentEvent e) { } @Override public void componentMoved(final ComponentEvent e) { } @Override public void componentResized(final ComponentEvent e) { // constrain maximum size boolean resize = false; final Dimension size = frame.getSize(); System.out.println("COMPONENT RESIZED incoming size " + size); if (size.width > MAX_SIZE.width) { size.width = MAX_SIZE.width; resize = true; } if (size.height > MAX_SIZE.height) { size.height = MAX_SIZE.height; resize = true; } if (size.width < MIN_SIZE.width) { size.width = MIN_SIZE.width; resize = true; } if (size.height < MIN_SIZE.height) { size.height = MIN_SIZE.height; resize = true; } if (resize) { frame.setSize(size); } System.out.println("save resized " + resize + " size " + size); saveSizeInPreferences(size); } @Override public void componentShown(final ComponentEvent e) { } }); this.frame.addWindowListener(new WindowAdapter() { @Override public void windowClosing(final WindowEvent e) { if (null != picker) { // TODO ARG does not work if you use the name 'pixelPicker' // (collides with local var; this.pP won't work). picker.hideCursor(); } } }); this.frame.setSize(getSizeFromPreferences()); } return this.frame; } @Override public void setFittingCursor(final FittingCursor fittingCursor) { System.out.println("DefaultDecayGraph.setFittingCursor " + fittingCursor); if (null == this.fittingCursor) { // first time, create a listener fittingCursorListener = new FittingCursorListenerImpl(); } else if (this.fittingCursor != fittingCursor) { // fitting cursor changed, remove listener from old version this.fittingCursor.removeListener(fittingCursorListener); } this.fittingCursor = fittingCursor; this.fittingCursor.addListener(fittingCursorListener); } @Override public void setTitle(final String title) { frame.setTitle(title); } @Override public void setData(final int promptIndex, final double[] prompt, final FitResults fitResults) { createDatasets(bins, timeInc, promptIndex, prompt, fittingCursor, fitResults); } @Override public void setChiSquare(final double chiSquare) { final String text = "" + roundToDecimalPlaces(chiSquare, 6); chiSqTextField.setText(text); } @Override public void setPhotons(final int photons) { photonTextField.setText("" + photons); } /* * Sets whether vertical axis should be logarithmic. * */ public void setLogarithmic(final boolean logarithmic) { this.logarithmic = logarithmic; } @Override public void setStartStop(final double transStart, final double dataStart, final double transStop) { startStopDraggingUI.setStartStopValues(transStart, dataStart, transStop); } @Override public void setStartStopProportion(final double transStartProportion, final double dataStartProportion, final double transStopProportion) { // calculate new start and stop final double transStartCalc = transStartProportion * maxValue; final double dataStartCalc = dataStartProportion * maxValue; final double transStopCalc = transStopProportion * maxValue; // if changed, notify cursor listeners if (null == transStart || transStart != transStartCalc || null == dataStart || dataStart != dataStartCalc || null == transStop || transStop != transStopCalc) { transStart = transStartCalc; dataStart = dataStartCalc; transStop = transStopCalc; if (null != fittingCursor) { fittingCursor.setTransientStartTime(transStart); fittingCursor.setDataStartTime(dataStart); fittingCursor.setTransientStopTime(transStop); } } } /** * Creates the chart * * @param bins number of bins * @param timeInc time increment per bin * @return the chart */ JFreeChart createCombinedChart(final int bins, final double timeInc) { // create empty chart data sets decayDataset = new XYSeriesCollection(); residualDataset = new XYSeriesCollection(); // make a common horizontal axis for both sub-plots final NumberAxis timeAxis = new NumberAxis(TIME_AXIS_LABEL); timeAxis.setLabel(UNITS_LABEL); timeAxis.setRange(0.0, (bins - 1) * timeInc); // make a vertically combined plot final CombinedDomainXYPlot parent = new CombinedDomainXYPlot(timeAxis); // create decay sub-plot NumberAxis photonAxis; if (logarithmic) { photonAxis = new LogarithmicAxis(PHOTON_AXIS_LABEL); } else { photonAxis = new NumberAxis(PHOTON_AXIS_LABEL); } final XYSplineRenderer decayRenderer = new XYSplineRenderer(); decayRenderer.setSeriesShapesVisible(0, false); decayRenderer.setSeriesShapesVisible(1, false); decayRenderer.setSeriesLinesVisible(2, false); decayRenderer.setSeriesShape(2, new Ellipse2D.Float(-1.0f, -1.0f, 2.0f, 2.0f)); decayRenderer.setSeriesPaint(0, PROMPT_COLOR); decayRenderer.setSeriesPaint(1, FITTED_COLOR); decayRenderer.setSeriesPaint(2, DECAY_COLOR); decaySubPlot = new XYPlot(decayDataset, null, photonAxis, decayRenderer); decaySubPlot.setDomainCrosshairVisible(true); decaySubPlot.setRangeCrosshairVisible(true); // add decay sub-plot to parent parent.add(decaySubPlot, DECAY_WEIGHT); // create residual sub-plot final NumberAxis residualAxis = new NumberAxis(RESIDUAL_AXIS_LABEL); final XYSplineRenderer residualRenderer = new XYSplineRenderer(); residualRenderer.setSeriesPaint(0, RESIDUAL_COLOR); residualRenderer.setSeriesLinesVisible(0, false); residualRenderer.setSeriesShape(0, new Ellipse2D.Float(-1.0f, -1.0f, 2.0f, 2.0f)); final XYPlot residualSubPlot = new XYPlot(residualDataset, null, residualAxis, residualRenderer); residualSubPlot.setDomainCrosshairVisible(true); residualSubPlot.setRangeCrosshairVisible(true); residualSubPlot.setFixedLegendItems(null); // add residual sub-plot to parent parent.add(residualSubPlot, RESIDUAL_WEIGHT); // now make the top level JFreeChart final JFreeChart chart = new JFreeChart(null, JFreeChart.DEFAULT_TITLE_FONT, parent, true); chart.removeLegend(); return chart; } /** * Creates the data sets for the chart * * @param bins number of time bins * @param timeInc time increment per time bin * @param promptIndex starting bin of prompt * @param prompt prompt curve * @param fittingCursor cursor information * @param fitResults from the fit */ private void createDatasets(final int bins, final double timeInc, final int promptIndex, final double[] prompt, final FittingCursor fittingCursor, final FitResults fitResults) { final XYSeries series1 = new XYSeries("Prompt"); final XYSeries series2 = new XYSeries("Fitted"); final XYSeries series3 = new XYSeries("Data"); final XYSeries series4 = new XYSeries("Residuals"); double xCurrent; // show transient data; find the maximum transient data in this pass double yDataMax = -Double.MAX_VALUE; xCurrent = 0.0; for (int i = 0; i < bins; ++i) { // show transient data final double yData = fitResults.getTransient()[i]; // keep track of maximum if (yData > yDataMax) { yDataMax = yData; } // logarithmic plots can't handle <= 0.0 series3.add(xCurrent, (yData > 0.0 ? yData : null)); xCurrent += timeInc; } // show prompt if any if (null != prompt) {// debugging double yPromptMax = -Double.MAX_VALUE; for (int i = 0; i < prompt.length; ++i) { // debugging System.out.println(" " + prompt[i]); // find max if (prompt[i] > yPromptMax) { yPromptMax = prompt[i]; } } // add prompt data xCurrent = 0.0; for (int i = 0; i < bins; ++i) { Double value = null; if (null != prompt) { if (i >= promptIndex) { if (i - promptIndex < prompt.length) { // logarithmic plots can't handle <= 0 if (prompt[i - promptIndex] > 0.0) { value = prompt[i - promptIndex]; value = yDataMax * value / yPromptMax; } } } } series1.add(xCurrent, value); xCurrent += timeInc; } } final int transStart = fittingCursor.getTransientStartIndex(); final int dataStart = fittingCursor.getDataStartIndex(); int transEnd = fittingCursor.getTransientStopIndex(); System.out.println("graphing cursors indices are " + transStart + " " + dataStart + " " + transEnd); if (0 == transStart && 0 == dataStart && 0 == transEnd) { transEnd = bins - 1; } // show fitted & residuals xCurrent = 0.0; for (int i = 0; i < bins; ++i) { // only within cursors if (transStart <= i && i <= transEnd) { // from transStart..transEnd show fitted final double yFitted = fitResults.getYFitted()[i - transStart]; // don't allow fitted to grow the chart downward or upward if (1.0 <= yFitted && yFitted <= yDataMax) { series2.add(xCurrent, yFitted); } else { series2.add(xCurrent, null); } // from dataStart..transEnd show residuals if (dataStart <= i && 0.0 < yFitted) { final double yData = fitResults.getTransient()[i]; series4.add(xCurrent, yData - yFitted); } else { series4.add(xCurrent, null); } } else { series2.add(xCurrent, null); series4.add(xCurrent, null); } xCurrent += timeInc; } synchronized (synchObject) { decayDataset.removeAllSeries(); decayDataset.addSeries(series1); decayDataset.addSeries(series2); decayDataset.addSeries(series3); residualDataset.removeAllSeries(); residualDataset.addSeries(series4); } } /** * Restores size from Java Preferences. * * @return size */ private Dimension getSizeFromPreferences() { final Preferences prefs = Preferences.userNodeForPackage(this.getClass()); return new Dimension(prefs.getInt(WIDTH_KEY, FRAME_SIZE.width), prefs.getInt(HEIGHT_KEY, FRAME_SIZE.height)); } /** * Saves the size to Java Preferences. * */ private void saveSizeInPreferences(final Dimension size) { final Preferences prefs = Preferences.userNodeForPackage(this.getClass()); prefs.putInt(WIDTH_KEY, size.width); prefs.putInt(HEIGHT_KEY, size.height); } private double roundToDecimalPlaces(final double value, final int decimalPlaces) { final double decimalTerm = Math.pow(10.0, decimalPlaces); final int tmp = (int) Math.round(value * decimalTerm); final double rounded = tmp / decimalTerm; return rounded; } /** * Inner class, UI which allows us to paint on top of the components, using * JXLayer. * * @param <V> component */ static class StartStopDraggingUI<V extends JComponent> extends AbstractLayerUI<V> { private static final int CLOSE_ENOUGH = 4; // pizels private final ChartPanel panel; private final XYPlot plot; private final IStartStopProportionListener listener; private final double maxValue; private boolean dragTransStartMarker = false; private boolean dragDataStartMarker = false; private boolean dragTransStopMarker = false; private volatile Double transStartMarkerProportion; private volatile Double dataStartMarkerProportion; private volatile Double transStopMarkerProportion; private int y0; private int y1; private int xTransStart; private int xDataStart; private int xTransStop; /** * Creates the UI. * * @param panel for the chart * @param plot within the chart * @param listener to be notified when user drags start/stop vertical bars * @param maxValue used to scale cursors */ StartStopDraggingUI(final ChartPanel panel, final XYPlot plot, final IStartStopProportionListener listener, final double maxValue) { this.panel = panel; this.plot = plot; this.listener = listener; this.maxValue = maxValue; } void setStartStopValues(final double transStartValue, final double dataStartValue, final double transStopValue) { transStartMarkerProportion = transStartValue / maxValue; dataStartMarkerProportion = dataStartValue / maxValue; transStopMarkerProportion = transStopValue / maxValue; } /** * Used to draw the start/stop vertical bars. Overrides 'paintLayer()', not * 'paint()'. * */ @Override protected void paintLayer(final Graphics2D g2D, final JXLayer<? extends V> layer) { // synchronized with chart data update synchronized (synchObject) { // this paints chart layer as is super.paintLayer(g2D, layer); } if (null != transStartMarkerProportion && null != dataStartMarkerProportion && null != transStopMarkerProportion) { // adjust to current size final Rectangle2D area = getDataArea(); final double x = area.getX(); y0 = (int) area.getY(); y1 = (int) (area.getY() + area.getHeight()); final double width = area.getWidth(); // System.out.println("x " + area.getX() + " y " + area.getY() + // " width " + area.getWidth() + " height " + area.getHeight()); xTransStart = (int) Math.round(x + width * transStartMarkerProportion) + HORZ_TWEAK; xDataStart = (int) Math.round(x + width * dataStartMarkerProportion) + HORZ_TWEAK; xTransStop = (int) Math.round(x + width * transStopMarkerProportion) + HORZ_TWEAK; // custom painting is here g2D.setStroke(new BasicStroke(2f)); g2D.setXORMode(XORvalue(TRANS_START_COLOR)); g2D.drawLine(xTransStart, y0, xTransStart, y1); g2D.setXORMode(XORvalue(DATA_START_COLOR)); g2D.drawLine(xDataStart, y0, xDataStart, y1); g2D.setXORMode(XORvalue(TRANS_STOP_COLOR)); g2D.drawLine(xTransStop, y0, xTransStop, y1); } } /** * Mouse listener, catches drag events * */ @Override protected void processMouseMotionEvent(final MouseEvent event, final JXLayer<? extends V> layer) { super.processMouseMotionEvent(event, layer); if (event.getID() == MouseEvent.MOUSE_DRAGGED) { if (dragTransStartMarker || dragDataStartMarker || dragTransStopMarker) { final double newProportion = getDraggedProportion(event); if (dragTransStartMarker) { if (newProportion <= dataStartMarkerProportion) { transStartMarkerProportion = newProportion; } else { transStartMarkerProportion = dataStartMarkerProportion; } } else if (dragDataStartMarker) { if (newProportion >= transStartMarkerProportion) { if (newProportion <= transStopMarkerProportion) { dataStartMarkerProportion = newProportion; } else { dataStartMarkerProportion = transStopMarkerProportion; } } else { dataStartMarkerProportion = transStartMarkerProportion; } } else { if (newProportion >= dataStartMarkerProportion) { transStopMarkerProportion = newProportion; } else { transStopMarkerProportion = dataStartMarkerProportion; } } // mark the ui as dirty and needing to be repainted setDirty(true); } } } private Color XORvalue(final Color color) { final int drawRGB = color.getRGB(); final int backRGB = BACK_COLOR.getRGB(); return new Color(drawRGB ^ backRGB); } /** * Gets the currently dragged horizontal value as a proportion, a value * between 0.0 and 1.0. * * @return proportion */ private double getDraggedProportion(final MouseEvent e) { final Rectangle2D dataArea = panel.getChartRenderingInfo().getPlotInfo().getDataArea(); final Rectangle2D area = getDataArea(); double proportion = (e.getX() - area.getX()) / area.getWidth(); if (proportion < 0.0) { proportion = 0.0; } else if (proportion > 1.0) { proportion = 1.0; } return proportion; } /** * Mouse listener, catches mouse button events. * */ @Override protected void processMouseEvent(final MouseEvent e, final JXLayer<? extends V> l) { super.processMouseEvent(e, l); if (null != transStartMarkerProportion && null != transStopMarkerProportion) { if (e.getID() == MouseEvent.MOUSE_PRESSED) { final int x = e.getX(); final int y = e.getY(); if (y > y0 - CLOSE_ENOUGH && y < y1 + CLOSE_ENOUGH) { if (Math.abs(x - xTransStart) < CLOSE_ENOUGH) { // check for superimposition if (xTransStart == xDataStart) { if (xTransStart == xTransStop) { // all three superimposed if (x < xTransStart) { // start dragging trans start line dragTransStartMarker = true; } else { // start dragging trans stop line dragTransStopMarker = true; } } else { // trans and data start superimposed if (x < xTransStart) { // start dragging trans start line dragTransStartMarker = true; } else { // start dragging data start line dragDataStartMarker = true; } } } else { // no superimposition; start dragging start line dragTransStartMarker = true; } } else if (Math.abs(x - xDataStart) < CLOSE_ENOUGH) { // check for superimposition if (xDataStart == xTransStop) { // data start and trans stop superimposed if (x < xDataStart) { // start dragging data start line dragDataStartMarker = true; } else { // start dragging trans stop line dragTransStopMarker = true; } } else { // no superimposition; start dragging data start line dragDataStartMarker = true; } } else if (Math.abs(x - xTransStop) < CLOSE_ENOUGH) { // possible superimpositions already checked // start dragging trans stop line dragTransStopMarker = true; } } } if (e.getID() == MouseEvent.MOUSE_RELEASED) { dragTransStartMarker = dragDataStartMarker = dragTransStopMarker = false; SwingUtilities.invokeLater(new Runnable() { @Override public void run() { if (null != listener) { listener.setStartStopProportion(transStartMarkerProportion, dataStartMarkerProportion, transStopMarkerProportion); } } }); } } } /** * Gets the area of the chart panel. As you resize larger and larger the * maximum value returned for height is 724 and the maximum width 961. * * @return 2D rectangle area */ private Rectangle2D getDataArea() { final Rectangle2D dataArea = panel.getChartRenderingInfo().getPlotInfo().getDataArea(); return dataArea; } /** * Converts screen x to chart x value. * * @return chart value */ private double screenToValue(final int x) { return plot.getDomainAxis().java2DToValue(x, getDataArea(), RectangleEdge.TOP); } } private class FittingCursorListenerImpl implements FittingCursorListener { @Override public void cursorChanged(final FittingCursor cursor) { final double transStart = cursor.getTransientStartTime(); final double dataStart = cursor.getDataStartTime(); final double transStop = cursor.getTransientStopTime(); setStartStop(transStart, dataStart, transStop); frame.repaint(); } } } /** * Used only within DecayGraph, to get results from StartStopDraggingUI inner * class. * * @author Aivar Grislis */ interface IStartStopProportionListener { public void setStartStopProportion(double transStartProportion, double dataStartProportion, double transStopProportion); }