/*
* Chart2D, a component for displaying ITrace2D instances.
* Copyright (C) 2004 - 2011 Achim Westermann, Achim.Westermann@gmx.de
*
* This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version.
*
* This library 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*
* If you modify or optimize the code in a useful way please let me know. Achim.Westermann@gmx.de
*/
package info.monitorenter.gui.chart;
import info.monitorenter.gui.chart.ITrace2D.DistancePoint;
import info.monitorenter.gui.chart.axis.AAxis;
import info.monitorenter.gui.chart.axis.AxisLinear;
import info.monitorenter.gui.chart.axistickpainters.AxisTickPainterDefault;
import info.monitorenter.gui.chart.events.Chart2DActionPrintSingleton;
import info.monitorenter.util.Range;
import info.monitorenter.util.StringUtil;
import java.awt.Color;
import java.awt.Container;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Stroke;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
import java.awt.event.MouseMotionListener;
import java.awt.image.BufferedImage;
import java.awt.print.PageFormat;
import java.awt.print.Printable;
import java.awt.print.PrinterException;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import javax.swing.JPanel;
import javax.swing.JToolTip;
import javax.swing.Timer;
/**
* <code> Chart2D</code> is a component for displaying the data contained in a
* <code>ITrace2D</code>. It inherits many features from
* <code>javax.swing.JPanel</code> and allows specific configuration. <br>
* In order to simplify the use of it, the scaling, labeling and choosing of
* display- range is done automatically which flattens the free configuration.
* <p>
* There are several default settings that may be changed in
* <code>Chart2D</code><br>
* <ul>
* <li>The display range is chosen always big enough to show every
* <code>TracePoint2D</code> contained in the all <code>ITrace2d</code>
* instances connected. This is because the
* {@link info.monitorenter.gui.chart.IAxis} of the chart (for x and y) use by
* default a
* {@link info.monitorenter.gui.chart.rangepolicies.RangePolicyUnbounded}. To
* change this, get the axis of the chart to change (via {@link #getAxisX()},
* {@link #getAxisY()}) and invoke
* {@link info.monitorenter.gui.chart.IAxis#setRangePolicy(IRangePolicy)} with
* the desired view port behavior.
* <li>During the <code>paint()</code> operation every <code>TracePoint2D</code>
* is taken from the <code>ITrace2D</code>- instance exactly in the order, it's
* iterator returns them. From every <code>TracePoint2D</code> then a line is
* drawn to the next. <br>
* Unordered traces may cause a weird display. Choose the right implementation
* of <code>ITrace2D</code> to avoid this. To change this line painting behavior
* you can use custom renderers at the level of traces via
* {@link info.monitorenter.gui.chart.ITrace2D#addTracePainter(ITracePainter)}
* or
* {@link info.monitorenter.gui.chart.ITrace2D#setTracePainter(ITracePainter)}.
* <li>If no scaling is chosen, no grids will be painted. See:
* <code>{@link IAxis#setPaintScale(boolean)}</code> This allows saving of many
* computations.
* <li>The distance of the scalepoints is always big enough to display the
* labels fully without overwriting each other.</li>
* </ul>
* <p>
* <h3>Demo- code:</h3>
*
* <pre>
*
* ...
* Chart2D test = new Chart2D();
* JFrame frame = new JFrame("Chart2D- Debug");
*
* frame.setSize(400,200);
* frame.setVisible(true);
* ITrace2D atrace = new Trace2DLtd(100);
* ...
* <further configuration of trace>
* ...
* test.addTrace(atrace);
* ....
* while(expression){
* atrace.addPoint(adouble,bdouble);
* ....
* }
* </pre>
*
* <p>
* <h3>PropertyChangeEvents</h3>
* {@link java.beans.PropertyChangeListener} instances may be added via
* {@link javax.swing.JComponent#addPropertyChangeListener(java.lang.String, java.beans.PropertyChangeListener)}
* . They inherit the properties to listen from
* {@link java.awt.Container#addPropertyChangeListener(java.lang.String, java.beans.PropertyChangeListener)}
* . Additionally more <code>PropertyChangeEvents</code> are triggered.
* <p>
* As the set of traces inside this class is a collection (and no single
* property) the {@link java.beans.PropertyChangeEvent} fired for a change of
* properties property will contain a reference to the <code>Chart2D</code>
* instance as well as the <code>ITrace2D</code> (if involved in the change). <br>
* <table width="100%">
* <tr>
* <th ><code>getPropertyName()</code></th>
* <th><code>getSource()</code></th>
* <th><code>getOldValue()</code></th>
* <th><code>getNewValue()</code></th>
* <th>occurrence</th>
* </tr>
* <tr>
* <td>{@link #PROPERTY_BACKGROUND_COLOR}</td>
* <td>{@link Chart2D}</td>
* <td>{@link java.awt.Color}</td>
* <td>{@link java.awt.Color}</td>
* <td>if a change of the background color occurs.</td>
* </tr>
* <tr>
* <td>{@link #PROPERTY_AXIS_X}</td>
* <td>{@link Chart2D}</td>
* <td>null</td>
* <td>{@link info.monitorenter.gui.chart.IAxis}</td>
* <td>if a new axis is added in x dimension ({@link #addAxisXBottom(AAxis)}, @link
* {@link #addAxisXTop(AAxis)}).</td>
* </tr>
* <tr>
* <td>{@link #PROPERTY_AXIS_X}</td>
* <td>{@link Chart2D}</td>
* <td>{@link info.monitorenter.gui.chart.IAxis}</td>
* <td>null</td>
* <td>if an axis is removed in x dimension ({@link #removeAxisXBottom(IAxis)},
* {@link #removeAxisXTop(IAxis)}).</td>
* </tr>
* <tr>
* <td>{@link #PROPERTY_AXIS_Y}</td>
* <td>{@link Chart2D}</td>
* <td>null</td>
* <td>{@link info.monitorenter.gui.chart.IAxis}</td>
* <td>if a new axis is added in y dimension ({@link #addAxisYLeft(AAxis)},
* {@link #addAxisYRight(AAxis)}).</td>
* </tr>
* <tr>
* <td>{@link #PROPERTY_AXIS_X_BOTTOM_REPLACE}</td>
* <td>{@link Chart2D}</td>
* <td>{@link info.monitorenter.gui.chart.IAxis}</td>
* <td>{@link info.monitorenter.gui.chart.IAxis}</td>
* <td>if a axis is replaced in bottom x dimension (
* {@link #setAxisXBottom(AAxis, int)}).</td>
* </tr>
* <tr>
* <td>{@link #PROPERTY_AXIS_X_TOP_REPLACE}</td>
* <td>{@link Chart2D}</td>
* <td>{@link info.monitorenter.gui.chart.IAxis}</td>
* <td>{@link info.monitorenter.gui.chart.IAxis}</td>
* <td>if a axis is replaced in top x dimension (
* {@link #setAxisXTop(AAxis, int)}).</td>
* </tr>
* <tr>
* <td>{@link #PROPERTY_AXIS_Y_LEFT_REPLACE}</td>
* <td>{@link Chart2D}</td>
* <td>{@link info.monitorenter.gui.chart.IAxis}</td>
* <td>{@link info.monitorenter.gui.chart.IAxis}</td>
* <td>if a axis is replaced in left y dimension (
* {@link #setAxisYLeft(AAxis, int)}).</td>
* </tr>
* <tr>
* <td>{@link #PROPERTY_AXIS_Y_RIGHT_REPLACE}</td>
* <td>{@link Chart2D}</td>
* <td>{@link info.monitorenter.gui.chart.IAxis}</td>
* <td>{@link info.monitorenter.gui.chart.IAxis}</td>
* <td>if a axis is replaced in right y dimension (
* {@link #setAxisYRight(AAxis, int)} ).</td>
* </tr>
* <tr>
* <td>{@link #PROPERTY_GRID_COLOR}</td>
* <td>{@link Chart2D}</td>
* <td>{@link java.awt.Color}</td>
* <td>{@link java.awt.Color}</td>
* <td>if a change of the grid color occurs.</td>
* </tr>
* <tr>
* <td>{@link #PROPERTY_ADD_REMOVE_TRACE}</td>
* <td>{@link Chart2D}</td>
* <td>{@link ITrace2D}</td>
* <td>{@link ITrace2D}</td>
* <td>
* If a change of the traces occurs. If the old value is null a new trace has
* been added. If the new value is null, oldvalue trace has been removed. If
* both are null this is a bug.</td>
* </tr>
* <tr>
* <td>{@link #PROPERTY_PAINTLABELS}</td>
* <td>{@link Chart2D}</td>
* <td>{@link java.lang.Boolean}</td>
* <td>{@link java.lang.Boolean}</td>
* <td>if a change of the paint labels flag occurs.</td>
* </tr>
* <tr>
* <td>{@link #PROPERTY_TOOLTIP_TYPE}</td>
* <td>{@link Chart2D}</td>
* <td>{@link IToolTipType}</td>
* <td>{@link IToolTipType}</td>
* <td>if a change of the tool tip type occurs.</td>
* </tr>
* <tr>
* <td>{@link #PROPERTY_POINT_HIGHLIGHTING_ENABLED}</td>
* <td>{@link Chart2D}</td>
* <td>{@link Boolean}</td>
* <td>{@link Boolean}</td>
* <td>if point highlighting is enabled/disabled.</td>
* </tr>
* <tr>
* <td>{@link ITrace2D#PROPERTY_POINT_HIGHLIGHTERS_CHANGED}</td>
* <td>{@link Chart2D}</td>
* <td>{@link IPointPainter}</td>
* <td>null</td>
* <td>if a point highlighter was added to any of the currently contained
* traces.</td>
* </tr>
* <tr>
* <td>{@link ITrace2D#PROPERTY_POINT_HIGHLIGHTERS_CHANGED}</td>
* <td>{@link Chart2D}</td>
* <td>null</td>
* <td>{@link IPointPainter}</td>
* <td>if a point highlighter was removed from any of the currently contained
* traces.</td>
* </tr>
* <tr>
* <td>{@link #PROPERTY_ANTIALIASING_ENABLED}</td>
* <td>{@link Chart2D}</td>
* <td>{@link Boolean}</td>
* <td>{@link Boolean}</td>
* <td>if antialiasing is enabled/disabled.</td>
* </tr>
* <tr>
* <td>{@link #PROPERTY_POINTFINDER}</td>
* <td>{@link Chart2D}</td>
* <td>{@link IPointFinder}</td>
* <td>{@link IPointFinder}</td>
* <td>if {@link Chart2D#setPointFinder(IPointFinder)} caused a change.</td>
* </tr>
* </table>
* <p>
*
* @author <a href='mailto:Achim.Westermann@gmx.de'>Achim Westermann </a>
*
* @version $Revision: 1.142.2.1 $
*/
public class Chart2D extends JPanel implements PropertyChangeListener, Iterable<ITrace2D>,
Printable {
/**
* Types of tool tip.
* <p>
*
* @author <a href="mailto:Achim.Westermann@gmx.de">Achim Westermann</a>
* @version $Revision: 1.142.2.1 $
*/
public static enum PointFinder implements IPointFinder {
/**
* Uses the Manhattan distance to find the nearest point.
* <p>
* This implementation is slower than MANHATTAN but has a search field in
* form of a circle which is more natural for human eyes.
*
* @see Chart2D#getNearestPointEuclid(int, int)
*/
EUCLID {
/**
* @see info.monitorenter.gui.chart.Chart2D.PointFinder#getNearestPoint(java.awt.event.MouseEvent,
* info.monitorenter.gui.chart.Chart2D)
*/
public ITracePoint2D getNearestPoint(final int mouseEventX, final int mouseEventY,
final Chart2D chart) {
return chart.getNearestPointEuclid(mouseEventX, mouseEventY);
}
},
/**
* Uses the Manhattan distance to find the nearest point.
* <p>
* This implementation is faster than EUCLID (only
* subtractions/additions/abs) but has a search field in form of a rhombus
* which may confuse human eyes.
*
* @see Chart2D#getNearestPointManhattan(int, int)
*/
MANHATTAN {
/**
* @see info.monitorenter.gui.chart.Chart2D.PointFinder#getNearestPoint(java.awt.event.MouseEvent,
* info.monitorenter.gui.chart.Chart2D)
*/
public ITracePoint2D getNearestPoint(final int mouseEventX, final int mouseEventY,
final Chart2D chart) {
return chart.getNearestPointManhattan(mouseEventX, mouseEventY);
}
};
/**
* Default implementation always returns null.
* <p>
*
* @see info.monitorenter.gui.chart.IPointFinder#getNearestPoint(java.awt.event.MouseEvent,
* Chart2D)
*/
public ITracePoint2D getNearestPoint(final MouseEvent me, final Chart2D chart) {
return this.getNearestPoint(me.getX(), me.getY(), chart);
}
}
/**
* Types of tool tip.
* <p>
*
* @author <a href="mailto:Achim.Westermann@gmx.de">Achim Westermann</a>
* @version $Revision: 1.142.2.1 $
*/
public static enum ToolTipType implements IToolTipType {
/**
* Chart data value tool tips are shown.
* <p>
* Note that this implementation only works correctly for one left y axis
* and one bottom x axis as it does not search for the nearest trace.
* Displayed values will be formatted according to the formatting of the
* axes mentioned above.
* <p>
*/
DATAVALUES {
/**
* @see info.monitorenter.gui.chart.IToolTipType#getDescription()
*/
@Override
public String getDescription() {
return "Value at cursor (rel. to 1st trace)";
}
/**
* @see info.monitorenter.gui.chart.Chart2D.ToolTipType#getToolTipText(java.awt.event.MouseEvent)
*/
@Override
public String getToolTipText(final Chart2D chart, final MouseEvent me) {
String result;
ITracePoint2D tracePoint = chart.translateMousePosition(me);
StringBuffer buffer = new StringBuffer("X: ");
buffer.append(chart.getAxisX().getFormatter().format(tracePoint.getX())).append(" ");
buffer.append("Y: ");
buffer.append(chart.getAxisY().getFormatter().format(tracePoint.getY()));
result = buffer.toString();
return result;
}
},
/** No tool tips are shown. */
NONE,
/** Pixel tool tips are shown (used for debugging). */
PIXEL {
/**
* @see info.monitorenter.gui.chart.IToolTipType#getDescription()
*/
@Override
public String getDescription() {
return "Pixel, not implemented yet";
}
/**
* @see info.monitorenter.gui.chart.Chart2D.ToolTipType#getToolTipText(java.awt.event.MouseEvent)
*/
@Override
public String getToolTipText(final Chart2D chart, final MouseEvent me) {
return "pixel, not implemented yet";
}
},
/**
* Snaps to the nearest <code>{@link TracePoint2D}</code> and shows it's
* value.
* <p>
* Warning: due to the data structure of multiple axes this is very
* expensive!
* <p>
*/
VALUE_SNAP_TO_TRACEPOINTS {
/**
* @see info.monitorenter.gui.chart.IToolTipType#getDescription()
*/
@Override
public String getDescription() {
return "Values, snap to nearest point";
}
/**
* @see info.monitorenter.gui.chart.Chart2D.ToolTipType#getToolTipText(java.awt.event.MouseEvent)
*/
@Override
public String getToolTipText(final Chart2D chart, final MouseEvent me) {
String result;
ITracePoint2D point = chart.getPointFinder().getNearestPoint(me, chart);
/*
* We need the axes of the point for correct formatting (expensive...).
*/
ITrace2D trace = point.getListener();
IAxis<?> xAxis = chart.getAxisX(trace);
IAxis<?> yAxis = chart.getAxisY(trace);
chart.setRequestedRepaint(true);
StringBuffer buffer = new StringBuffer("X: ");
buffer.append(xAxis.getFormatter().format(point.getX())).append(" ");
buffer.append("Y: ");
buffer.append(yAxis.getFormatter().format(point.getY()));
result = buffer.toString();
return result;
}
};
/**
* @see info.monitorenter.gui.chart.IToolTipType#getDescription()
*/
public String getDescription() {
return "None";
}
/**
* @see info.monitorenter.gui.chart.IToolTipType#getToolTipText(info.monitorenter.gui.chart.Chart2D,
* java.awt.event.MouseEvent)
*/
public String getToolTipText(final Chart2D chart, final MouseEvent me) {
// NONE implementation (defined by this enum type).
return null;
}
}
/** Speaking names for axis constants - used for debugging only. */
public static final String[] AXIX_CONSTANT_NAMES = new String[] {"dummy", "X", "Y", "X,Y" };
/**
* Constant describing the bottom side of the chart.
* <p>
*
* @see IAxis#getAxisPosition()
*/
public static final int CHART_POSITION_BOTTOM = 32;
/**
* Constant describing the left side of the chart.
* <p>
*
* @see IAxis#getAxisPosition()
*/
public static final int CHART_POSITION_LEFT = 4;
/**
* Constant describing the right side of the chart.
* <p>
*
* @see IAxis#getAxisPosition()
*/
public static final int CHART_POSITION_RIGHT = 8;
/**
* Constant describing the top side of the chart.
* <p>
*
* @see IAxis#getAxisPosition()
*/
public static final int CHART_POSITION_TOP = 16;
/**
* A package wide switch for debugging problems with scaling. Set to false the
* compiler will remove the debugging statements.
*/
public static final boolean DEBUG_SCALING = false;
/**
* A package wide switch for debugging problems with highlighting. Set to
* false the compiler will remove the debugging statements.
*/
public static final boolean DEBUG_HIGHLIGHTING = false;
/**
* A package wide switch for debugging problems with multithreading. Set to
* false the compiler will remove the debugging statements.
*/
public static final boolean DEBUG_THREADING = false;
/**
* The bean property <code>constant</code> identifying a change of the
* antialiasing enabled state.
* <p>
* Use this constant to register a {@link java.beans.PropertyChangeListener}
* with the <code>Chart2D</code>.
* <p>
*/
public static final String PROPERTY_ANTIALIASING_ENABLED = "Chart2D.PROPERTY_ANTIALIASING_ENABLED";
/**
* The bean property <code>constant</code> identifying a change of the
* internal <code>{@link IAxis}</code> instance for the x dimension.
* <p>
* Use this constant to register a {@link java.beans.PropertyChangeListener}
* with the <code>Chart2D</code>.
* <p>
* See the class description for property change events fired.
* <p>
*/
public static final String PROPERTY_AXIS_X = "Chart2D.PROPERTY_AXIS_X";
/**
* The bean property <code>constant</code> identifying a replacement of an
* internal <code>{@link IAxis}</code> instance for the bottom x dimension.
* <p>
* Use this constant to register a {@link java.beans.PropertyChangeListener}
* with the <code>Chart2D</code>.
* <p>
* See the class description for property change events fired.
* <p>
*/
public static final String PROPERTY_AXIS_X_BOTTOM_REPLACE = "Chart2D.PROPERTY_AXIS_X_BOTTOM_REPLACE";
/**
* The bean property <code>constant</code> identifying a replacement of an
* internal <code>{@link IAxis}</code> instance for the top x dimension.
* <p>
* Use this constant to register a {@link java.beans.PropertyChangeListener}
* with the <code>Chart2D</code>.
* <p>
* See the class description for property change events fired.
* <p>
*/
public static final String PROPERTY_AXIS_X_TOP_REPLACE = "Chart2D.PROPERTY_AXIS_X_TOP_REPLACE";
/**
* The bean property <code>constant</code> identifying a change of the
* internal <code>{@link IAxis}</code> instance for the y dimension.
* <p>
* Use this constant to register a {@link java.beans.PropertyChangeListener}
* with the <code>Chart2D</code>.
* <p>
* See the class description for property change events fired.
* <p>
*/
public static final String PROPERTY_AXIS_Y = "Chart2D.PROPERTY_AXIS_Y";
/**
* The bean property <code>constant</code> identifying a replacement of an
* internal <code>{@link IAxis}</code> instance for the left y dimension.
* <p>
* Use this constant to register a {@link java.beans.PropertyChangeListener}
* with the <code>Chart2D</code>.
* <p>
* See the class description for property change events fired.
* <p>
*/
public static final String PROPERTY_AXIS_Y_LEFT_REPLACE = "Chart2D.PROPERTY_AXIS_Y_LEFT_REPLACE";
/**
* The bean property <code>constant</code> identifying a replacement of an
* internal <code>{@link IAxis}</code> instance for the right y dimension.
* <p>
* Use this constant to register a {@link java.beans.PropertyChangeListener}
* with the <code>Chart2D</code>.
* <p>
* See the class description for property change events fired.
* <p>
*/
public static final String PROPERTY_AXIS_Y_RIGHT_REPLACE = "Chart2D.PROPERTY_AXIS_Y_RIGHT_REPLACE";
/**
* The bean property <code>constant</code> identifying a change of the
* background color. <br>
* Use this constant to register a {@link java.beans.PropertyChangeListener}
* with the <code>Chart2D</code>.
* <p>
* The property change events for this change are constructed and fired by the
* superclass {@link java.awt.Container} so this constant is just for
* clarification of the String that is related to that property.
* <p>
*/
public static final String PROPERTY_BACKGROUND_COLOR = "background";
/**
* The bean property <code>constant</code> identifying a change of the font. <br/>
* <p>
* Use this constant to register a {@link java.beans.PropertyChangeListener}
* with the <code>Chart2D</code>.
* <p>
* The property change events for this change are constructed and fired by the
* superclass {@link java.awt.Container} so this constant is just for
* clarification of the String that is related to that property.
* <p>
*/
public static final String PROPERTY_FONT = "font";
/**
* The bean property <code>constant</code> identifying a change of the
* foreground color. <br>
* Use this constant to register a {@link java.beans.PropertyChangeListener}
* with the <code>Chart2D</code>.
* <p>
* The property change events for this change are constructed and fired by the
* superclass {@link java.awt.Container} so this constant is just for
* clarification of the String that is related to that property.
* <p>
*/
public static final String PROPERTY_FOREGROUND_COLOR = "foreground";
/**
* The bean property <code>constant</code> identifying a change of the grid
* color.
* <p>
* Use this constant to register a {@link java.beans.PropertyChangeListener}
* with the <code>Chart2D</code>.
* <p>
*/
public static final String PROPERTY_GRID_COLOR = "Chart2D.PROPERTY_GRID_COLOR";
/**
* The bean property <code>constant</code> identifying a change of traces.
* <p>
* Use this constant to register a {@link java.beans.PropertyChangeListener}
* with the <code>Chart2D</code>.
* <p>
*/
public static final String PROPERTY_ADD_REMOVE_TRACE = IAxis.PROPERTY_ADD_REMOVE_TRACE;
/**
* The bean property <code>constant</code> identifying a change of the paint
* labels flag.
* <p>
* Use this constant to register a {@link java.beans.PropertyChangeListener}
* with the <code>Chart2D</code>.
* <p>
*/
public static final String PROPERTY_PAINTLABELS = "Chart2D.PROPERTY_PAINTLABELS";
/**
* The bean property <code>constant</code> identifying a change of the point
* highlighting enabled state.
* <p>
* Use this constant to register a {@link java.beans.PropertyChangeListener}
* with the <code>Chart2D</code>.
* <p>
*/
public static final String PROPERTY_POINT_HIGHLIGHTING_ENABLED = "Chart2D.PROPERTY_POINT_HIGHLIGHTING_ENABLED";
/**
* The bean property <code>constant</code> identifying a change of the
* internal <code>{@link IPointFinder}</code> instance used to find the
* nearest point corresponding to mouse events over the chart.
* <p>
* Use this constant to register a {@link java.beans.PropertyChangeListener}
* with the <code>Chart2D</code>.
* <p>
* See the class description for property change events fired.
* <p>
*/
public static final String PROPERTY_POINTFINDER = "Chart2D.POINTFINDER";
/**
* The bean property <code>constant</code> identifying a change of the tool
* tip type (<code>{Chart2D{@link #setToolTipType(IToolTipType)}</code>). <br/>
* <p>
* Use this constant to register a {@link java.beans.PropertyChangeListener}
* with the <code>Chart2D</code>.
* <p>
* The property change events for this change are constructed and fired by the
* superclass {@link java.awt.Container} so this constant is just for
* clarification of the String that is related to that property.
* <p>
*/
public static final String PROPERTY_TOOLTIP_TYPE = "Chart2D.PROPERTY_TOOLTIP_TYPE";
/** Generated <code>serial version UID</code>. */
private static final long serialVersionUID = 3978425840633852978L;
/** Constant describing the x axis (needed for scaling). */
public static final int X = 1;
/** Constant describing the x and y axis (needed for scaling). */
public static final int X_Y = 3;
/** Constant describing the y axis (needed for scaling). */
public static final int Y = 2;
/**
* The bottom x axes of the chart.
* <p>
* The first element is always existing and is the downward compatible result
* of the call <code>{@link Chart2D#getAxisX()}</code>.
* <p>
*/
private List<IAxis<?>> m_axesXBottom;
/**
* The top x axes of the chart.
* <p>
* If empty no top x axes are shown.
* <p>
*/
private List<IAxis<?>> m_axesXTop;
/**
* The left y axes of the chart.
* <p>
* The first element is always existing and is the downward compatible result
* of the call <code>{@link Chart2D#getAxisY()}</code>.
* <p>
*/
private List<IAxis<?>> m_axesYLeft;
/**
* The right y axes of the chart.
* <p>
* If empty no right y axes are shown.
* <p>
*/
private List<IAxis<?>> m_axesYRight;
/** The internal label painter for this chart. */
private IAxisTickPainter m_axisTickPainter;
/** The grid color. */
private Color m_gridcolor = Color.lightGray;
/**
* Chart - wide setting for the ms to give a repaint operation time for
* collecting several repaint requests into one (performance versus update
* speed).
* <p>
*/
protected int m_minPaintLatency = 50;
/**
* The axis that is used for translation from mouse event to x value by method
* <code>Chart2D{@link #translateMousePosition(MouseEvent)}</code>.
* <p>
* Defaults to the first bottom x axis.
* <p>
*/
private AAxis<?> m_mouseTranslationXAxis;
/**
* The axis that is used for translation from mouse event to y value by method
* <code>Chart2D{@link #translateMousePosition(MouseEvent)}</code>.
* <p>
* Defaults to the first left y axis.
* <p>
*/
private AAxis<?> m_mouseTranslationYAxis;
/**
* When not null this format will be used within paint: then we deal with a
* printing request.
*/
private transient PageFormat m_pageFormat;
/**
* Flag that decides whether labels for traces are painted below the chart.
*/
private boolean m_paintLabels = true;
/**
* The point finder used to find the nearest point corresponding to a mouse
* event.
*/
private IPointFinder m_pointFinder = PointFinder.EUCLID;
/**
* Used to track mouse motion events and highlight the nearest trace point
* according to the point highlighters in the corresponding trace (
* <code>{@link ITrace2D#getPointHighlighters()}</code>).
* <p>
* Also removes highlighters (potentially if they are exclusive) from the
* previous highlighted point.
* <p>
*/
private final PointHighlighter m_pointHighlightListener = new PointHighlighter();
/**
* Tracks mouse motion events and highlights the nearest point in the trace.
* <p>
*
* @author <a href="mailto:achim.westermann@gmx.de">Achim Westermann</a>
*/
final class PointHighlighter extends MouseMotionAdapter implements MouseMotionListener,
PropertyChangeListener {
/**
* Default constructor, adds property change listener for point highlighters
* to the enclosing chart.
* <p>
*/
public PointHighlighter() {
/*
* Need an identity hash map as the trace keys will change upon points
* added thus making them "unfindable" in a map based on hashcode!
*/
this.m_previousHighlighted = new IdentityHashMap<ITrace2D, ITracePoint2D>();
Chart2D.this.addPropertyChangeListener(ITrace2D.PROPERTY_POINT_HIGHLIGHTERS_CHANGED, this);
Chart2D.this.addPropertyChangeListener(Chart2D.PROPERTY_ADD_REMOVE_TRACE, this);
}
/** Needed to de-highlight previously highlighted points. */
private Map<ITrace2D, ITracePoint2D> m_previousHighlighted;
/**
* Activates or deactivates point higlighting.
* <p>
* This handles tracking of mouse motion events of the chart.
* <p>
*
* @param onoff
* if true, highlighting will be enabled, if false it will be
* deactivated.
*
* @return true if a change took place.
*/
@SuppressWarnings("synthetic-access")
public boolean setActive(boolean onoff) {
boolean result = false;
boolean isEnabled = Chart2D.this.isEnabledPointHighlighting();
if (!onoff) {
if (isEnabled) {
synchronized (Chart2D.this) {
// deactivate all previously highlighted traces:
for (Map.Entry<ITrace2D, ITracePoint2D> entry : this.m_previousHighlighted.entrySet()) {
synchronized (entry.getKey()) {
Set<IPointPainter< ? >> highlighters = entry.getKey().getPointHighlighters();
Set<IPointPainter< ? >> additionalPainters = entry.getValue()
.getAdditionalPointPainters();
Iterator<IPointPainter< ? >> itAdditionasPainters = additionalPainters.iterator();
while (itAdditionasPainters.hasNext()) {
IPointPainter< ? > assignedHighlighter = itAdditionasPainters.next();
for (IPointPainter< ? > highlighter : highlighters) {
// cannot check for equality as outside configured same
// additional point painters not working for highlighting
// could be erased.
if (assignedHighlighter == highlighter) {
itAdditionasPainters.remove();
break;
}
}
}
}
}
Chart2D.this.removeMouseMotionListener(this);
Chart2D.this.firePropertyChange(PROPERTY_POINT_HIGHLIGHTING_ENABLED, Boolean.TRUE,
Boolean.FALSE);
this.m_previousHighlighted.clear();
Chart2D.this.setRequestedRepaint(true);
result = true;
}
}
} else {
if (!isEnabled) {
synchronized (Chart2D.this) {
Chart2D.this.addMouseMotionListener(this);
Chart2D.this.firePropertyChange(PROPERTY_POINT_HIGHLIGHTING_ENABLED, Boolean.FALSE,
Boolean.TRUE);
Chart2D.this.setRequestedRepaint(true);
result = true;
}
}
}
return result;
}
/**
* Attaches the highlighters of the trace of the point to the point: it will
* be highlighted then.
* <p>
*
* @param point
* the point to highlight.
*/
private void attachHighlighters(ITracePoint2D point) {
ITrace2D trace = point.getListener();
for (IPointPainter< ? > highlighter : trace.getPointHighlighters()) {
point.addAdditionalPointPainter(highlighter);
}
}
/**
* Removes the exclusive point highlighters from the old highlighted point.
*/
private void clearOutdatedHighlighters(final ITrace2D trace) {
ITracePoint2D previousHighlightedPoint = this.m_previousHighlighted.remove(trace);
if (previousHighlightedPoint != null) {
Iterator<IPointPainter< ? >> itAdditionaPainters = previousHighlightedPoint
.getAdditionalPointPainters().iterator();
IPointPainter< ? > additionalPainter;
Set<IPointPainter< ? >> highlighters = trace.getPointHighlighters();
if (Chart2D.DEBUG_HIGHLIGHTING) {
System.err.println("Trace " + trace.getName() + " has highlighters " + highlighters);
ITrace2D prevHiTrace = previousHighlightedPoint.getListener();
if (prevHiTrace != null) {
System.err.println("Previously highlighted trace: " + prevHiTrace.getName());
}
}
while (itAdditionaPainters.hasNext()) {
additionalPainter = itAdditionaPainters.next();
for (IPointPainter< ? > highlighter : highlighters) {
/*
* Cannot rely on "contains" method as comparable/equals might judge
* an equal highlighter added externally via api as a highlighter
*/
if (highlighter == additionalPainter) {
itAdditionaPainters.remove();
break;
} else {
if (Chart2D.DEBUG_HIGHLIGHTING) {
System.err.println("Additional painter " + additionalPainter + " and highlighter "
+ highlighter + " of trace " + trace + " judged not as same.");
}
}
}
}
} else {
if (Chart2D.DEBUG_HIGHLIGHTING) {
System.err.println("No previous point highlighted in trace " + trace.getName());
}
}
}
/**
* Attaches highlighters of the trace of the point next to the cursor and
* removes highlighters from the previous highlighted point.
* <p>
*
* TODO: Peek at getToolTipText invocations: Save operations in case the
* mouse has not moved much?
*
* @see java.awt.event.MouseMotionAdapter#mouseMoved(java.awt.event.MouseEvent)
*/
@Override
public void mouseMoved(MouseEvent e) {
ITracePoint2D point = Chart2D.this.getPointFinder().getNearestPoint(e, Chart2D.this);
// don't work on empty charts:
if (point != null) {
ITracePoint2D previousHighlightedPoint = this.m_previousHighlighted
.get(point.getListener());
if (!point.equals(previousHighlightedPoint)) {
ITrace2D trace = point.getListener();
// avoid duplicate or no highlighting in concurrent paint situation.
synchronized (this) {
synchronized (trace) {
this.clearOutdatedHighlighters(trace);
this.attachHighlighters(point);
this.m_previousHighlighted.put(trace, point);
Chart2D.this.setRequestedRepaint(true);
}
}
}
}
}
/**
* This is needed in case the point Highlighters of a trace (
* <code>{@link ITrace2D#setPointHighlighter(IPointPainter)}</code>,
* <code>{@link ITrace2D#addPointHighlighter(IPointPainter)}</code> or
* <code>{@link ITrace2D#removePointHighlighter(IPointPainter)}</code> or
* <code>{@link ITrace2D#removeAllPointHighlighters()}</code>) are changed
* in order to re-attach the proper point highlighters to the highlighted
* points.
* <p>
* Also a hook is installed for
* <code>{@link Chart2D#PROPERTY_ADD_REMOVE_TRACE}</code> to clear out a
* reference to a highlighted trace to make it garbage-collectable in case
* the trace is removed.
* <p>
*
* @see java.beans.PropertyChangeListener#propertyChange(java.beans.PropertyChangeEvent)
* @see ITrace2D#addPointHighlighter(IPointPainter)
* @see ITrace2D#setPointHighlighter(IPointPainter)
* @see ITrace2D#removePointHighlighter(IPointPainter)
* @see ITrace2D#removeAllPointHighlighters()
*/
public void propertyChange(PropertyChangeEvent evt) {
String property = evt.getPropertyName();
if (ITrace2D.PROPERTY_POINT_HIGHLIGHTERS_CHANGED.equals(property)) {
ITrace2D trace = (ITrace2D) evt.getSource();
ITracePoint2D point = this.m_previousHighlighted.get(trace);
if (point != null) {
if ((evt.getNewValue() != null) && (evt.getOldValue() == null)) {
if (this.m_previousHighlighted != null) {
synchronized (Chart2D.this) {
synchronized (trace) {
point.addAdditionalPointPainter((IPointPainter< ? >) evt.getNewValue());
Chart2D.this.setRequestedRepaint(true);
}
}
}
} else if ((evt.getNewValue() == null) && (evt.getOldValue() != null)) {
if (this.m_previousHighlighted != null) {
synchronized (Chart2D.this) {
synchronized (trace) {
point.removeAdditionalPointPainter((IPointPainter< ? >) evt.getOldValue());
Chart2D.this.setRequestedRepaint(true);
}
}
}
} else {
throw new RuntimeException(
"Programming error. Unneccessary event caught: "
+ evt
+ ". You only have to fire this event, if a point highlighter was addded or removed.");
}
}
} else if (Chart2D.PROPERTY_ADD_REMOVE_TRACE.equals(property)) {
ITrace2D oldTrace2d = (ITrace2D) evt.getOldValue();
if (evt.getNewValue() == null) {
// trace was removed, so remove my potential reference:
this.m_previousHighlighted.remove(oldTrace2d);
}
} else {
throw new RuntimeException("Programming error: " + this.getClass().getName()
+ " only has to be registered to the event "
+ ITrace2D.PROPERTY_POINT_HIGHLIGHTERS_CHANGED + " on instances of type (or subtype) "
+ ITrace2D.class.getName());
}
}
}
/**
* Internal timer for repaint control with guarantee that the interval between
* two frames will not be lower than <code>{@link Chart2D#m_minPaintLatency}
* </code> ms.
* <p>
*/
private Timer m_repainter;
/**
* Internal flag that stores a request for a repaint that guarantees that two
* invocations of <code></code> will always have at least have an interval of
* <code>{@link Chart2D#m_minPaintLatency}</code> ms.
* <p>
* Access to it has to be synchronized!
*/
private boolean m_requestedRepaint;
/**
* Flag to remember whether this chart has synchronized it's x start
* coordinates with another chart.
*/
private boolean m_synchronizedXStart = false;
/** A chart this chart will synchronize it's start coordinates in x dimension. */
private Chart2D m_synchronizedXStartChart;
/** Flag for showing coordinates as tool tips. */
private IToolTipType m_toolTip = ToolTipType.NONE;
/**
* Internal counter for all point highlighters of all traces to allow
* automatic enablement/disablement of highlighting (performance).
*/
private int m_traceHighlighterCount = 0;
/** Used to create trace point instances. */
private ITracePointProvider m_tracePointProvider;
/**
* Boolean flag to turn on antialiasing.
*/
private boolean m_useAntialiasing = false;
/**
* The end x pixel coordinate of the chart.
*/
private int m_xChartEnd;
/**
* The start x coordinate of the chart.
*/
private int m_xChartStart;
/**
* The y coordinate of the upper edge of the chart's display area in px.
* <p>
* The px coordinates in awt / swing start from top and increase towards the
* bottom.
* <p>
*/
private int m_yChartEnd;
/**
* The start y coordinate of the chart.
*/
private int m_yChartStart;
/**
* Creates a new chart.
* <p>
*/
public Chart2D() {
// initialize the axis collections:
this.m_axesXBottom = new LinkedList<IAxis<?>>();
this.m_axesXTop = new LinkedList<IAxis<?>>();
this.m_axesYLeft = new LinkedList<IAxis<?>>();
this.m_axesYRight = new LinkedList<IAxis<?>>();
this.setTracePointProvider(new TracePointProviderDefault());
AAxis<?> axisX = new AxisLinear<IAxisScalePolicy>();
this.setAxisXBottom(axisX, 0);
axisX.getAxisTitle().setTitle("X");
AAxis<?> axisY = new AxisLinear<IAxisScalePolicy>();
this.setAxisYLeft(axisY, 0);
axisY.getAxisTitle().setTitle("Y");
this.setAxisTickPainter(new AxisTickPainterDefault());
Font dflt = this.getFont();
if (dflt != null) {
this.setFont(new Font(dflt.getFontName(), dflt.getStyle(), 10));
}
this.getBackground();
this.setBackground(Color.white);
// turn off tool tips by default (performance):
this.setToolTipType(Chart2D.ToolTipType.NONE);
// one initial call to paint for side effect computations
// potentially needed from outside (m_XstartChart...):
this.setRequestedRepaint(true);
// set a custom cursor:
this.setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
this.m_repainter = new Timer(this.m_minPaintLatency, new ActionListener() {
/**
* Repaints the Chart if dirty.
* <p>
*
* @param e
* invoked by the timer to trigger the action.
*/
public void actionPerformed(final ActionEvent e) {
synchronized (Chart2D.this) {
if (Chart2D.this.isRequestedRepaint()) {
if (Chart2D.DEBUG_THREADING) {
System.out.println(Thread.currentThread().getName() + " triggering repaint()");
}
// Only here this deprecated call may be done:
Chart2D.this.repaint(Chart2D.this.m_minPaintLatency);
Chart2D.this.setRequestedRepaint(false);
}
}
}
});
Timer.setLogTimers(false);
this.m_repainter.setRepeats(true);
this.m_repainter.setCoalesce(true);
this.m_repainter.start();
}
/**
* Adds the given x axis to the list of internal bottom x axes.
* <p>
* The given axis must not be contained before (e.g. as right y axis or bottom
* x axis...).
* <p>
*
* @param axisX
* the additional bottom x axis.
*/
public void addAxisXBottom(final AAxis<?> axisX) {
this.ensureUniqueAxis(axisX);
this.m_axesXBottom.add(axisX);
axisX.setChart(this, Chart2D.X, Chart2D.CHART_POSITION_BOTTOM);
this.listenToAxis(axisX);
this.firePropertyChange(Chart2D.PROPERTY_AXIS_X, null, axisX);
this.setRequestedRepaint(true);
}
/**
* Adds the given x axis to the list of internal top x axes.
* <p>
* The given axis must not be contained before (e.g. as right y axis or bottom
* x axis...).
* <p>
*
* @param axisX
* the additional top x axis.
*/
public void addAxisXTop(final AAxis<?> axisX) {
this.ensureUniqueAxis(axisX);
this.m_axesXTop.add(axisX);
axisX.setChart(this, Chart2D.X, Chart2D.CHART_POSITION_TOP);
this.listenToAxis(axisX);
this.firePropertyChange(Chart2D.PROPERTY_AXIS_X, null, axisX);
this.setRequestedRepaint(true);
}
/**
* Adds the given y axis to the list of internal left y axes.
* <p>
* The given axis must not be contained before (e.g. as right y axis or bottom
* x axis...).
* <p>
*
* @param axisY
* the additional left y axis.
*/
public void addAxisYLeft(final AAxis<?> axisY) {
this.ensureUniqueAxis(axisY);
this.m_axesYLeft.add(axisY);
axisY.setChart(this, Chart2D.Y, Chart2D.CHART_POSITION_LEFT);
this.listenToAxis(axisY);
this.firePropertyChange(Chart2D.PROPERTY_AXIS_Y, null, axisY);
this.setRequestedRepaint(true);
}
/**
* Adds the given y axis to the list of internal right y axes.
* <p>
* The given axis must not be contained before (e.g. as right y axis or bottom
* x axis...).
* <p>
*
* @param axisY
* the additional right y axis.
*/
public void addAxisYRight(final AAxis<?> axisY) {
this.ensureUniqueAxis(axisY);
this.m_axesYRight.add(axisY);
axisY.setChart(this, Chart2D.Y, Chart2D.CHART_POSITION_RIGHT);
this.listenToAxis(axisY);
this.firePropertyChange(Chart2D.PROPERTY_AXIS_Y, null, axisY);
this.setRequestedRepaint(true);
}
/**
* Convenience method that adds the trace to this chart with relation to the
* first bottom x axis and the first left y axis. It will be painted (if it's
* {@link ITrace2D#isVisible()} returns true) in this chart.
* <p>
* This method will trigger a {@link java.beans.PropertyChangeEvent} being
* fired on all instances registered by
* {@link javax.swing.JComponent#addPropertyChangeListener(java.lang.String, java.beans.PropertyChangeListener)}
* (registered with <code>String</code> argument
* {@link IAxis#PROPERTY_ADD_REMOVE_TRACE}) on the internal bottom x axis and
* left y axis.
* <p>
*
* @param points
* the trace to add.
* @see IAxis#PROPERTY_ADD_REMOVE_TRACE
* @see Chart2D#addTrace(ITrace2D, IAxis, IAxis)
*/
public final void addTrace(final ITrace2D points) {
IAxis<?> xAxis = this.m_axesXBottom.get(0);
IAxis<?> yAxis = this.m_axesYLeft.get(0);
this.addTrace(points, xAxis, yAxis);
}
/**
* Adds the trace to this chart with relation to the given x axis and y axis.
* It will be painted (if it's {@link ITrace2D#isVisible()} returns true) in
* this chart.
* <p>
* This method will trigger a {@link java.beans.PropertyChangeEvent} being
* fired on all instances registered by
* {@link javax.swing.JComponent#addPropertyChangeListener(java.lang.String, java.beans.PropertyChangeListener)}
* (registered with <code>String</code> argument
* {@link IAxis#PROPERTY_ADD_REMOVE_TRACE}) on the axis of this chart.
* <p>
* The given x and y axis will be responsible for computation of the scale of
* this trace.
* <p>
*
* @param points
* the trace to add.
* @param xAxis
* the x axis responsible for the scale of this trace - it has to be
* contained in this chart or an exception will be thrown.
* @param yAxis
* the y axis responsible for the scale of this trace - it has to be
* contained in this chart or an exception will be thrown.
*
* @see IAxis#PROPERTY_ADD_REMOVE_TRACE
*/
public final void addTrace(final ITrace2D points, final IAxis<?> xAxis, final IAxis<?> yAxis) {
if (!this.m_axesXBottom.contains(xAxis)) {
if (!this.m_axesXTop.contains(xAxis)) {
throw new IllegalArgumentException(
"Given x axis ("
+ xAxis.getAxisTitle().getTitle()
+ ") has to be added to this chart first (via setAxisX(AAxis) or addAxisXBottom(AAXis) or addAxisXTop(AAXis)).");
}
}
if (!this.m_axesYLeft.contains(yAxis)) {
if (!this.m_axesYRight.contains(yAxis)) {
throw new IllegalArgumentException(
"Given y axis ("
+ yAxis.getAxisTitle().getTitle()
+ ") has to be added to this chart first (via setAxisY(AAxis) or addAxisYLeft(AAXis) or addAxisYRight(AAXis)).");
}
}
/*
*
* This lock is needed to ensure a lock on the tree is acquired before a
* lock to the chart. Method paint will also first acquire the treeLock and
* then has to get the lock on the chart while this code path might descend
* into ChartPanel where Container.getComponents() (in method
* containsTraceLabel()) will require the tree lock after having already
* acquired the chart lock -> deadlock between paint thread and addTrace
* thread.
*/
if (Chart2D.DEBUG_THREADING) {
System.out.println(Thread.currentThread().getName()
+ ", addTrace(ITrace2D, XAxis, YAxis): 0 locks.");
}
synchronized (this.getTreeLock()) {
/*
* synchronization necessary to avoid that a highlighter is added to the
* trace while counting them.
*/
if (Chart2D.DEBUG_THREADING) {
System.out.println(Thread.currentThread().getName()
+ ", addTrace(ITrace2D, XAxis, YAxis): 1 locks.");
}
synchronized (this) {
if (Chart2D.DEBUG_THREADING) {
System.out.println(Thread.currentThread().getName()
+ ", addTrace(ITrace2D, XAxis, YAxis): 2 locks.");
}
synchronized (points) {
if (Chart2D.DEBUG_THREADING) {
System.out.println(Thread.currentThread().getName()
+ ", addTrace(ITrace2D, XAxis, YAxis): 3 locks.");
}
boolean success = false;
success |= xAxis.addTrace(points);
success |= yAxis.addTrace(points);
if (success) {
this.listenToTrace(points);
int amountOfHighlighters = points.getPointHighlighters().size();
this.trackHighlightingEnablement(amountOfHighlighters);
this.firePropertyChange(IAxis.PROPERTY_ADD_REMOVE_TRACE, null, points);
}
}
if (Chart2D.DEBUG_THREADING) {
System.out.println(Thread.currentThread().getName()
+ ", addTrace(ITrace2D, XAxis, YAxis): dropped 1 lock, 2 locks remaining.");
}
}
if (Chart2D.DEBUG_THREADING) {
System.out.println(Thread.currentThread().getName()
+ ", addTrace(ITrace2D, XAxis, YAxis): dropped 1 lock, 1 locks remaining.");
}
}
if (Chart2D.DEBUG_THREADING) {
System.out.println(Thread.currentThread().getName()
+ ", addTrace(ITrace2D, XAxis, YAxis): dropped 1 lock, 0 locks remaining.");
}
}
/**
* Calculates the end x coordinate (right bound) in pixel of the chart to
* draw.
* <p>
* As a side effect the {@link IAxis#setPixelXLeft(int)} is set here.
* <p>
*
* This value depends on the current <code>{@link FontMetrics}</code> used to
* paint the x labels and the maximum amount of characters that are used for
* the x labels (<code>{@link IAxisLabelFormatter#getMaxAmountChars()}</code>)
* because an x label may occur on the right edge of the chart and should not
* be clipped.
* <p>
*
* @param g2d
* needed for size informations.
* @return the end x coordinate (right bound) in pixel of the chart to draw.
*/
private int calculateXChartEnd(final Graphics g2d) {
int result;
result = (int) this.getSize().getWidth();
IAxis<?> currentAxis;
int axisWidth = 0;
if (this.m_axesYRight.size() > 0) {
ListIterator<IAxis<?>> it = this.m_axesYRight.listIterator(this.m_axesYRight.size());
while (it.hasPrevious()) {
currentAxis = it.previous();
axisWidth = currentAxis.getWidth(g2d);
currentAxis.setPixelXRight(result);
if (currentAxis.isVisible()) {
result = result - axisWidth;
}
currentAxis.setPixelXLeft(result);
}
if (result == this.getSize().getWidth()) {
// ensure a minimum offset for when no axes are present
result -= 20;
}
} else {
// use the maximum label width of the x axes to avoid x labels
// being clipped in case there are no right y axes:
Iterator<IAxis<?>> it = this.m_axesXBottom.iterator();
int xAxesMaxLabelWidth = 0;
int tmp;
while (it.hasNext()) {
currentAxis = it.next();
tmp = currentAxis.getWidth(g2d);
if (tmp > xAxesMaxLabelWidth) {
xAxesMaxLabelWidth = tmp;
}
}
it = this.m_axesXTop.iterator();
while (it.hasNext()) {
currentAxis = it.next();
tmp = currentAxis.getWidth(g2d);
if (tmp > xAxesMaxLabelWidth) {
xAxesMaxLabelWidth = tmp;
}
}
result = result - xAxesMaxLabelWidth;
}
return result;
}
/**
* Calculates the start x coordinate (left bound) in pixel of the chart to
* draw.
* <p>
* As a side effect the {@link IAxis#setPixelXRight(int)} is set here.
* <p>
* This value depends on the current <code>{@link FontMetrics}</code> used to
* paint the y labels and the maximum amount of characters that are used for
* the y labels (<code>{@link IAxisLabelFormatter#getMaxAmountChars()}</code>
* ).
* <p>
*
* @param g2d
* needed for size information.
*
* @return the start x coordinate (left bound) in pixel of the chart to draw.
*/
private int calculateXChartStart(final Graphics g2d) {
int result = 0;
// reverse iteration because the most left axes are the latter ones:
ListIterator<IAxis<?>> it = this.m_axesYLeft.listIterator(this.m_axesYLeft.size());
IAxis<?> currentAxis;
while (it.hasPrevious()) {
currentAxis = it.previous();
currentAxis.setPixelXLeft(result);
if (currentAxis.isVisible()) {
result += currentAxis.getWidth(g2d);
}
currentAxis.setPixelXRight(result);
}
// ensure a minimum offset for e.g. when no Y axes are visible
return result > 0 ? result : 20;
}
/**
* Installs the offset the the left y-axes in case this chart
* is stacked / synchronized vertically via {@link #setSynchronizedXStartChart(Chart2D)}.<p>
*
* This is only necessary for the charts whose left y axes should start more to the right than if they were
* all on their own and didn't have to care for other charts.
* <p>
*
* @param g2d
* needed for size information.
*
* @return the start x coordinate (left bound) in pixel of the chart to draw.
*/
private int installXAxisLeftOffset(final Graphics g2d, final int offset) {
int result = offset;
// reverse iteration because the most left axes are the latter ones:
ListIterator<IAxis<?>> it = this.m_axesYLeft.listIterator(this.m_axesYLeft.size());
IAxis<?> currentAxis;
while (it.hasPrevious()) {
currentAxis = it.previous();
currentAxis.setPixelXLeft(result);
if (currentAxis.isVisible()) {
result += currentAxis.getWidth(g2d);
}
currentAxis.setPixelXRight(result);
}
// ensure a minimum offset for e.g. when no Y axes are visible
return result > 0 ? result : 20;
}
/**
* Calculates the end y coordinate (upper bound) in pixel of the chart to
* draw.
* <p>
* Note that y coordinates are related to the top of a frame, so a higher y
* value marks a visual lower chart value.
* <p>
* As a side effect the {@link IAxis#setPixelYBottom(int)} is set here.
* <p>
* The value computed here is the maximum overhang of all y axes caused by
* their font height of their labels or the summation of all top x axis
* heights (if this is greater) .
* <p>
*
* @param g2d
* needed for size informations.
* @return the end y coordinate (upper bound) in pixel of the chart to draw.
*/
private int calculateYChartEnd(final Graphics g2d) {
IAxis<?> currentAxis;
int tmp;
int result = 0;
int maxAxisYHeight = 0;
int axesXTopHeight = 0;
/*
* 1) Find the max y axis height (this is just the overhang cause by label.
*
* TODO: maybe this is too much work in case all fonts of all axes are the
* same and every axis will give the same result for the height (debug).
*/
Iterator<IAxis<?>> it = this.m_axesYLeft.iterator();
while (it.hasNext()) {
currentAxis = it.next();
tmp = currentAxis.getHeight(g2d);
if (currentAxis.isVisible() && tmp > maxAxisYHeight) {
maxAxisYHeight = tmp;
}
}
it = this.m_axesYRight.iterator();
while (it.hasNext()) {
currentAxis = it.next();
tmp = currentAxis.getHeight(g2d);
if (currentAxis.isVisible() && tmp > maxAxisYHeight) {
maxAxisYHeight = tmp;
}
}
// 2) Find the total height of top x axes
// (and calculate the y pixel of the top axes):
ListIterator<IAxis<?>> listIt = this.m_axesXTop.listIterator(this.m_axesXTop.size());
int axisHeight = 0;
while (listIt.hasPrevious()) {
currentAxis = listIt.previous();
currentAxis.setPixelYTop(axesXTopHeight);
axisHeight = currentAxis.getHeight(g2d);
if (currentAxis.isVisible()) {
axesXTopHeight += axisHeight;
}
currentAxis.setPixelYBottom(axesXTopHeight);
}
// 3) Find the maximum result:
result = Math.max(maxAxisYHeight, axesXTopHeight);
// ensure minimum offset when no axes are visible
return result > 0 ? result : 20;
}
/**
* Calculates the start y coordinate (lower bound) in pixel of the chart to
* draw.
* <p>
* As a side effect the {@link IAxis#setPixelYBottom(int)} is set here.
* <p>
* Note that y coordinates are related to the top of a frame, so a higher y
* value marks a visual lower chart value.
* <p>
*
* @param g2d
* needed for size informations.
* @param labelHeight
* the height of the labels below the chart.
* @return the start y coordinate (lower bound) in pixel of the chart to draw.
*/
private int calculateYChartStart(final Graphics g2d, final int labelHeight) {
int result;
result = (int) this.getSize().getHeight();
result = result - labelHeight;
IAxis<?> currentAxis;
int axesXBottomHeight = 0;
Iterator<IAxis<?>> it = this.m_axesXBottom.iterator();
while (it.hasNext()) {
currentAxis = it.next();
currentAxis.setPixelYBottom(result);
if (currentAxis.isVisible()) {
result -= currentAxis.getHeight(g2d);
}
currentAxis.setPixelYTop(result);
}
result = result - axesXBottomHeight;
if (result == this.getSize().getHeight()) {
// ensure minimum offset when no axis are visible
result -= 20;
}
return result;
}
/**
* @see javax.swing.JComponent#createToolTip()
*/
@Override
public JToolTip createToolTip() {
/*
* If desired return here a HTMLToolTip that transforms the text given to
* setTipText into a View (with BasicHTML) and sets it as the
* putClientProperty BasicHtml.html of itself.
*/
JToolTip result = super.createToolTip();
return result;
}
/**
* Destroys the chart.
* <p>
* This method is only of interest if you have an application that dynamically
* adds and removes charts. So if you use the same Chart2D object(s) during
* the applications lifetime there is no need to use this method.
* <p>
*/
public void destroy() {
if (Chart2D.DEBUG_THREADING) {
System.out.println("destroy, 0 locks");
}
synchronized (this) {
if (Chart2D.DEBUG_THREADING) {
System.out.println("destroy, 1 lock");
}
this.m_axesXBottom.clear();
this.m_axesXBottom = null;
this.m_axesXTop.clear();
this.m_axesXTop = null;
this.m_axesYLeft.clear();
this.m_axesYLeft = null;
this.m_axesYRight.clear();
this.m_axesYRight = null;
// terminate the timer
this.m_repainter.stop();
}
}
/**
* Switches point highlighting on or off depending on the given argument.
* <p>
* Turning off this removes a <code>{@link MouseMotionListener}</code> and
* therefore avoids receiving a lot of mouse events which will result in
* potentially very expensive (many traces with many points) computations to
* find the nearest point corresponding to the mouse pointer. So dropping the
* point highlighting feature may result in much better real time performance.
* <p>
*
* Keep in mind that the view part of point highlighting is configured by
* configuring the point highlighters for your traces:
* <code>{@link ITrace2D#addPointHighlighter(IPointPainter)}</code>.
* <p>
*
* This method might trigger a {@link java.beans.PropertyChangeEvent} being
* fired on all instances registered by
* {@link javax.swing.JComponent#addPropertyChangeListener(java.lang.String, java.beans.PropertyChangeListener)}
* (registered with <code>String</code> argument
* {@link Chart2D#PROPERTY_POINT_HIGHLIGHTING_ENABLED}).
* <p>
*
* @see ITrace2D#addPointHighlighter(IPointPainter)
*
* @see Chart2D#isEnabledPointHighlighting()
*
* @param onoff
* if true the closest point to the cursor will be highlighted, if
* false you might gain performance by having this feature turned
* off!
*
* @return true if a change of this state did take place (you did not call
* this at least twice with the same argument).
*/
public boolean enablePointHighlighting(final boolean onoff) {
boolean result;
result = this.m_pointHighlightListener.setActive(onoff);
return result;
}
/**
* Ensures that the axis to add is not in duty in any axis function for this
* chart.
* <p>
*
* @param axisToAdd
* the axis to test.
*/
private void ensureUniqueAxis(final IAxis<?> axisToAdd) {
if (this.m_axesXBottom.contains(axisToAdd)) {
throw new IllegalArgumentException("Given axis (" + axisToAdd.getAxisTitle().getTitle()
+ " is already configured as bottom x axis!");
}
if (this.m_axesXTop.contains(axisToAdd)) {
throw new IllegalArgumentException("Given axis (" + axisToAdd.getAxisTitle().getTitle()
+ " is already configured as top x axis!");
}
if (this.m_axesYLeft.contains(axisToAdd)) {
throw new IllegalArgumentException("Given axis (" + axisToAdd.getAxisTitle().getTitle()
+ " is already configured as left y axis!");
}
if (this.m_axesYRight.contains(axisToAdd)) {
throw new IllegalArgumentException("Given axis (" + axisToAdd.getAxisTitle().getTitle()
+ " is already configured as right y axis!");
}
}
/**
* Cleanup when this instance is dropped.
* <p>
*
* @see java.lang.Object#finalize()
*
* @throws Throwable
* if a finalizer of a superclass fails.
*/
@Override
protected void finalize() throws Throwable {
super.finalize();
this.destroy();
}
/**
* Returns an array with the x (position 0) and the y axis (position 1) of the
* given trace if it is correctly set up. If the given trace is not set up
* correctly with this chart a missing axis in one dimension will be reflected
* in <code>null</code> on the corresponding position
* <p>
*
* @param trace
* the trace to find the axes of.
* @return an array with the x (position 0) and the y axis (position 1) of the
* given trace if it is correctly set up.
*/
public IAxis<?>[] findAxesOfTrace(final ITrace2D trace) {
IAxis<?>[] result = new IAxis[2];
// 1) find x axis:
IAxis<?> xAxis = null;
for (IAxis<?> axis : this.m_axesXBottom) {
if (axis.getTraces().contains(trace)) {
xAxis = axis;
break;
}
}
if (xAxis == null) {
for (IAxis<?> axis : this.m_axesXTop) {
if (axis.getTraces().contains(trace)) {
xAxis = axis;
break;
}
}
}
// 2) find y axis:
IAxis<?> yAxis = null;
for (IAxis<?> axis : this.m_axesYLeft) {
if (axis.getTraces().contains(trace)) {
yAxis = axis;
break;
}
}
if (yAxis == null) {
for (IAxis<?> axis : this.m_axesYRight) {
if (axis.getTraces().contains(trace)) {
yAxis = axis;
break;
}
}
}
result[0] = xAxis;
result[1] = yAxis;
return result;
}
/**
* Returns the <code>{@link List}<{@link IAxis}></code> with all axes of the chart.
* <p>
*
* @return the <code>{@link List}<{@link IAxis}></code> with all axes of
* the chart
*/
public final List<IAxis<?>> getAxes() {
List<IAxis<?>> result = new LinkedList<IAxis<?>>();
result.addAll(this.getAxesXBottom());
result.addAll(this.getAxesXTop());
result.addAll(this.getAxesYLeft());
result.addAll(this.getAxesYRight());
return result;
}
/**
* Returns the <code>{@link List}<{@link IAxis}></code> with instances that are
* painted in x dimension on the bottom of the chart.
* <p>
* <b>Caution!</b> The original list is returned so modifications of it will
* cause unpredictable side effects.
* <p>
*
* @return the <code>{@link List}<{@link IAxis}></code> with instances
* that are painted in x dimension on the bottom of the chart.
*/
public final List<IAxis<?>> getAxesXBottom() {
return this.m_axesXBottom;
}
/**
* Returns the <code>{@link List}<{@link IAxis}></code> with instances that are
* painted in x dimension on top of the chart.
* <p>
* <b>Caution!</b> The original list is returned so modifications of it will
* cause unpredictable side effects.
* <p>
*
* @return the <code>{@link List}<{@link IAxis}></code> with instances
* that are painted in x dimension on top of the chart.
*/
public final List<IAxis<?>> getAxesXTop() {
return this.m_axesXTop;
}
/**
* Returns the <code>{@link List}<{@link IAxis}></code> with instances that are
* painted in y dimension on the left side of the chart.
* <p>
* <b>Caution!</b> The original list is returned so modifications of it will
* cause unpredictable side effects.
* <p>
*
* @return the <code>{@link List}<{@link IAxis}></code> with instances
* that are painted in y dimension on the left side of the chart.
*/
public final List<IAxis<?>> getAxesYLeft() {
return this.m_axesYLeft;
}
/**
* Returns the <code>{@link List}<{@link IAxis}></code> with instances that are
* painted in y dimension on the right side of the chart.
* <p>
* <b>Caution!</b> The original list is returned so modifications of it will
* cause unpredictable side effects.
* <p>
*
* @return the <code>{@link List}<{@link IAxis}></code> with instances
* that are painted in y dimension on the right side of the chart.
*/
public final List<IAxis<?>> getAxesYRight() {
return this.m_axesYRight;
}
/**
* Returns the painter for the ticks of the axis.
* <p>
*
* @return Returns the painter for the ticks of the axis.
*/
public IAxisTickPainter getAxisTickPainter() {
return this.m_axisTickPainter;
}
/**
* Returns the first bottom axis for the x dimension.
* <p>
*
* @return the first bottom axis for the x dimension.
*/
public final IAxis<?> getAxisX() {
return this.m_axesXBottom.get(0);
}
/**
* Returns the x axis that the given trace belongs to or null if this trace
* does not belong to any x axis of this chart.
* <p>
*
* @param trace
* the trace to find the corresponding x axis of this chart for.
* @return the x axis that the given trace belongs to or null if this trace
* does not belong to any x axis of this chart.
*/
public IAxis<?> getAxisX(final ITrace2D trace) {
IAxis<? > result = null;
IAxis<? > current = null;
Iterator<IAxis<?>> it = this.m_axesXBottom.iterator();
while (it.hasNext()) {
current = it.next();
if (current.hasTrace(trace)) {
result = current;
break;
}
}
if (result == null) {
it = this.m_axesXTop.iterator();
while (it.hasNext()) {
current = it.next();
if (current.hasTrace(trace)) {
result = current;
break;
}
}
}
return result;
}
/**
* Returns the first left axis for the y dimension.
* <p>
*
* @return the first left axis for the y dimension.
*/
public final IAxis<?> getAxisY() {
return this.m_axesYLeft.get(0);
}
/**
* Returns the y axis that the given trace belongs to or null if this trace
* does not belong to any y axis of this chart.
* <p>
*
* @param trace
* the trace to find the corresponding y axis of this chart for.
* @return the y axis that the given trace belongs to or null if this trace
* does not belong to any y axis of this chart.
*/
public IAxis<?> getAxisY(final ITrace2D trace) {
IAxis<?> result = null;
IAxis<?> current = null;
Iterator<IAxis<?>> it = this.m_axesYLeft.iterator();
while (it.hasNext()) {
current = it.next();
if (current.hasTrace(trace)) {
result = current;
break;
}
}
if (result == null) {
it = this.m_axesYRight.iterator();
while (it.hasNext()) {
current = it.next();
if (current.hasTrace(trace)) {
result = current;
break;
}
}
}
return result;
}
/**
* Returns the color of the grid.
* <p>
*
* @return the color of the grid.
*/
public final Color getGridColor() {
return this.m_gridcolor;
}
/**
* @see javax.swing.JComponent#getHeight()
*/
@Override
public int getHeight() {
int result = -1;
if (this.m_pageFormat != null) {
Chart2DActionPrintSingleton printTrigger = Chart2DActionPrintSingleton.getInstance(this);
if (printTrigger != null) {
if (printTrigger.isPrintWholePage()) {
int dpiScreen = Toolkit.getDefaultToolkit().getScreenResolution();
result = (int) this.m_pageFormat.getImageableHeight() * 72 / dpiScreen;
}
}
}
if (result == -1) {
result = super.getHeight();
}
return result;
}
/**
* Returns the chart - wide setting for the ms to give a repaint operation
* time for collecting several repaint requests into one (performance vs.
* update speed).
* <p>
*
* @return the setting for the ms to give a repaint operation time for
* collecting several repaint requests into one (performance vs.
* update speed).
*/
public synchronized int getMinPaintLatency() {
return this.m_minPaintLatency;
}
/**
* Returns the nearest <code>{@link ITracePoint2D}</code> to the given mouse
* event's screen coordinates in Euclid distance.
* <p>
* This method is expensive and should not be used when rendering fast
* changing charts with many points.
* <p>
* Using the Manhattan distance is much faster than Euclid distance as it only
* includes basic addition an absolute value for computation per point (vs.
* square root, addition and quadrature for Euclid distance). However the
* euclid distance spans a circle for the nearest points which is visually
* more normal for end users than the Manhattan distance which forms a rhombus
* and reaches far distances in only one dimension.
* <p>
*
* @param mouseEventX
* the x pixel value relative to the chart (e.g.: <code>
* {@link MouseEvent#getY()}</code>).
*
* @param mouseEventY
* the y pixel value relative to the chart (e.g.: <code>
* {@link MouseEvent#getY()}</code>).
*
* @return the nearest <code>{@link ITracePoint2D}</code> to the given mouse
* event's screen coordinates.
*
* @TODO: This is called twice per mouse move (tool tip and highlighter):
* Cache value throughout a paint iteration (delete at end)
*/
public ITracePoint2D getNearestPointEuclid(final int mouseEventX, final int mouseEventY) {
ITracePoint2D result = null;
/*
* Normalize pixel values:
*/
double scaledX = 0;
double scaledY = 0;
double rangeX = this.getXChartEnd() - this.getXChartStart();
if (rangeX != 0) {
scaledX = ((double) mouseEventX - this.getXChartStart()) / rangeX;
}
double rangeY = this.getYChartStart() - this.getYChartEnd();
if (rangeY != 0) {
scaledY = 1.0 - ((double) mouseEventY - this.getYChartEnd()) / rangeY;
}
/*
* TODO: Maybe cache this call because it searches all axes and evicts
* duplicates of their assigned traces (subject to profiling).
*/
Set<ITrace2D> traces = this.getTraces();
DistancePoint distanceBean;
DistancePoint winner = null;
for (ITrace2D trace : traces) {
distanceBean = trace.getNearestPointEuclid(scaledX, scaledY);
if (winner == null) {
winner = distanceBean;
} else {
if (distanceBean.getDistance() < winner.getDistance()) {
winner = distanceBean;
}
}
}
if (winner != null) {
result = winner.getPoint();
}
return result;
}
/**
* Returns the nearest <code>{@link MouseEvent}</code> to the given mouse
* event's screen coordinates in Euclid distance.
* <p>
* This method is expensive and should not be used when rendering fast
* changing charts with many points.
* <p>
* Note that the given mouse event should be an event fired on this chart
* component. Else results will point to the nearest point of the chart in the
* direction of the mouse event's position.
* <p>
* Using the Manhattan distance is much faster than Euclid distance as it only
* includes basic addition an absolute value for computation per point (vs.
* square root, addition and quadrature for Euclid distance). However the
* euclid distance spans a circle for the nearest points which is visually
* more normal for end users than the Manhattan distance which forms a rhombus
* and reaches far distances in only one dimension.
* <p>
*
* @param me
* a mouse event fired on this component.
* @return nearest <code>{@link MouseEvent}</code> to the given mouse event's
* screen coordinates or <code>null</code> if the chart is empty.
*/
public ITracePoint2D getNearestPointEuclid(final MouseEvent me) {
return this.getNearestPointEuclid(me.getX(), me.getY());
}
/**
* Returns the nearest <code>{@link ITracePoint2D}</code> to the given mouse
* event's screen coordinates in Manhattan distance.
* <p>
* This method is expensive and should not be used when rendering fast
* changing charts with many points.
* <p>
*
* Using the Manhattan distance is much faster than Euclid distance as it only
* includes basic addition an absolute value for computation per point (vs.
* square root, addition and quadrature for Euclid distance). However the
* euclid distance spans a circle for the nearest points which is visually
* more normal for end users than the Manhattan distance which forms a rhombus
* and reaches far distances in only one dimension.
* <p>
*
* @param mouseEventX
* the x pixel value relative to the chart (e.g.: <code>
* {@link MouseEvent#getY()}</code>).
* @param mouseEventY
* the y pixel value relative to the chart (e.g.: <code>
* {@link MouseEvent#getY()}</code>).
*
* @return the nearest <code>{@link ITracePoint2D}</code> to the given mouse
* event's screen coordinates.
*
* @TODO: This is called twice per mouse move (tool tip and highlighter):
* Cache value throughout a paint iteration (delete at end)
*/
public ITracePoint2D getNearestPointManhattan(final int mouseEventX, final int mouseEventY) {
ITracePoint2D result = null;
/*
* Normalize pixel values:
*/
double scaledX = 0;
double scaledY = 0;
double rangeX = this.getXChartEnd() - this.getXChartStart();
if (rangeX != 0) {
scaledX = ((double) mouseEventX - this.getXChartStart()) / rangeX;
}
double rangeY = this.getYChartStart() - this.getYChartEnd();
if (rangeY != 0) {
scaledY = 1.0 - ((double) mouseEventY - this.getYChartEnd()) / rangeY;
}
/*
* TODO: Maybe cache this call because it searches all axes and evicts
* duplicates of their assigned traces (subject to profiling).
*/
Set<ITrace2D> traces = this.getTraces();
DistancePoint distanceBean;
DistancePoint winner = null;
for (ITrace2D trace : traces) {
distanceBean = trace.getNearestPointManhattan(scaledX, scaledY);
if (winner == null) {
winner = distanceBean;
} else {
if (distanceBean.getDistance() < winner.getDistance()) {
winner = distanceBean;
}
}
}
if (winner != null) {
result = winner.getPoint();
}
return result;
}
/**
* Returns the nearest <code>{@link MouseEvent}</code> to the given mouse
* event's screen coordinates in Manhattan distance.
* <p>
* This method is expensive and should not be used when rendering fast
* changing charts with many points.
* <p>
* Note that the given mouse event should be an event fired on this chart
* component. Else results will point to the nearest point of the chart in the
* direction of the mouse event's position.
* <p>
* Using the Manhattan distance is much faster than Euclid distance as it only
* includes basic addition an absolute value for computation per point (vs.
* square root, addition and quadrature for Euclid distance). However the
* euclid distance spans a circle for the nearest points which is visually
* more normal for end users than the Manhattan distance which forms a rhombus
* and reaches far distances in only one dimension.
* <p>
*
* @param me
* a mouse event fired on this component.
* @return nearest <code>{@link MouseEvent}</code> to the given mouse event's
* screen coordinates or <code>null</code> if the chart is empty.
*/
public ITracePoint2D getNearestPointManhattan(final MouseEvent me) {
return this.getNearestPointManhattan(me.getX(), me.getY());
}
/**
* Returns the point finder used to find the nearest point corresponding to a
* mouse event.
* <p>
*
* @return the point finder used to find the nearest point corresponding to a
* mouse event.
*/
public IPointFinder getPointFinder() {
return this.m_pointFinder;
}
/**
* @see javax.swing.JComponent#getPreferredSize()
*/
@Override
public Dimension getPreferredSize() {
// TODO Auto-generated method stub
return super.getPreferredSize();
}
/**
* Overridden to allow full - page printing.
* <p>
*
* @see java.awt.Component#getSize()
*/
@Override
public Dimension getSize() {
return new Dimension(this.getWidth(), this.getHeight());
}
/**
* Returns the chart that will be synchronized for finding the start
* coordinate of this chart to draw in x dimension (<code>
* {@link #getXChartStart()}</code>
* ).
* <p>
* This feature is used to allow two separate charts to be painted stacked in
* y dimension (one below the other) that have different x start coordinates
* (because of different y labels that shift that value) with an equal
* starting x value (thus be comparable visually if their x values match).
* <p>
*
* @return the chart that will be synchronized for finding the start
* coordinate of this chart to draw in x dimension (<code>
* {@link #getXChartStart()}</code>).
*/
public synchronized final Chart2D getSynchronizedXStartChart() {
return this.m_synchronizedXStartChart;
}
/**
* @see javax.swing.JComponent#getToolTipText(java.awt.event.MouseEvent)
*/
@Override
public final String getToolTipText(final MouseEvent event) {
String result;
result = this.m_toolTip.getToolTipText(this, event);
if (result == null) {
result = super.getToolTipText(event);
}
/*
* Point highlighting is managed subsequently in
* ToolTipType.VALUE_SNAP_TO_TRACEPOINTS! This is done for 2 reasons: 1.
* Don't compute the point related to a mouse event twice. 2. Don't allow
* highlighted points with a tool tip that shows the data of the coordinates
* (vs. the data of the point highlighted) which might be misleading!
*/
return result;
}
/**
* Returns the type of tool tip shown.
* <p>
*
* @see Chart2D.ToolTipType#DATAVALUES
* @see Chart2D.ToolTipType#NONE
* @see Chart2D.ToolTipType#PIXEL
* @see Chart2D.ToolTipType#VALUE_SNAP_TO_TRACEPOINTS
*
* @return the type of tool tip shown.
*/
public IToolTipType getToolTipType() {
return this.m_toolTip;
}
/**
* Returns the trace point creator of this chart.
* <p>
*
* @return the trace point creator of this chart.
*/
public ITracePointProvider getTracePointProvider() {
return this.m_tracePointProvider;
}
/**
* Returns the set of traces that are currently rendered by this instance.
* <p>
* The instances are collected from all underlying axes. The resulting <code>
* {@link Set}</code>
* is not an original set. Therefore modification methods like
* <code>{@link Set#add(Object)}</code> or <code>{@link Set#clear()}</code>
* will not have any effect on the setup of this chart.
* <p>
*
* @return the set of traces that are currently rendered by this instance.
*/
public final SortedSet<ITrace2D> getTraces() {
SortedSet<ITrace2D> result = new TreeSet<ITrace2D>();
// 1.1) axes x bottom:
Iterator<IAxis<?>> it = this.m_axesXBottom.iterator();
IAxis<?> currentAxis;
Set<ITrace2D> axisTraces;
while (it.hasNext()) {
currentAxis = it.next();
axisTraces = currentAxis.getTraces();
// addAll not feasible: assumes currentAxis.getTraces() is sorted and
// order is lost?
for (ITrace2D trace : axisTraces) {
result.add(trace);
}
// result.addAll(currentAxis.getTraces());
}
// 1.2) axes x top:
it = this.m_axesXTop.iterator();
while (it.hasNext()) {
currentAxis = it.next();
axisTraces = currentAxis.getTraces();
for (ITrace2D trace : axisTraces) {
result.add(trace);
}
// result.addAll(currentAxis.getTraces());
}
// We skip y axes as by contract every
// trace has to be at least in one x axis
// (not logical if trace is e.g. in y axes
// only
// 2.1) axes y left:
// 2.2) axes y right:
return result;
}
/**
* @see javax.swing.JComponent#getWidth()
*/
@Override
public int getWidth() {
int result = -1;
if (this.m_pageFormat != null) {
Chart2DActionPrintSingleton printTrigger = Chart2DActionPrintSingleton.getInstance(this);
if (printTrigger != null) {
if (printTrigger.isPrintWholePage()) {
int dpiScreen = Toolkit.getDefaultToolkit().getScreenResolution();
result = (int) this.m_pageFormat.getImageableWidth() * 72 / dpiScreen;
}
}
}
if (result == -1) {
result = super.getWidth();
}
return result;
}
/**
* Returns the width of the X axis in px.
* <p>
*
* @return Returns the width of the X axis in px.
*
*/
public final synchronized int getXAxisWidth() {
return this.m_xChartEnd - this.m_xChartStart;
}
/**
* Returns the x coordinate of the chart's right edge in px.
* <p>
*
* @return the x coordinate of the chart's right edge in px.
*/
public final synchronized int getXChartEnd() {
return this.m_xChartEnd;
}
/**
* Returns the x coordinate of the chart's left edge in px.
* <p>
*
* @return Returns the x coordinate of the chart's left edge in px.
*/
public final synchronized int getXChartStart() {
return this.m_xChartStart;
}
/**
* Returns the y coordinate of the upper edge of the chart's display area in
* px.
* <p>
* Pixel coordinates in awt / swing start from top and increase towards the
* bottom.
* <p>
*
* @return The y coordinate of the upper edge of the chart's display area in
* px.
*/
public final synchronized int getYChartEnd() {
return this.m_yChartEnd;
}
/**
* Returns the y coordinate of the chart's lower edge in px.
* <p>
* Pixel coordinates in awt / swing start from top and increase towards the
* bottom.
* <p>
*
* @return Returns the y coordinate of the chart's lower edge in px.
*/
public synchronized int getYChartStart() {
return this.m_yChartStart;
}
/**
* Returns true if the connection of both interpolated points would cross the
* visible area.
* <p>
*
* Caution this method is only intended for two points that were invisible and
* have been interpolated to a bound of the chart to avoid drawing horizontal
* or vertical lines at the bound. In case two points that were not
* interpolated really have a vertical or horizontal connection the result
* would not be quite correct in terms of painting: Their line would not be
* drawn even if it would be correct.
* <p>
*
* Also this method assumes that both points were interpolated towards a bound
* of the chart via a connection to a previous or following point (line) which
* means that two points interpolated to the maximum and the minimum (x or y)
* do not have to be tested for intersections with the viewport rectangle.
* <p>
*
* @param oldpoint
* interpolated point.
*
* @param newpoint
* interpolated point following on oldpoint
*
* @return true if the connection of both points
*/
private boolean hasChartIntersection(ITracePoint2D oldpoint, ITracePoint2D newpoint) {
boolean result = true;
// if we needed a more generic (slower) solution we had to test:
// if x1 >= xmax && x2 >= xmax
// Caution: getX() and getY() will always return 0.0 by now as we don't
// backtrace values from interpolated points!
result = (oldpoint.getScaledX() != newpoint.getScaledX() || oldpoint.getScaledY() != newpoint
.getScaledY());
return result;
}
/**
* Internally transfers the state of the old axis to the new one.
* <p>
*
* This includes things as traces, paint grid state, title, etc..
* <p>
*
* <h4>Contract</h4> The new axis already has to be added to a chart to avoid
* a NPE when adding the traces of the old one!
* <p>
*
* Note that listening / unlistening to the axes is handled from above as this
* is triggered for example by {@link #setAxisXBottom(AAxis, int)} which also
* delegates to {@link #removeAxisXBottom(IAxis)} and
* {@link #addAxisXBottom(AAxis)}.
* <p>
*
* @param old
* the old axis (being removed).
*
* @param axisNew
* the replacing new axis.
*/
private void internalTransferAxisState(IAxis<?> old, AAxis<?> axisNew) {
// 1. Traces:
Set<ITrace2D> traces = old.removeAllTraces();
/*
* add the traces: this has to be after adding axis to avoid npe in addTrace
* as no chart is set!
*/
for (ITrace2D trace : traces) {
axisNew.addTrace(trace);
}
// 2. Title:
IAxis.AxisTitle title = old.removeAxisTitle();
axisNew.setAxisTitle(title);
// 3. Formatter:
IAxisLabelFormatter formatter = old.getFormatter();
axisNew.setFormatter(formatter);
// 4. paint grid flag:
boolean isPaintGrid = old.isPaintGrid();
axisNew.setPaintGrid(isPaintGrid);
// 5. paint scale flag:
boolean isPaintScale = old.isPaintScale();
axisNew.setPaintScale(isPaintScale);
// 6. start major tick:
boolean startMajorTick = old.isStartMajorTick();
axisNew.setStartMajorTick(startMajorTick);
// 7. visibility:
boolean visible = old.isVisible();
axisNew.setVisible(visible);
// 8. Range policy:
IRangePolicy rangePolicy = old.getRangePolicy();
axisNew.setRangePolicy(rangePolicy);
// 9. Range:
Range range = old.getRange();
axisNew.setRange(range);
// done for now...
}
/**
* Interpolates (linear) the two neighboring points.
* <p>
* Calling this method only makes sense if argument invisible is not null or
* if argument visible is not null (if then invisible is null, the visible
* point will be returned).
* <p>
* Visibility is determined only by their internally normalized coordinates
* that are within [0.0,1.0] for visible points.
* <p>
*
* @param visible
* the visible point.
* @param invisible
* the invisible point.
* @return the interpolation towards the exceeded bound.
*/
private ITracePoint2D interpolateVisible(final ITracePoint2D invisible,
final ITracePoint2D visible) {
ITracePoint2D result;
/*
* In the first call invisible is null because it is the previous point
* (there was no previous point: just return the new point):
*/
if (invisible == null) {
result = visible;
} else {
/*
* Interpolation is done by the two point form: (y - y1)/(x - x1) = (y2 -
* y1)/(x2 - x1) solved to the missing value.
*/
// interpolate
double xInterpolate = Double.NaN;
double yInterpolate = Double.NaN;
// find the bounds that has been exceeded:
// It is possible that two bound have been exceeded,
// then only one interpolation will be valid:
boolean interpolated = false;
boolean interpolatedWrong = false;
if (invisible.getScaledX() > 1.0) {
// right x bound
xInterpolate = 1.0;
yInterpolate = (visible.getScaledY() - invisible.getScaledY())
/ (visible.getScaledX() - invisible.getScaledX()) * (1.0 - invisible.getScaledX())
+ invisible.getScaledY();
interpolated = true;
interpolatedWrong = Double.isNaN(yInterpolate) || yInterpolate < 0.0 || yInterpolate > 1.0;
}
if ((invisible.getScaledX() < 0.0) && (!interpolated || interpolatedWrong)) {
// left x bound
xInterpolate = 0.0;
yInterpolate = (visible.getScaledY() - invisible.getScaledY())
/ (visible.getScaledX() - invisible.getScaledX()) * -invisible.getScaledX()
+ invisible.getScaledY();
interpolated = true;
interpolatedWrong = Double.isNaN(yInterpolate) || yInterpolate < 0.0 || yInterpolate > 1.0;
}
if ((invisible.getScaledY() > 1.0) && (!interpolated || interpolatedWrong)) {
// upper y bound, checked
yInterpolate = 1.0;
xInterpolate = (1.0 - invisible.getScaledY())
* (visible.getScaledX() - invisible.getScaledX())
/ (visible.getScaledY() - invisible.getScaledY()) + invisible.getScaledX();
interpolated = true;
interpolatedWrong = Double.isNaN(xInterpolate) || xInterpolate < 0.0 || xInterpolate > 1.0;
}
if ((invisible.getScaledY() < 0.0) && (!interpolated || interpolatedWrong)) {
// lower y bound
yInterpolate = 0.0;
xInterpolate = -invisible.getScaledY() * (visible.getScaledX() - invisible.getScaledX())
/ (visible.getScaledY() - invisible.getScaledY()) + invisible.getScaledX();
interpolated = true;
interpolatedWrong = Double.isNaN(xInterpolate) || xInterpolate < 0.0 || xInterpolate > 1.0;
}
if (interpolatedWrong) {
result = visible;
} else {
result = this.m_tracePointProvider.createTracePoint(0, 0);
// transfer potential point highlighters to the synthetic point:
for (IPointPainter< ? > highlighter : invisible.getAdditionalPointPainters()) {
result.addAdditionalPointPainter(highlighter);
}
result.setScaledX(xInterpolate);
result.setScaledY(yInterpolate);
result.setListener(invisible.getListener());
// TODO: do we have to compute and set the unscaled real values too?
}
}
return result;
}
/**
* Returns true if highlighting of the nearest point to the cursor is enabled.
* <p>
*
* @return true if highlighting of the nearest point to the cursor is enabled.
*/
public boolean isEnabledPointHighlighting() {
boolean isEnabled = false;
for (MouseMotionListener listener : this.getMouseMotionListeners()) {
if (listener == this.m_pointHighlightListener) {
isEnabled = true;
break;
}
}
return isEnabled;
}
/**
* Returns true if labels for each chart are painted below it, false else.
* <p>
*
* @return Returns if labels are painted.
*/
public final boolean isPaintLabels() {
return this.m_paintLabels;
}
/**
* Returns the requestedRepaint.
* <p>
*
* @return the requestedRepaint
*/
protected synchronized boolean isRequestedRepaint() {
return this.m_requestedRepaint;
}
/**
* Returns true if chart coordinates are drawn as tool tips.
* <p>
*
* @return true if chart coordinates are drawn as tool tips.
*/
public final boolean isToolTipCoords() {
return this.m_toolTip == Chart2D.ToolTipType.DATAVALUES;
}
/**
* Returns whether antialiasing is used.
* <p>
*
* @return whether antialiasing is used.
*/
public final boolean isUseAntialiasing() {
return this.m_useAntialiasing;
}
/**
* Returns true if the given point is in the visible drawing area of the
* Chart2D.
* <p>
* If the point is null false will be returned.
* <p>
* This only works if the point argument has been scaled already.
* <p>
*
* @param point
* the point to test.
* @return true if the given point is in the visible drawing area of the
* Chart2D.
*/
public boolean isVisible(final ITracePoint2D point) {
boolean result;
if (point == null) {
result = false;
} else {
result = !(Double.isNaN(point.getScaledX()) || Double.isNaN(point.getScaledY()))
&& !(point.getScaledX() > 1.0 || point.getScaledX() < 0.0 || point.getScaledY() > 1.0 || point
.getScaledY() < 0.0);
}
return result;
}
/**
* Returns an <code>Iterator</code> over the contained {@link ITrace2D}
* instances.
*
* @return an <code>Iterator</code> over the contained {@link ITrace2D}
* instances.
*/
public final Iterator<ITrace2D> iterator() {
return this.getTraces().iterator();
}
/**
* Helper that adds this chart as a listener to the required property change
* events.
* <p>
*
* @param axis
* the axis to listen to.
*/
private void listenToAxis(final IAxis<?> axis) {
axis.addPropertyChangeListener(IAxis.PROPERTY_ADD_REMOVE_TRACE, this);
axis.addPropertyChangeListener(IAxis.PROPERTY_LABELFORMATTER, this);
axis.addPropertyChangeListener(IAxis.PROPERTY_PAINTGRID, this);
axis.addPropertyChangeListener(IAxis.PROPERTY_RANGEPOLICY, this);
axis.addPropertyChangeListener(IAxis.PROPERTY_AXIS_SCALE_POLICY_CHANGED, this);
}
/**
* Helper that adds this chart as a listener to the required property change
* events.
* <p>
*
* @param trace
* the trace to listen to.
*/
private void listenToTrace(final ITrace2D trace) {
// for tracking removal/addition of point highlighters visually:
trace.addPropertyChangeListener(ITrace2D.PROPERTY_POINT_HIGHLIGHTERS_CHANGED,
this.m_pointHighlightListener);
// for tracking enablement/disablement of point highlighting feature
// (expensive mouse listener)
trace.addPropertyChangeListener(ITrace2D.PROPERTY_POINT_HIGHLIGHTERS_CHANGED, this);
}
/**
* Internally sets the value of <code>{@link #getXChartStart()}</code> and
* <code>{@link #getXChartEnd()}</code> with respect to another chart
* synchronized to the same value.
* <p>
*
* @param g2d
* needed for size information.
* @see #calculateXChartStart(Graphics)
* @see #calculateXChartEnd(Graphics)
*/
private void negociateXChart(final Graphics g2d) {
if (this.m_synchronizedXStartChart != null) {
int myXChartStart = this.calculateXChartStart(g2d);
int otherXChartStart = this.m_synchronizedXStartChart
.calculateXChartStart(g2d);
int correctionShift = Math.abs(myXChartStart - otherXChartStart);
this.m_xChartStart = Math.max(this.calculateXChartStart(g2d), this.m_synchronizedXStartChart
.calculateXChartStart(g2d));
this.m_xChartEnd = Math.max(this.calculateXChartEnd(g2d), this.m_synchronizedXStartChart
.calculateXChartEnd(g2d));
synchronized (this.m_synchronizedXStartChart) {
this.m_synchronizedXStartChart.m_xChartStart = this.m_xChartStart;
this.m_synchronizedXStartChart.m_xChartEnd = this.m_xChartEnd;
/*
* Install correction shift to the one that would be naturally more to the left:
*/
if (myXChartStart > otherXChartStart) {
this.m_synchronizedXStartChart.installXAxisLeftOffset(g2d, correctionShift);
} else {
this.installXAxisLeftOffset(g2d, correctionShift);
}
}
} else {
if (!this.m_synchronizedXStart) {
this.m_xChartStart = this.calculateXChartStart(g2d);
this.m_xChartEnd = this.calculateXChartEnd(g2d);
}
}
}
/**
*
* This method is just overridden to ensure a lock on this instance and then a lock on
* <code>{@link Container#getTreeLock()}</code> just as it has to be done in that order in
* <code>{@link #addTrace(ITrace2D, IAxis, IAxis)}</code>.
* <p>
*
* More info: This lock is needed to ensure a lock on the tree is acquired
* before a lock to the chart. Method addTrace has a code path that might
* descend into ChartPanel where Container.getComponents() (in method
* containsTraceLabel()) will require the tree lock after having already
* acquired the chart lock -> deadlock between paint thread and addTrace
* thread when using ChartPanel.
* <p>
*
* @see javax.swing.JComponent#paint(java.awt.Graphics)
*/
@Override
public void paint(Graphics g) {
synchronized (this.getTreeLock()) {
super.paint(g);
}
}
/**
* A basic rule of a JComponent is: <br>
* <b>Never invoke this method directly. </b> <br>
* See the description of <code>
* {@link javax.swing.JComponent#paintComponent(java.awt.Graphics)}</code>
* for details.
* <p>
* If you do invoke this method you may encounter performance issues,
* flickering UI and even deadlocks.
* <p>
*
* @param g
* the graphics context to use.
*/
@Override
protected synchronized void paintComponent(final Graphics g) {
if (Chart2D.DEBUG_THREADING) {
System.out.println("paint, 1 lock");
}
super.paintComponent(g);
// printing ?
if (this.m_pageFormat != null) {
/*
* User (0,0) is typically outside the imageable area, so we must
* translate by the X and Y values in the PageFormat to avoid clipping
*/
Graphics2D g2d = (Graphics2D) g;
double startX = this.m_pageFormat.getImageableX();
double startY = this.m_pageFormat.getImageableY();
g2d.translate(startX, startY);
}
this.updateScaling(false);
// will be used in several iterations.
ITrace2D trace;
Iterator<ITrace2D> traceIt;
// painting trace labels
this.negociateXChart(g);
int labelHeight = this.paintTraceLabels(g);
// finding start point of coordinate System.
this.m_yChartStart = this.calculateYChartStart(g, labelHeight);
this.m_yChartEnd = this.calculateYChartEnd(g);
int rangex = this.m_xChartEnd - this.m_xChartStart;
int rangey = this.m_yChartStart - this.m_yChartEnd;
this.paintCoordinateSystem(g);
// paint Traces.
int tmpx = 0;
int oldtmpx;
int tmpy = 0;
int oldtmpy;
ITracePoint2D oldpoint = null;
ITracePoint2D newpoint = null;
ITracePoint2D tmppt = null;
traceIt = this.getTraces().iterator();
// Some operations (e.g. stroke) need Graphics2d
Graphics2D g2d = null;
Stroke backupStroke = null;
if (g instanceof Graphics2D) {
g2d = (Graphics2D) g;
backupStroke = g2d.getStroke();
if (this.isUseAntialiasing()) {
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_BICUBIC);
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
}
}
int count = 0;
Iterator<ITracePainter< ? >> itTracePainters;
Iterator<IErrorBarPolicy< ? >> itTraceErrorBarPolicies;
ITracePainter< ? > tracePainter;
IErrorBarPolicy< ? > errorBarPolicy;
while (traceIt.hasNext()) {
oldpoint = null;
newpoint = null;
count++;
trace = traceIt.next();
if (trace.isVisible()) {
synchronized (trace) {
if (Chart2D.DEBUG_THREADING) {
System.out.println("Chart2D.paintComponent(" + Thread.currentThread().getName()
+ "), 2 locks (lock on trace " + trace.getName() + ")");
}
boolean hasErrorBars = trace.getHasErrorBars();
if (g2d != null) {
g2d.setStroke(trace.getStroke());
}
g.setColor(trace.getColor());
Set<ITracePainter< ? >> tracePainters = trace.getTracePainters();
itTracePainters = tracePainters.iterator();
tracePainter = null;
while (itTracePainters.hasNext()) {
tracePainter = itTracePainters.next();
tracePainter.startPaintIteration(g);
}
if (hasErrorBars) {
errorBarPolicy = null;
Set<IErrorBarPolicy< ? >> errorBarPolicies = trace.getErrorBarPolicies();
itTraceErrorBarPolicies = errorBarPolicies.iterator();
while (itTraceErrorBarPolicies.hasNext()) {
errorBarPolicy = itTraceErrorBarPolicies.next();
errorBarPolicy.startPaintIteration(g);
}
}
Iterator<ITracePoint2D> pointIt = trace.iterator();
boolean newpointVisible = false;
boolean oldpointVisible = false;
while (pointIt.hasNext()) {
oldpoint = newpoint;
oldtmpx = tmpx;
oldtmpy = tmpy;
newpoint = pointIt.next();
newpointVisible = this.isVisible(newpoint);
oldpointVisible = this.isVisible(oldpoint);
/*
* Special case: if we have NaN just don't interpolate anything or
* paint but just continue (and give a signal to trace painters to
* discontinue which is neccessary for implementations that paint
* polylines and must not accumulate polylines that have a
* discontinuation within):
*/
boolean isNaNNewpoint = Double.isNaN(newpoint.getX()) || Double.isNaN(newpoint.getY());
boolean isNanOldpoint;
if (oldpoint == null) {
isNanOldpoint = false;
} else {
isNanOldpoint = Double.isNaN(oldpoint.getX()) || Double.isNaN(oldpoint.getY());
}
if (isNaNNewpoint || isNanOldpoint) {
/*
* Only discontinue when entering NaN space as calls to it for
* subsequent NaN values would repeat the same polyline paint of
* the last valid point in TracePainterPolyline (senseless).
*/
if (!(isNanOldpoint) && (isNaNNewpoint)) {
for (ITracePainter< ? > painter : trace.getTracePainters()) {
painter.discontinue(g2d);
}
}
if (!isNaNNewpoint) {
tmpx = this.m_xChartStart + (int) Math.round(newpoint.getScaledX() * rangex);
tmpy = this.m_yChartStart - (int) Math.round(newpoint.getScaledY() * rangey);
}
} else if (!newpointVisible && !oldpointVisible) {
// save for next loop:
tmppt = (ITracePoint2D) newpoint.clone();
int tmptmpx = tmpx;
int tmptmpy = tmpy;
/*
* check if the interconnection of both invisible points cuts the
* visible area:
*/
oldpoint = this.interpolateVisible(oldpoint, newpoint);
newpoint = this.interpolateVisible(newpoint, oldpoint);
tmpx = this.m_xChartStart + (int) Math.round(newpoint.getScaledX() * rangex);
tmpy = this.m_yChartStart - (int) Math.round(newpoint.getScaledY() * rangey);
oldtmpx = this.m_xChartStart + (int) Math.round(oldpoint.getScaledX() * rangex);
oldtmpy = this.m_yChartStart - (int) Math.round(oldpoint.getScaledY() * rangey);
// only paint if intersection!
if (this.hasChartIntersection(oldpoint, newpoint)) {
// don't use error bars for interpolated points that do not
// intersect the chart's viewport!
this.paintPoint(oldtmpx, oldtmpy, tmpx, tmpy, true, trace, g, newpoint, false);
}
// restore for next loop start:
newpoint = tmppt;
tmpx = tmptmpx;
tmpy = tmptmpy;
} else if (newpointVisible && !oldpointVisible) {
// entering the visible bounds: interpolate from old point
// to new point
oldpoint = this.interpolateVisible(oldpoint, newpoint);
tmpx = this.m_xChartStart + (int) Math.round(newpoint.getScaledX() * rangex);
tmpy = this.m_yChartStart - (int) Math.round(newpoint.getScaledY() * rangey);
oldtmpx = this.m_xChartStart + (int) Math.round(oldpoint.getScaledX() * rangex);
oldtmpy = this.m_yChartStart - (int) Math.round(oldpoint.getScaledY() * rangey);
// don't use error bars for interpolated points!
this.paintPoint(oldtmpx, oldtmpy, tmpx, tmpy, true, trace, g, newpoint, false);
} else if (!newpointVisible && oldpointVisible) {
// leaving the visible bounds:
tmppt = (ITracePoint2D) newpoint.clone();
newpoint = this.interpolateVisible(newpoint, oldpoint);
tmpx = this.m_xChartStart + (int) Math.round(newpoint.getScaledX() * rangex);
tmpy = this.m_yChartStart - (int) Math.round(newpoint.getScaledY() * rangey);
// don't use error bars for interpolated points!
this.paintPoint(oldtmpx, oldtmpy, tmpx, tmpy, true, trace, g, newpoint, false);
// restore for next loop start:
newpoint = tmppt;
} else {
// staying in the visible bounds: just paint
tmpx = this.m_xChartStart + (int) Math.round(newpoint.getScaledX() * rangex);
tmpy = this.m_yChartStart - (int) Math.round(newpoint.getScaledY() * rangey);
this
.paintPoint(oldtmpx, oldtmpy, tmpx, tmpy, false, trace, g, newpoint, hasErrorBars);
}
}
itTracePainters = trace.getTracePainters().iterator();
while (itTracePainters.hasNext()) {
tracePainter = itTracePainters.next();
tracePainter.endPaintIteration(g);
}
if (hasErrorBars) {
itTraceErrorBarPolicies = trace.getErrorBarPolicies().iterator();
while (itTraceErrorBarPolicies.hasNext()) {
errorBarPolicy = itTraceErrorBarPolicies.next();
errorBarPolicy.endPaintIteration(g);
}
}
}
}
if (Chart2D.DEBUG_THREADING) {
System.out.println("paint(" + Thread.currentThread().getName() + "), left lock on trace "
+ trace.getName());
}
}
if (g2d != null) {
g2d.setStroke(backupStroke);
}
}
/**
* Paints the axis, the scales and the labels for the chart.
* <p>
* <b>Caution</b> This is highly coupled code and only factored out for better
* overview. This method may only be called by {@link #paint(Graphics)} and
* the order of this invocation there must not be changed.
* <p>
*
* @param g2d
* the graphics context to use.
*/
private void paintCoordinateSystem(final Graphics g2d) {
// drawing the axes:
g2d.setColor(this.getForeground());
IAxis<?> currentAxis;
// 1) x axes:
// 1.1) x axes bottom:
Iterator<IAxis<?>> it = this.m_axesXBottom.iterator();
while (it.hasNext()) {
currentAxis = it.next();
currentAxis.paint(g2d);
}
// 1.2) Top x axes:
it = this.m_axesXTop.iterator();
while (it.hasNext()) {
currentAxis = it.next();
currentAxis.paint(g2d);
}
// 2) y axes:
// 2.1) y axes left
it = this.m_axesYLeft.iterator();
while (it.hasNext()) {
currentAxis = it.next();
currentAxis.paint(g2d);
}
// 2.1) y axes right
it = this.m_axesYRight.iterator();
while (it.hasNext()) {
currentAxis = it.next();
currentAxis.paint(g2d);
}
}
/**
* Internally renders the error bars for the given point for the given trace.
* <p>
* The current point to render in px is defined by the first two arguments,
* the next point to render in px is defined by the 2nd two arguments.
* <p>
*
* @param trace
* needed to get the {@link IErrorBarPolicy} instances to use.
* @param oldtmpx
* the x coordinate of the original point to render an error bar for.
* @param oldtmpy
* the y coordinate of the original point to render an error bar for.
* @param tmpx
* the x coordinate of the original next point to render an error bar
* for.
* @param tmpy
* the y coordinate of the original next point to render an error bar
* for.
* @param g2d
* the graphics context to use.
* @param discontinue
* if a discontinuity has been taken place and all potential cached
* points by an <code>{@link ITracePainter}</code> (done for polyline
* performance boost) have to be drawn immediately before starting a
* new point caching.
* @param original
* intended for information only, should nor be needed to paint the
* point neither be changed in any way!
*/
private void paintErrorBars(final ITrace2D trace, final int oldtmpx, final int oldtmpy,
final int tmpx, final int tmpy, final Graphics g2d, final boolean discontinue,
final ITracePoint2D original) {
IErrorBarPolicy< ? > errorBarPolicy;
Iterator<IErrorBarPolicy< ? >> itTraceErrorBarPolicies = trace.getErrorBarPolicies().iterator();
while (itTraceErrorBarPolicies.hasNext()) {
errorBarPolicy = itTraceErrorBarPolicies.next();
errorBarPolicy.paintPoint(oldtmpx, oldtmpy, tmpx, tmpy, g2d, original);
if (discontinue) {
errorBarPolicy.discontinue(g2d);
}
}
}
/**
* Internally paints the point with respect to trace painters (
* {@link ITracePainter}) and error bar painter ({@link IErrorBarPolicy}) of
* the trace.
* <p>
* This method must not be called directly as it does not support
* interpolation of visibility bounds (discontinuations).
* <p>
*
* @param xPxOld
* the x coordinate of the previous point to render in px
* (potentially an interpolation of it if the old point was not
* visible and the new point is).
* @param yPxOld
* the y coordinate of the previous point to render in px
* (potentially an interpolation of it if the old point was not
* visible and the new point is).
* @param xPxNew
* the x coordinate of the point to render in px (potentially an
* interpolation of it if the old point was visible and the new point
* is not).
* @param yPxNew
* the y coordinate of the point to render in px (potentially an
* interpolation of it if the old point was visible and the new point
* is not).
* @param trace
* needed for obtaining trace painters and error bar painters.
* @param g2d
* the graphics context to use.
* @param discontinue
* if a discontinuation has been taken place and all potential cached
* points by an <code>{@link ITracePainter}</code> (done for polyline
* performance boost) have to be drawn immediately before starting a
* new point caching.
* @param original
* intended for information only, should nor be needed to paint the
* point neither be changed in any way!
* @param errorBarSupport
* optimization that allows to skip error bar code.
*/
private final void paintPoint(final int xPxOld, final int yPxOld, final int xPxNew,
final int yPxNew, final boolean discontinue, final ITrace2D trace, final Graphics g2d,
final ITracePoint2D original, final boolean errorBarSupport) {
Iterator<ITracePainter< ? >> itTracePainters;
ITracePainter< ? > tracePainter;
itTracePainters = trace.getTracePainters().iterator();
while (itTracePainters.hasNext()) {
tracePainter = itTracePainters.next();
tracePainter.paintPoint(xPxOld, yPxOld, xPxNew, yPxNew, g2d, original);
Set<IPointPainter< ? >> additionalHighlighters = original.getAdditionalPointPainters();
Iterator<IPointPainter< ? >> itPointHighlighters = additionalHighlighters.iterator();
IPointPainter< ? > highlighter;
while (itPointHighlighters.hasNext()) {
highlighter = itPointHighlighters.next();
highlighter.paintPoint(xPxNew, yPxNew, xPxNew, yPxNew, g2d, original);
}
if (discontinue) {
tracePainter.discontinue(g2d);
}
}
if (errorBarSupport) {
this.paintErrorBars(trace, xPxOld, yPxOld, xPxNew, yPxNew, g2d, discontinue, original);
}
}
/**
* Internally paints the labels for the traces below the chart.
* <p>
*
* @param g2d
* the graphic context to use.
* @return the amount of vertical (y) px used for the labels.
*/
private int paintTraceLabels(final Graphics g2d) {
int labelheight = 0;
Dimension d = this.getSize();
if (this.m_paintLabels) {
ITrace2D trace;
Iterator<ITrace2D> traceIt = this.getTraces().iterator();
int xtmpos = this.m_xChartStart;
int ytmpos = (int) d.getHeight() - 2;
int remwidth = (int) d.getWidth() - this.m_xChartStart;
int allwidth = remwidth;
int lblwidth = 0;
String tmplabel;
boolean crlfdone = false;
// finding the font- dimensions in px
FontMetrics fontdim = g2d.getFontMetrics();
// includes leading space
int fontheight = fontdim.getHeight();
if (traceIt.hasNext()) {
labelheight += fontheight;
}
while (traceIt.hasNext()) {
trace = traceIt.next();
if (trace.isVisible()) {
tmplabel = trace.getLabel();
if (!StringUtil.isEmpty(tmplabel)) {
lblwidth = fontdim.stringWidth(tmplabel) + 10;
// conditional linebreak.
// crlfdone avoids never doing linebreak if all
// labels.length()>allwidth
if (lblwidth > remwidth) {
if (!(lblwidth > allwidth) || (!crlfdone)) {
ytmpos -= fontheight;
xtmpos = this.m_xChartStart;
labelheight += fontheight;
crlfdone = true;
remwidth = (int) d.getWidth() - this.m_xChartStart;
} else {
crlfdone = false;
}
}
remwidth -= lblwidth;
g2d.setColor(trace.getColor());
g2d.drawString(tmplabel, xtmpos, ytmpos);
xtmpos += lblwidth;
}
}
}
}
return labelheight;
}
/**
* @see java.awt.print.Printable#print(java.awt.Graphics,
* java.awt.print.PageFormat, int)
*/
public int print(final Graphics graphics, final PageFormat pageFormat, final int pageIndex)
throws PrinterException {
int result;
if (pageIndex > 0) {
// We have only one page, and 'page' is zero-based.
result = NO_SUCH_PAGE;
} else {
// mark we are in a printing - paint iteration:
this.m_pageFormat = pageFormat;
this.updateScaling(true);
/* Now print the window and its visible contents */
this.printAll(graphics);
/* tell the caller that this page is part of the printed document */
result = PAGE_EXISTS;
}
return result;
}
/**
* Receives all <code>{@link PropertyChangeEvent}</code> from all instances
* the chart registers itself as a <code>{@link PropertyChangeListener}</code>
* .
* <p>
*
* @param evt
* the property change event that was fired.
* @see java.beans.PropertyChangeListener#propertyChange(java.beans.PropertyChangeEvent)
*/
public void propertyChange(final PropertyChangeEvent evt) {
if (Chart2D.DEBUG_THREADING) {
System.out
.println("chart.propertyChange (" + Thread.currentThread().getName() + "), 0 locks");
}
synchronized (this) {
if (Chart2D.DEBUG_THREADING) {
System.out.println("Chart2D.propertyChange, " + evt.getPropertyName() + " ("
+ Thread.currentThread().getName() + "), 1 lock");
}
// TODO: use the property change reactor idiom also used in AAxis for
// performance.
String property = evt.getPropertyName();
if (property.equals(IRangePolicy.PROPERTY_RANGE)) {
// repaint
} else if (property.equals(IRangePolicy.PROPERTY_RANGE_MAX)) {
// repaint
} else if (property.equals(IRangePolicy.PROPERTY_RANGE_MIN)) {
// repaint
} else if (property.equals(ITrace2D.PROPERTY_STROKE)) {
/*
* TODO: perhaps react more fine grained for the following events: just
* repaint the trace without all the paint code (scaling, axis,...).
* But: These property changes are triggered by humans and occur very
* seldom. Huge work non-l&f performance improvement.
*/
} else if (property.equals(ITrace2D.PROPERTY_COLOR)) {
// repaint
} else if (property.equals(IAxis.PROPERTY_LABELFORMATTER)) {
/*
* TODO: Maybe only repaint the axis? Much complicated work vs.
* occassional user interaction.
*/
} else if (property.equals(IAxis.PROPERTY_ADD_REMOVE_TRACE)) {
/*
* Relay the event as outsiders don't want to deal with internals
* (listen to axes to be informed whenever a trace was added). Also:
* repaint definetely!
*/
this.firePropertyChange(IAxis.PROPERTY_ADD_REMOVE_TRACE, evt.getOldValue(), evt
.getNewValue());
} else if (property.equals(ITrace2D.PROPERTY_POINT_HIGHLIGHTERS_CHANGED)) {
int highlightersAddedOrRemoved = 0;
if (evt.getOldValue() != null) {
highlightersAddedOrRemoved--;
}
if (evt.getNewValue() != null) {
highlightersAddedOrRemoved++;
}
this.trackHighlightingEnablement(highlightersAddedOrRemoved);
} else if (property.equals(IAxis.PROPERTY_AXIS_SCALE_POLICY_CHANGED)) {
// repaint
} else if (property.equals(IAxis.PROPERTY_PAINTGRID)) {
// repaint
} else if (property.equals(IAxis.PROPERTY_RANGEPOLICY)) {
// repaint
} else {
throw new IllegalStateException("Received a property change event \"" + property
+ "\" the code is not expecting (programming error).");
}
this.setRequestedRepaint(true);
}
if (Chart2D.DEBUG_THREADING) {
System.out.println("Chart2D.propertyChange, leaving (" + Thread.currentThread().getName()
+ "), 0 locks");
}
}
/**
* Convenience method to remove all traces from this chart.
* <p>
* This method is broken down to every axis contained in the trace and will
* fire a <code>{@link PropertyChangeEvent}</code> for the
* <code>{@link PropertyChangeEvent#getPropertyName()}</code>
* <code>{@link IAxis#PROPERTY_ADD_REMOVE_TRACE}</code> for every single trace
* removed to <code>{@link PropertyChangeListener}</code> of the corresponding
* axes.
* <p>
*
* @return a non-original-backed set of distinct traces that was contained in
* this chart before.
*/
public Set<ITrace2D> removeAllTraces() {
Set<ITrace2D> result = new TreeSet<ITrace2D>();
Set<ITrace2D> axisTraces;
// 1.1) axes x bottom:
Iterator<IAxis<?>> it = this.m_axesXBottom.iterator();
IAxis<?> currentAxis;
while (it.hasNext()) {
currentAxis = it.next();
axisTraces = currentAxis.removeAllTraces();
result.addAll(axisTraces);
}
// 1.2) axes x top:
it = this.m_axesXTop.iterator();
while (it.hasNext()) {
currentAxis = it.next();
axisTraces = currentAxis.removeAllTraces();
result.addAll(axisTraces);
axisTraces.clear();
}
/*
* We skip "result.addAll(...) for y axes as by contract every trace has to
* be at least in one x axis (not logical if trace is e.g. in y axes only).
*/
// 2.1) axes y left:
it = this.m_axesYLeft.iterator();
while (it.hasNext()) {
currentAxis = it.next();
axisTraces = currentAxis.removeAllTraces();
axisTraces.clear();
}
// 2.2) axes y right:
it = this.m_axesYRight.iterator();
while (it.hasNext()) {
currentAxis = it.next();
axisTraces = currentAxis.removeAllTraces();
axisTraces.clear();
}
return result;
}
/**
* Removes the given x axis from the list of internal bottom x axes.
* <p>
* The given axis should be contained before or false will be returned.
* <p>
*
* @param axisX
* the bottom x axis to remove.
* @return true if the given axis was successfully removed or false if it was
* not configured as a bottom x axis before or could not be removed
* for another reason.
*/
public boolean removeAxisXBottom(final IAxis<?> axisX) {
boolean result = this.m_axesXBottom.remove(axisX);
this.unlistenToAxis(axisX);
this.firePropertyChange(Chart2D.PROPERTY_AXIS_X, axisX, null);
this.setRequestedRepaint(true);
return result;
}
/**
* Removes the given x axis from the list of internal top x axes.
* <p>
* The given axis should be contained before or false will be returned.
* <p>
*
* @param axisX
* the top x axis to remove.
* @return true if the given axis was successfully removed or false if it was
* not configured as a top x axis before or could not be removed for
* another reason.
*/
public boolean removeAxisXTop(final IAxis<?> axisX) {
boolean result = this.m_axesXTop.remove(axisX);
this.unlistenToAxis(axisX);
this.firePropertyChange(Chart2D.PROPERTY_AXIS_X, axisX, null);
this.setRequestedRepaint(true);
return result;
}
/**
* Removes the given y axis from the list of internal left y axes.
* <p>
* The given axis should be contained before or false will be returned.
* <p>
*
* @param axisY
* the left y axis to remove.
* @return true if the given axis was successfully removed or false if it was
* not configured as a left y axis before or could not be removed for
* another reason.
*/
public boolean removeAxisYLeft(final IAxis<?> axisY) {
boolean result = this.m_axesYLeft.remove(axisY);
this.unlistenToAxis(axisY);
this.firePropertyChange(Chart2D.PROPERTY_AXIS_Y, axisY, null);
this.setRequestedRepaint(true);
return result;
}
/**
* Removes the given y axis from the list of internal right y axes.
* <p>
* The given axis should be contained before or false will be returned.
* <p>
*
* @param axisY
* the right y axis to remove.
* @return true if the given axis was successfully removed or false if it was
* not configured as a right y axis before or could not be removed for
* another reason.
*/
public boolean removeAxisYRight(final IAxis<?> axisY) {
boolean result = this.m_axesYRight.remove(axisY);
this.unlistenToAxis(axisY);
this.firePropertyChange(Chart2D.PROPERTY_AXIS_Y, axisY, null);
this.setRequestedRepaint(true);
return result;
}
/**
* Removes the given instance from this <code>Chart2D</code> if it is
* contained.
* <p>
* This method will trigger a {@link java.beans.PropertyChangeEvent} being
* fired on all instances registered by
* {@link javax.swing.JComponent#addPropertyChangeListener(java.lang.String, java.beans.PropertyChangeListener)}
* (registered with <code>String</code> argument
* {@link IAxis#PROPERTY_ADD_REMOVE_TRACE} on the internal axes).
* <p>
*
* @return true if the given trace was removed successfully, false else.
*
* @param points
* the trace to remove.
* @see IAxis#PROPERTY_ADD_REMOVE_TRACE
*/
public final boolean removeTrace(final ITrace2D points) {
boolean result = false;
if (Chart2D.DEBUG_THREADING) {
System.out.println("removeTrace, 0 locks");
}
synchronized (this) {
if (Chart2D.DEBUG_THREADING) {
System.out.println("removeTrace, 1 lock");
}
synchronized (points) {
// remove the trace from the axes it is potentially contained in:
// 1) x - axes:
Iterator<IAxis<?>> it = this.m_axesXBottom.iterator();
IAxis<?> currentAxis;
boolean successRemoveX = false;
while (it.hasNext()) {
currentAxis = it.next();
successRemoveX = currentAxis.removeTrace(points);
if (successRemoveX) {
break;
}
}
// was not found in bottom x axes:
if (!successRemoveX) {
it = this.m_axesXTop.iterator();
while (it.hasNext()) {
currentAxis = it.next();
successRemoveX = currentAxis.removeTrace(points);
if (successRemoveX) {
break;
}
}
}
// 2) y - axes:
boolean successRemoveY = false;
it = this.m_axesYLeft.iterator();
while (it.hasNext()) {
currentAxis = it.next();
successRemoveY = currentAxis.removeTrace(points);
}
// was not found in left y axes:
if (!successRemoveY) {
it = this.m_axesYRight.iterator();
while (it.hasNext()) {
currentAxis = it.next();
successRemoveY = currentAxis.removeTrace(points);
if (successRemoveY) {
break;
}
}
}
boolean success = successRemoveY && successRemoveX;
if (success ) {
int amountofremovedhighlighters = points.getPointHighlighters().size();
this.trackHighlightingEnablement(amountofremovedhighlighters);
this.unlistenToTrace(points);
this.setRequestedRepaint(true);
}
result = success;
return result;
}
}
}
/**
* @deprecated use {@link #setRequestedRepaint(boolean)}.
* @see java.awt.Component#repaint()
*/
@Override
@Deprecated
public void repaint() {
super.repaint();
}
/**
* @deprecated use {@link #setRequestedRepaint(boolean)}.
* @see java.awt.Component#repaint(int, int, int, int)
*/
@Override
@Deprecated
public void repaint(final int x, final int y, final int width, final int height) {
super.repaint(x, y, width, height);
}
/**
* @deprecated use {@link #setRequestedRepaint(boolean)}.
* @see java.awt.Component#repaint(long)
*/
@Override
@Deprecated
public void repaint(final long tm) {
super.repaint(tm);
}
/**
* @deprecated use {@link #setRequestedRepaint(boolean)}.
* @see javax.swing.JComponent#repaint(long, int, int, int, int)
*/
@Override
@Deprecated
public void repaint(final long tm, final int x, final int y, final int width, final int height) {
super.repaint(tm, x, y, width, height);
}
/**
* @deprecated use {@link #setRequestedRepaint(boolean)}.
* @see javax.swing.JComponent#repaint(java.awt.Rectangle)
*/
@Override
@Deprecated
public void repaint(final Rectangle r) {
super.repaint(r);
}
/**
* Only intended for <code>{@link Chart2DActionPrintSingleton}</code>.
* <p>
*/
public void resetPrintMode() {
this.m_pageFormat = null;
this.setRequestedRepaint(true);
}
/**
* Sets the axis tick painter.
* <p>
*
* @param tickPainter
* The axis tick painter to set.
*/
public synchronized void setAxisTickPainter(final IAxisTickPainter tickPainter) {
this.m_axisTickPainter = tickPainter;
}
/**
* Sets the first bottom x axis to use.
* <p>
* This is compatibility support for the API of jchart2d prior to 3.0.0 where
* only one x axis was supported.
* <p>
*
* @deprecated use <code>{@link #setAxisXBottom(AAxis, int)}</code> instead.
*
* @see #setAxisXBottom(AAxis, int)
*
* @param axisX
* the first bottom x axis to use.
*
* @return a copied List with the previous bottom x <code>{@link IAxis}</code>
* instance that was used at position 0.
*/
@Deprecated
public List<IAxis<?>> setAxisX(final AAxis<?> axisX) {
List<IAxis<?>> axesBottom = new LinkedList<IAxis<?>>();
for(IAxis<?> axis:this.m_axesXBottom) {
// don't call remove here (concurrent modification exception), just collect:
axesBottom.add(axis);
}
List<IAxis<?>> axesTop = new LinkedList<IAxis<?>>();
for(IAxis<?> axis:this.m_axesXTop) {
// don't call remove here (concurrent modification exception), just collect:
axesTop.add(axis);
}
// now remove
for(IAxis<?>axis:axesBottom) {
this.removeAxisXBottom(axis);
}
for(IAxis<?>axis:axesTop) {
this.removeAxisXTop(axis);
}
axesBottom.addAll(axesTop);
return axesBottom;
}
/**
* Sets the bottom x axis on the given position to use and replaces the old
* one on that place.
* <p>
* This method delegates to <code>{@link #addAxisXBottom(AAxis)}</code> and
* also uses <code>{@link #removeAxisXBottom(IAxis)}</code> in case a bottom x
* axis was configured at the position. So the events
* <code>{@link Chart2D#PROPERTY_AXIS_X}</code> will be fired for remove and
* add.
* <p>
*
* Furthermore this method uses "replace - semantics". The
* <code>{@link ITrace2D}</code> instances contained in the previous x bottom
* axis will be implanted to this new axis. Also the title and stuff like grid
* settings will be transferred. For this an event with property
* <code>{@link Chart2D#PROPERTY_AXIS_X_BOTTOM_REPLACE} </code> is fired.
* <p>
*
* <b>Note</b> that <code>{@link PropertyChangeListener}s</code> of the axis will not be
* transferred silently to the new axis but have to handle their unregistering
* from the old axis / registering to the new axis from outside to give them
* the chance to manage their state transitions by themselves.<br/>
* Before the state of the old axis is transferred they will receive
* <code>{@link PropertyChangeListener#propertyChange(PropertyChangeEvent)}</code>
* with <code>{@link Chart2D#PROPERTY_AXIS_X_BOTTOM_REPLACE}</code> as code
* and the old and new axis as values and have the change to change their peer
* to listen on thus receiving the change events generated on the new axis. At
* the moment the replace event is sent they will already have received the
* event <code>{@link Chart2D#PROPERTY_AXIS_X}</code> event for the removal:
* So be careful not to react on that first event by removing your listener
* from the axis as you then will not receive the replace event!
* <p>
*
* @param axisX
* the first bottom x axis to use.
*
* @param position
* the index of the axis on bottom x dimension (starting from 0).
*
* @return the previous axis on that bottom x position or null.
*/
public IAxis<?> setAxisXBottom(final AAxis<?> axisX, final int position) {
IAxis<?> old = null;
if (this.m_axesXBottom.size() > position) {
// this is a replace operation!
old = this.m_axesXBottom.get(position);
this.removeAxisXBottom(old);
this.firePropertyChange(PROPERTY_AXIS_X_BOTTOM_REPLACE, old, axisX);
}
// add anyways in case no axis has been set before (see constructor)
this.addAxisXBottom(axisX);
// we can only transfer state (which includes adding traces after the new
// axis is assigned to a chart!
if (old != null) {
this.internalTransferAxisState(old, axisX);
}
this.m_mouseTranslationXAxis = axisX;
this.setRequestedRepaint(true);
return old;
}
/**
* Sets the top x axis on the given position to use and replaces the old one
* on that place.
* <p>
* This method delegates to <code>{@link #addAxisXBottom(AAxis)}</code> and
* also uses <code>{@link #removeAxisXBottom(IAxis)}</code> in case a top x
* axis was configured on the position. So the events
* <code>{@link Chart2D#PROPERTY_AXIS_X}</code> will be fired for remove and
* add.
* <p>
*
* Furthermore this method uses "replace - semantics". The
* <code>{@link ITrace2D}</code> instances contained in the previous x top
* axis will be implanted to this new axis. Also the title and stuff like grid
* settings will be transferred. For this an event with property
* <code>{@link Chart2D#PROPERTY_AXIS_Y_LEFT_REPLACE} </code> is fired.
* <p>
*
* <b>Note</b> that <code>{@link PropertyChangeListener}s</code> of the axis will not be
* transferred silently to the new axis but have to handle their unregistering
* from the old axis / registering to the new axis from outside to give them
* the chance to manage their state transitions by themselves.<br/>
* Before the state of the old axis is transferred they will receive
* <code>{@link PropertyChangeListener#propertyChange(PropertyChangeEvent)}</code>
* with <code>{@link Chart2D#PROPERTY_AXIS_X_TOP_REPLACE}</code> as code and
* the old and new axis as values and have the change to change their peer to
* listen on thus receiving the change events generated on the new axis. At
* the moment the replace event is sent they will already have received the
* event <code>{@link Chart2D#PROPERTY_AXIS_X}</code> event for the removal:
* So be careful not to react on that first event by removing your listener
* from the axis as you then will not receive the replace event!
* <p>
*
* @param axisX
* the top x axis to use.
*
* @param position
* the index of the axis on top x dimension (starting from 0).
*
* @return the previous axis on that bottom x position.
*/
public IAxis<?> setAxisXTop(final AAxis<?> axisX, final int position) {
IAxis<?> old = null;
if (this.m_axesXTop.size() > position) {
// this is a replace operation!
old = this.m_axesXTop.get(position);
this.removeAxisXTop(old);
this.firePropertyChange(PROPERTY_AXIS_X_TOP_REPLACE, old, axisX);
}
// we can only transfer state (which includes adding traces after the new
// axis is assigned to a chart!
if (old != null) {
this.internalTransferAxisState(old, axisX);
}
// add anyways in case no axis has been set before (see constructor)
this.addAxisXTop(axisX);
this.m_mouseTranslationXAxis = axisX;
this.setRequestedRepaint(true);
return old;
}
/**
* Sets the first and only left y axis to use.
* <p>
* This is compatibility support for the API of jchart2d prior to 3.0.0 where
* only one y axis was supported.
* <p>
*
* @deprecated use <code>{@link #setAxisYLeft(AAxis, int)}</code> instead.
*
* @see #setAxisYLeft(AAxis, int)
*
* @param axisY
* the first left y axis to use.
*
* @return a copied List with the previous left y <code>{@link AAxis}</code>
* instance that was used at position 0.
*/
@Deprecated
public List<IAxis<?>> setAxisY(final AAxis<?> axisY) {
List<IAxis<?>> axesLeft = new LinkedList<IAxis<?>>();
for (IAxis<?> axis : this.m_axesYLeft) {
// don't call remove here (concurrent modification exception), just
// collect:
axesLeft.add(axis);
}
List<IAxis<?>> axesRight = new LinkedList<IAxis<?>>();
for (IAxis<?> axis : this.m_axesYRight) {
// don't call remove here (concurrent modification exception), just
// collect:
axesRight.add(axis);
}
// now remove
for (IAxis<?> axis : axesLeft) {
this.removeAxisYLeft(axis);
}
for (IAxis<?> axis : axesRight) {
this.removeAxisYRight(axis);
}
axesLeft.addAll(axesRight);
return axesLeft;
}
/**
* Sets the left y axis on the given position to use and replaces the old one
* on that place.
* <p>
* This method delegates to <code>{@link #addAxisYLeft(AAxis)}</code> and also
* uses <code>{@link #removeAxisYLeft(IAxis)}</code> in case an axis was
* configured on the position. So the events
* <code>{@link Chart2D#PROPERTY_AXIS_Y}</code> will be fired for remove and
* add.
* <p>
*
* Furthermore this method uses "replace - semantics". The
* <code>{@link ITrace2D}</code> instances contained in the previous left y
* axis will be implanted to this new axis. Also the title and stuff like grid
* settings will be transferred. For this an event with property
* <code>{@link Chart2D#PROPERTY_AXIS_Y_LEFT_REPLACE} </code> is fired.
* <p>
*
* <b>Note</b> that <code>{@link PropertyChangeListener}s</code> of the axis will not be
* transferred silently to the new axis but have to handle their unregistering
* from the old axis / registering to the new axis from outside to give them
* the chance to manage their state transitions by themselves.<br/>
* Before the state of the old axis is transferred they will receive
* <code>{@link PropertyChangeListener#propertyChange(PropertyChangeEvent)}</code>
* with <code>{@link Chart2D#PROPERTY_AXIS_Y_LEFT_REPLACE}</code> as code and
* the old and new axis as values and have the change to change their peer to
* listen on thus receiving the change events generated on the new axis. At
* the moment the replace event is sent they will already have received the
* event <code>{@link Chart2D#PROPERTY_AXIS_Y}</code> event for the removal:
* So be careful not to react on that first event by removing your listener
* from the axis as you then will not receive the replace event!
* <p>
*
* @param axisY
* the left y axis to use.
*
* @param position
* the index of the axis on left y dimension (starting from 0).
*
* @return the previous axis on that bottom x position.
*/
public IAxis<?> setAxisYLeft(final AAxis<?> axisY, final int position) {
IAxis<?> old = null;
if (this.m_axesYLeft.size() > position) {
// this is a replace operation!
old = this.m_axesYLeft.get(position);
this.removeAxisYLeft(old);
this.firePropertyChange(PROPERTY_AXIS_Y_LEFT_REPLACE, old, axisY);
}
/*
* We can only add after removal or else the position argument would get
* confused. Add anyways in case no axis has been set before (see
* constructor)
*/
this.addAxisYLeft(axisY);
/*
* We can only transfer state (which includes adding traces after the new
* axis is assigned to a chart!
*/
if (old != null) {
this.internalTransferAxisState(old, axisY);
}
this.m_mouseTranslationYAxis = axisY;
this.setRequestedRepaint(true);
return old;
}
/**
* Sets the right y axis on the given position to use and replaces the old one
* on that place.
* <p>
* This method delegates to <code>{@link #addAxisYRight(AAxis)}</code> and
* also uses <code>{@link #removeAxisYRight(IAxis)}</code> in case an axis was
* configured on the position. So the events
* <code>{@link Chart2D#PROPERTY_AXIS_Y}</code> will be fired for remove and
* add.
* <p>
*
* Furthermore this method uses "replace - semantics". The
* <code>{@link ITrace2D}</code> instances contained in the previous right y
* axis will be implanted to this new axis. Also the title and stuff like grid
* settings will be transferred. For this an event with property
* <code>{@link Chart2D#PROPERTY_AXIS_Y_RIGHT_REPLACE} </code> is fired.
* <p>
*
* <b>Note</b> that <code>{@link PropertyChangeListener}s</code> of the axis will not be
* transferred silently to the new axis but have to handle their unregistering
* from the old axis / registering to the new axis from outside to give them
* the chance to manage their state transitions by themselves.<br/>
* Before the state of the old axis is transferred they will receive
* <code>{@link PropertyChangeListener#propertyChange(PropertyChangeEvent)}</code>
* with <code>{@link Chart2D#PROPERTY_AXIS_Y_RIGHT_REPLACE}</code> as code and
* the old and new axis as values and have the change to change their peer to
* listen on thus receiving the change events generated on the new axis. At
* the moment the replace event is sent they will already have received the
* event <code>{@link Chart2D#PROPERTY_AXIS_Y}</code> event for the removal:
* So be careful not to react on that first event by removing your listener
* from the axis as you then will not receive the replace event!
* <p>
*
* @param axisY
* the right y axis to use.
*
* @param position
* the index of the axis on right y dimension (starting from 0).
*
* @return the previous axis on that bottom x position.
*/
public IAxis<?> setAxisYRight(final AAxis<?> axisY, final int position) {
IAxis<?> old = null;
if (this.m_axesYRight.size() > position) {
// this is a replace operation!
old = this.m_axesYRight.get(position);
this.removeAxisYLeft(old);
this.firePropertyChange(PROPERTY_AXIS_Y_RIGHT_REPLACE, old, axisY);
}
// add anyways in case no axis has been set before (see constructor)
this.addAxisYRight(axisY);
// we can only transfer state (which includes adding traces after the new
// axis is assigned to a chart!
if (old != null) {
this.internalTransferAxisState(old, axisY);
}
this.m_mouseTranslationYAxis = axisY;
this.setRequestedRepaint(true);
return old;
}
/**
* Set the grid color to use.
* <p>
*
* @param gridclr
* the grid color to use.
*/
public final void setGridColor(final Color gridclr) {
if (gridclr != null) {
Color old = this.m_gridcolor;
this.m_gridcolor = gridclr;
if (!old.equals(this.m_gridcolor)) {
this.firePropertyChange(Chart2D.PROPERTY_GRID_COLOR, old, this.m_gridcolor);
}
this.setRequestedRepaint(true);
}
}
/**
* Sets the ms to give a repaint operation time for collecting several repaint
* requests into one (performance vs. update speed).
* <p>
*
* @param minPaintLatency
* the setting for the ms to give a repaint operation time for
* collecting several repaint requests into one (performance vs.
* update speed).
*/
public synchronized void setMinPaintLatency(final int minPaintLatency) {
this.m_minPaintLatency = minPaintLatency;
this.m_repainter.setDelay(this.m_minPaintLatency);
}
/**
* Decide whether labels for each chart are painted below it. If set to true
* this will be done, else labels will be omitted.
* <p>
*
* @param paintLabels
* the value for paintLabels to set.
*/
public void setPaintLabels(final boolean paintLabels) {
final boolean change = this.m_paintLabels != paintLabels;
this.m_paintLabels = paintLabels;
if (change) {
this.firePropertyChange(Chart2D.PROPERTY_PAINTLABELS, new Boolean(!paintLabels), new Boolean(
paintLabels));
this.setRequestedRepaint(true);
}
}
/**
* Sets the point finder used to find the nearest point corresponding to a
* mouse event.
* <p>
*
* @see PointFinder#MANHATTAN
* @see PointFinder#EUCLID
*
* @param pointFinder
* the point finder used to find the nearest point corresponding to a
* mouse event.
*/
public void setPointFinder(final IPointFinder pointFinder) {
IPointFinder old = this.m_pointFinder;
if (!this.m_pointFinder.equals(pointFinder)) {
this.m_pointFinder = pointFinder;
this.firePropertyChange(PROPERTY_POINTFINDER, old, this.m_pointFinder);
}
}
/**
* Sets the requestedRepaint.
* <p>
* Internal method to request a repaint that guarantees that two invocations
* of <code></code> will always have at least have an interval of
* <code>{@link Chart2D#getMinPaintLatency()}</code> ms.
* <p>
* Methods <code>{@link Chart2D#repaint()}, {@link Chart2D#repaint(long)},
* {@link Chart2D#repaint(Rectangle)}, {@link Chart2D#repaint(int, int, int, int)}
* and {@link Chart2D#repaint(long, int, int, int, int)}</code> must not be
* called from application code that has to inform the UI to update the chart
* directly or a performance problem may arise as java awt / swing
* implementation does not guarantee to collapse several repaint requests into
* a single one but prefers to issue many paint invocations causing a high CPU
* load in realtime scenarios (adding several 100 points per second to a
* chart).
* <p>
* Only the internal timer may invoke the methods mentioned above.
* <p>
*
* @param requestedRepaint
* the requestedRepaint to set.
*/
public final synchronized void setRequestedRepaint(final boolean requestedRepaint) {
this.m_requestedRepaint = requestedRepaint;
}
/**
* Sets the chart that will be synchronized for finding the start coordinate
* of this chart to draw in x dimension ( <code>{@link #getXChartStart()}
* </code>).
* <p>
* This feature is used to allow two separate charts to be painted stacked in
* y dimension (one below the other) that have different x start coordinates
* (because of different y labels that shift that value) with an equal
* starting x value (thus be comparable visually if their x values match).
* <p>
*
* @param synchronizedXStartChart
* the chart that will be synchronized for finding the start
* coordinate of this chart to draw in x dimension (<code>
* {@link #getXChartStart()}</code>).
*/
public synchronized void setSynchronizedXStartChart(final Chart2D synchronizedXStartChart) {
this.m_synchronizedXStartChart = synchronizedXStartChart;
this.m_synchronizedXStart = false;
synchronized (synchronizedXStartChart) {
synchronizedXStartChart.m_synchronizedXStart = true;
}
}
/**
* Set whether this component should display the chart coordinates as a tool
* tip.
* <p>
* This turns on tool tip support (like
* {@link javax.swing.JComponent#setToolTipText(java.lang.String)}) if
* neccessary.
* <p>
*
* @deprecated use <code> {@link #setToolTipType(IToolTipType)} </code> with
* <code>{@link ToolTipType#DATAVALUES}</code> instead.
* @param toolTipCoords
* The toolTipCoords to set.
*/
@Deprecated
public final void setToolTipCoords(final boolean toolTipCoords) {
if (toolTipCoords) {
this.setToolTipType(Chart2D.ToolTipType.DATAVALUES);
} else {
this.setToolTipType(Chart2D.ToolTipType.NONE);
}
}
/**
* Sets the type of tool tip to use.
* <p>
* Use <code>{@link ToolTipType#NONE}</code> to turn of tool tips.
* <p>
*
* @param toolTipType
* one of the available <code>{@link ToolTipType}</code> constants.
*
* @see Chart2D.ToolTipType#DATAVALUES
* @see Chart2D.ToolTipType#NONE
* @see Chart2D.ToolTipType#PIXEL
* @see Chart2D.ToolTipType#VALUE_SNAP_TO_TRACEPOINTS
*/
public final void setToolTipType(final IToolTipType toolTipType) {
if (toolTipType == Chart2D.ToolTipType.NONE) {
// this is the hidden "unregister for tooltips trick".
this.setToolTipText(null);
} else {
// this turns on tooltips (awt).
this.setToolTipText("turnOn");
}
IToolTipType old = this.m_toolTip;
this.m_toolTip = toolTipType;
this.firePropertyChange(Chart2D.PROPERTY_TOOLTIP_TYPE, old, this.m_toolTip);
}
/**
* Sets the trace point creator of this chart.
* <p>
* Null assignment attempts will raise an <code>{@link AssertionError}</code>.
* <p>
*
* @param tracePointProvider
* the trace point creator of this chart to set.
*/
public void setTracePointProvider(final ITracePointProvider tracePointProvider) {
assert (tracePointProvider != null);
this.m_tracePointProvider = tracePointProvider;
}
/**
* Sets whether antialiasing is used.
* <p>
*
* @param useAntialiasing
* true if antialiasing should be used.
*/
public final void setUseAntialiasing(final boolean useAntialiasing) {
if (this.m_useAntialiasing != useAntialiasing) {
boolean oldstate = this.m_useAntialiasing;
this.m_useAntialiasing = useAntialiasing;
this.firePropertyChange(Chart2D.PROPERTY_ANTIALIASING_ENABLED, oldstate,
this.m_useAntialiasing);
}
}
/**
* Returns a BufferedImage with the current width and height of the chart
* filled with the Chart2D's graphics that may be written to a file or
* OutputStream by using:
* {@link javax.imageio.ImageIO#write(java.awt.image.RenderedImage, java.lang.String, java.io.File)}
* .
* <p>
* If the width and height of this chart is zero (this happens when the chart
* has not been {@link javax.swing.JComponent#setVisible(boolean)}, the chart
* was not integrated into layout correctly or the chart's dimenision was set
* to this value, a default of width 600 and height 400 will temporarily be
* set (syncrhonized), the image will be rendered, the old dimension will be
* reset and the image will be returned.<br/>
* If you want to paint offscreen images (without displayed chart) prefer
* invoke {@link #snapShot(int, int)} instead.
* <p>
*
* @return a BufferedImage of the Chart2D's graphics that may be written to a
* file or OutputStream.
* @since 1.03 - please download versions equal or greater than
* jchart2d-1.03.jar.
*/
public BufferedImage snapShot() {
int width = this.getWidth();
int height = this.getHeight();
if (width <= 0 && height <= 0) {
width = 600;
height = 400;
}
return this.snapShot(width, height);
}
/**
* Returns a BufferedImage with the given width and height that is filled with
* tChart2D's graphics that may be written to a file or OutputStream by using:
* {@link javax.imageio.ImageIO#write(java.awt.image.RenderedImage, java.lang.String, java.io.File)}
* .
* <p>
*
* @param width
* the width of the image to create.
* @param height
* the height of the image to create.
* @return a BufferedImage of the Chart2D's graphics that may be written to a
* file or OutputStream.
* @since 1.03 - please download versions equal or greater than
* jchart2d-1.03.jar.
*/
public BufferedImage snapShot(final int width, final int height) {
synchronized (this) {
Dimension dsave = new Dimension(this.getWidth(), this.getHeight());
this.setSize(new Dimension(width, height));
BufferedImage img;
img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics2D g2d = (Graphics2D) img.getGraphics();
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_BICUBIC);
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
this.paint(g2d);
this.setSize(dsave);
return img;
}
}
/**
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
String result = super.toString();
return result;
}
/**
* Internal helper to track the amount of highlighters of all traces to manage
* the enablement/disablement of the (expensive) highlighting feature.
* <p>
*
* Note that this method has to be called in synchronized state from
* addTrace/removeTrace methods.
* <p>
*
* @param addedOrRemovedTraceHighlighters
* the amount of added or removed (negative) trace highlighters.
*/
private void trackHighlightingEnablement(final int addedOrRemovedTraceHighlighters) {
if (this.m_traceHighlighterCount <= 0) {
if (addedOrRemovedTraceHighlighters > 0) {
this.enablePointHighlighting(true);
}
} else {
if ((addedOrRemovedTraceHighlighters < 0)
&& (Math.abs(addedOrRemovedTraceHighlighters) >= this.m_traceHighlighterCount)) {
this.enablePointHighlighting(false);
}
}
this.m_traceHighlighterCount += addedOrRemovedTraceHighlighters;
if (this.m_traceHighlighterCount < 0) {
System.err.println("Internal amount of point highlighters below zero: "
+ this.m_traceHighlighterCount);
}
}
/**
* Returns the translation of the mouse event coordinates of the given mouse
* event to the value within the chart.
* <p>
* Note that the mouse event has to be an event fired on this component!
* <p>
* Note that the returned tracepoint is not a real trace point of a trace but
* just used as a container here.
* <p>
*
* @deprecated this method is a candidate for wrong behavior when using
* multiple axes.
*
* @param mouseEvent
* a mouse event that has been fired on this component.
* @return the translation of the mouse event coordinates of the given mouse
* event to the value within the chart or null if no calculations
* could be performed as the chart was not painted before.
* @throws IllegalArgumentException
* if the given mouse event does not belong to this component.
*/
@Deprecated
public ITracePoint2D translateMousePosition(final MouseEvent mouseEvent)
throws IllegalArgumentException {
if (mouseEvent.getSource() != this) {
throw new IllegalArgumentException(
"The given mouse event does not belong to this chart but to: " + mouseEvent.getSource());
}
ITracePoint2D result = null;
double valueX = this.m_mouseTranslationXAxis.translateMousePosition(mouseEvent);
double valueY = this.m_mouseTranslationYAxis.translateMousePosition(mouseEvent);
result = this.m_tracePointProvider.createTracePoint(valueX, valueY);
return result;
}
/**
* Helper that removes this chart as a listener from the required property
* change events.
* <p>
*
* @param removedAxis
* the axis to not listen to any more.
*/
private void unlistenToAxis(final IAxis<?> removedAxis) {
removedAxis.removePropertyChangeListener(IAxis.PROPERTY_ADD_REMOVE_TRACE, this);
removedAxis.removePropertyChangeListener(IAxis.PROPERTY_LABELFORMATTER, this);
removedAxis.removePropertyChangeListener(IAxis.PROPERTY_PAINTGRID, this);
removedAxis.removePropertyChangeListener(IAxis.PROPERTY_RANGEPOLICY, this);
removedAxis.removePropertyChangeListener(IAxis.PROPERTY_AXIS_SCALE_POLICY_CHANGED, this);
}
/**
* Helper that removes this chart as a listener from the required property
* change events.
* <p>
*
* @param removedTrace
* the trace to not listen to any more.
*/
private void unlistenToTrace(final ITrace2D removedTrace) {
removedTrace.removePropertyChangeListener(ITrace2D.PROPERTY_POINT_HIGHLIGHTERS_CHANGED,
this.m_pointHighlightListener);
removedTrace.removePropertyChangeListener(ITrace2D.PROPERTY_POINT_HIGHLIGHTERS_CHANGED, this);
}
/**
* Compares wether the bounds since last invocation have changed and
* conditionally rescales the internal <code>{@link TracePoint2D}</code>
* instances.
* <p>
* Must only be called from <code>{@link #paint(Graphics)}</code>.
* <p>
* The old recorded values for the bounds are set to the actual values
* afterwards to allow detection of future changes again.
* <p>
* The force argument allows to enforce rescaling even if no change of data
* bounds took place since the last scaling. This is useful if e.g. the view
* upon the data is changed by a constraint (e.g.
* {@link info.monitorenter.gui.chart.rangepolicies.RangePolicyFixedViewport}
* ).
* <p>
*
* @param force
* if true no detection of changes of the data bounds as described
* above are performed: Rescaling is done unconditional.
*/
private synchronized void updateScaling(final boolean force) {
IAxis<?> currentAxis;
// 1) bottom x axes:
Iterator<IAxis<?>> it = this.m_axesXBottom.iterator();
while (it.hasNext()) {
currentAxis = it.next();
boolean changed = force;
changed = changed || currentAxis.isDirtyScaling();
if (changed) {
currentAxis.initPaintIteration();
currentAxis.scale();
if (Chart2D.DEBUG_SCALING) {
System.out.println("updateScaling: Scaling was performend for axis: "
+ currentAxis.getAxisTitle().getTitle());
}
} else {
if (Chart2D.DEBUG_SCALING) {
System.out.println("updateScaling: No scaling was performend for axis: "
+ currentAxis.getAxisTitle().getTitle());
}
}
}
// 2) top x axes:
it = this.m_axesXTop.iterator();
while (it.hasNext()) {
currentAxis = it.next();
boolean changed = force;
changed = changed || currentAxis.isDirtyScaling();
if (changed) {
currentAxis.initPaintIteration();
currentAxis.scale();
if (Chart2D.DEBUG_SCALING) {
System.out.println("updateScaling: Scaling was performend for axis: "
+ currentAxis.getAxisTitle().getTitle());
}
} else {
if (Chart2D.DEBUG_SCALING) {
System.out.println("updateScaling: No scaling was performend for axis: "
+ currentAxis.getAxisTitle().getTitle());
}
}
}
// 3) left y axes:
it = this.m_axesYLeft.iterator();
while (it.hasNext()) {
currentAxis = it.next();
boolean changed = force;
changed = changed || currentAxis.isDirtyScaling();
if (changed) {
currentAxis.initPaintIteration();
currentAxis.scale();
if (Chart2D.DEBUG_SCALING) {
System.out.println("updateScaling: Scaling was performend for axis: "
+ currentAxis.getAxisTitle().getTitle());
}
} else {
if (Chart2D.DEBUG_SCALING) {
System.out.println("updateScaling: No scaling was performend for axis: "
+ currentAxis.getAxisTitle().getTitle());
}
}
}
// 4) right y axes:
it = this.m_axesYRight.iterator();
while (it.hasNext()) {
currentAxis = it.next();
boolean changed = force;
changed = changed || currentAxis.isDirtyScaling();
if (changed) {
currentAxis.initPaintIteration();
currentAxis.scale();
if (Chart2D.DEBUG_SCALING) {
System.out.println("updateScaling: Scaling was performend for axis: "
+ currentAxis.getAxisTitle().getTitle());
}
} else {
if (Chart2D.DEBUG_SCALING) {
System.out.println("updateScaling: No scaling was performend for axis: "
+ currentAxis.getAxisTitle().getTitle());
}
}
}
}
}