package com.positive.charts.axis;
import java.awt.Paint;
import org.eclipse.core.runtime.ListenerList;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Display;
import com.positive.charts.block.RectangleInsets;
import com.positive.charts.common.RectangleEdge;
import com.positive.charts.event.AxisChangeEvent;
import com.positive.charts.event.AxisChangeListener;
import com.positive.charts.plot.Plot;
import com.positive.charts.util.RectangleUtil;
import com.positive.charts.util.Stroke;
import com.positive.charts.util.TextAnchor;
import com.positive.charts.util.TextUtilities;
import com.positive.colorchecker.StaticColorChecker;
/**
* The base class for all axes in JFreeChart. Subclasses are divided into those
* that display values ({@link ValueAxis}) and those that display categories (
* {@link CategoryAxis}).
*/
public abstract class BaseAxis implements Axis {
/** The default axis visibility. */
public static final boolean DEFAULT_AXIS_VISIBLE = true;
/** The default tick labels visibility. */
public static final boolean DEFAULT_TICK_LABELS_VISIBLE = true;
/** The default tick marks visible. */
public static final boolean DEFAULT_TICK_MARKS_VISIBLE = true;
/** The default tick mark inside length. */
public static final float DEFAULT_TICK_MARK_INSIDE_LENGTH = 0.0f;
/** The default tick mark outside length. */
public static final float DEFAULT_TICK_MARK_OUTSIDE_LENGTH = 2.0f;
/** A flag indicating whether or not the axis is visible. */
private boolean visible;
/** The label for the axis. */
private String label;
/** The font for displaying the axis label. */
private Font labelFont;
/** The insets for the axis label. */
private RectangleInsets labelInsets = new RectangleInsets(2, 2, 2, 2);
/** The label angle. */
private double labelAngle;
/** A flag that controls whether or not the axis line is visible. */
private boolean axisLineVisible;
/**
* A flag that indicates whether or not tick labels are visible for the
* axis.
*/
private boolean tickLabelsVisible;
/** The font used to display the tick labels. */
private Font tickLabelFont;
/** The blank space around each tick label. */
private RectangleInsets tickLabelInsets = new RectangleInsets(1, 4, 1, 4);
/**
* A flag that indicates whether or not tick marks are visible for the axis.
*/
private boolean tickMarksVisible;
/** The length of the tick mark inside the data area (zero permitted). */
private float tickMarkInsideLength;
/** The length of the tick mark outside the data area (zero permitted). */
private float tickMarkOutsideLength;
/** The fixed (horizontal or vertical) dimension for the axis. */
private int fixedDimension;
/**
* A reference back to the plot that the axis is assigned to (can be
* <code>null</code>).
*/
private transient Plot plot;
/** Storage for registered listeners. */
private transient final ListenerList listenerList;
/**
* Constructs an axis, using default values where necessary.
*
* @param label
* the axis label (<code>null</code> permitted).
*/
protected BaseAxis(final String label) {
this.label = label;
this.visible = DEFAULT_AXIS_VISIBLE;
this.labelAngle = 0.0;
this.axisLineVisible = true;
this.tickLabelsVisible = DEFAULT_TICK_LABELS_VISIBLE;
this.tickMarksVisible = DEFAULT_TICK_MARKS_VISIBLE;
this.tickMarkInsideLength = DEFAULT_TICK_MARK_INSIDE_LENGTH;
this.tickMarkOutsideLength = DEFAULT_TICK_MARK_OUTSIDE_LENGTH;
this.plot = null;
this.listenerList = new ListenerList();
}
/**
* Registers an object for notification of changes to the axis.
*
* @param listener
* the object that is being registered.
*
* @see #removeChangeListener(AxisChangeListener)
*/
public void addChangeListener(final AxisChangeListener listener) {
this.listenerList.add(listener);
}
/**
* Configures the axis to work with the current plot. Override this method
* to perform any special processing (such as auto-rescaling).
*/
public abstract void configure();
/**
* Draws an axis line at the current cursor position and edge.
*
* @param gc
* the graphics device.
* @param cursor
* the cursor position.
* @param dataArea
* the data area.
* @param edge
* the edge.
*/
protected void drawAxisLine(final GC gc, final int cursor,
final Rectangle dataArea, final RectangleEdge edge) {
if (edge == RectangleEdge.TOP) {
gc.drawLine(dataArea.x, cursor, RectangleUtil.getMaxX(dataArea),
cursor);
} else if (edge == RectangleEdge.BOTTOM) {
gc.drawLine(dataArea.x, cursor, RectangleUtil.getMaxX(dataArea),
cursor);
} else if (edge == RectangleEdge.LEFT) {
gc.drawLine(cursor, dataArea.y, cursor, RectangleUtil
.getMaxY(dataArea)); // dataArea.x was replaced with
// dataArea.y - it was a BUG
} else if (edge == RectangleEdge.RIGHT) {
gc.drawLine(cursor, dataArea.y, cursor, RectangleUtil
.getMaxY(dataArea)); // dataArea.x was replaced with
// dataArea.y - it was a BUG
}
}
/**
* Draws the axis label.
*
* @param label
* the label text.
* @param g2
* the graphics device.
* @param plotArea
* the plot area.
* @param dataArea
* the area inside the axes.
* @param edge
* the location of the axis.
* @param state
* the axis state (<code>null</code> not permitted).
*
* @return Information about the axis.
*/
protected AxisState drawLabel(final String label, final GC g2,
final Rectangle plotArea, final Rectangle dataArea,
final RectangleEdge edge, final AxisState state) {
// it is unlikely that 'state' will be null, but check anyway...
if (state == null) {
throw new IllegalArgumentException("Null 'state' argument.");
}
if ((label == null) || (label.length() == 0)) {
return state;
}
// Font font = getLabelFont();
// g2.setFont(font);
// g2.setPaint(getLabelPaint());
final RectangleInsets insets = this.getLabelInsets();
final Rectangle labelBounds = TextUtilities.getTextBounds(label, g2);
double labelx, labely;
if (edge == RectangleEdge.TOP) {
labelx = RectangleUtil.getCenterX(dataArea);
labely = state.getCursor() - insets.getBottom()
- labelBounds.height / 2.0;
TextUtilities.drawRotatedString(label, g2, (float) labelx,
(float) labely, TextAnchor.CENTER, this.getLabelAngle(),
TextAnchor.CENTER);
state.cursorUp(insets.getTop() + labelBounds.height
+ insets.getBottom());
} else if (edge == RectangleEdge.BOTTOM) {
labelx = RectangleUtil.getCenterX(dataArea);
labely = state.getCursor() + insets.getTop() + labelBounds.height
/ 2.0;
TextUtilities.drawRotatedString(label, g2, (float) labelx,
(float) labely, TextAnchor.CENTER, this.getLabelAngle(),
TextAnchor.CENTER);
state.cursorDown(insets.getTop() + labelBounds.height
+ insets.getBottom());
} else if (edge == RectangleEdge.LEFT) {
this.swapWidthAndHeight(labelBounds);
labelx = state.getCursor() - insets.getRight() - labelBounds.width
/ 2.0;
labely = RectangleUtil.getCenterY(dataArea);
TextUtilities.drawRotatedString(label, g2, (float) labelx,
(float) labely, TextAnchor.CENTER, this.getLabelAngle()
- Math.PI / 2.0, TextAnchor.BOTTOM_CENTER);
state.cursorLeft(insets.getLeft() + labelBounds.width
+ insets.getRight());
} else if (edge == RectangleEdge.RIGHT) {
this.swapWidthAndHeight(labelBounds);
labelx = state.getCursor() + insets.getLeft() + labelBounds.width
/ 2.0;
labely = RectangleUtil.getCenterY(dataArea);
TextUtilities.drawRotatedString(label, g2, (float) labelx,
(float) labely, TextAnchor.CENTER, this.getLabelAngle()
+ Math.PI / 2.0, TextAnchor.BOTTOM_CENTER);
state.cursorRight(insets.getLeft() + labelBounds.width
+ insets.getRight());
}
return state;
}
/**
* Returns the fixed dimension for the axis.
*
* @return The fixed dimension.
*
* @see #setFixedDimension(double)
*/
public int getFixedDimension() {
return this.fixedDimension;
}
/**
* Returns the label for the axis.
*
* @return The label for the axis (<code>null</code> possible).
*
* @see #getLabelFont()
* @see #getLabelPaint()
* @see #setLabel(String)
*/
public String getLabel() {
return this.label;
}
/**
* Returns the angle of the axis label.
*
* @return The angle (in radians).
*
* @see #setLabelAngle(double)
*/
public double getLabelAngle() {
return this.labelAngle;
}
/**
* Returns a rectangle that encloses the axis label. This is typically used
* for layout purposes (it gives the maximum dimensions of the label).
*
* @param g2
* the graphics device.
* @param edge
* the edge of the plot area along which the axis is measuring.
*
* @return The enclosing rectangle.
*/
protected Rectangle getLabelEnclosure(final GC g2, final RectangleEdge edge) {
Rectangle result = new Rectangle(0, 0, 0, 0);
final String axisLabel = this.getLabel();
if ((axisLabel != null) && (axisLabel.length() != 0)) {
g2.setFont(this.getLabelFont());
final Point textExtent = g2.textExtent(axisLabel);
Rectangle bounds = new Rectangle(0, 0, textExtent.x, textExtent.y);
final RectangleInsets insets = this.getLabelInsets();
bounds = insets.createOutsetRectangle(bounds);
final Rectangle labelBounds = bounds;
if (edge.isLeftOrRight()) {
// Rotated 90 degrees, so swap width and height.
this.swapWidthAndHeight(labelBounds);
}
result = labelBounds;
}
return result;
}
/**
* Returns the font for the axis label.
*
* @return The font (never <code>null</code>).
*
* @see #setLabelFont(Font)
*/
public Font getLabelFont() {
return this.labelFont;
}
/**
* Returns the insets for the label (that is, the amount of blank space that
* should be left around the label).
*
* @return The label insets (never <code>null</code>).
*
* @see #setLabelInsets(RectangleInsets)
*/
public RectangleInsets getLabelInsets() {
return this.labelInsets;
}
/**
* Returns the plot that the axis is assigned to. This method will return
* <code>null</code> if the axis is not currently assigned to a plot.
*
* @return The plot that the axis is assigned to (possibly <code>null</code>
* ).
*
* @see #setPlot(Plot)
*/
public Plot getPlot() {
return this.plot;
}
/**
* Returns the font used for the tick labels (if showing).
*
* @return The font (never <code>null</code>).
*
* @see #setTickLabelFont(Font)
*/
public Font getTickLabelFont() {
if (this.tickLabelFont == null) {
return Display.getCurrent().getSystemFont();
}
return this.tickLabelFont;
}
/**
* Returns the insets for the tick labels.
*
* @return The insets (never <code>null</code>).
*
* @see #setTickLabelInsets(RectangleInsets)
*/
public RectangleInsets getTickLabelInsets() {
return this.tickLabelInsets;
}
/**
* Returns the color/shade used for the tick labels.
*
* @return The paint used for the tick labels.
*
* @see #setTickLabelPaint(Paint)
*/
public Color getTickLabelPaint() {
final Color color = this.getPlot().getDrawingAssets().getColor(
Plot.COLOR_TICK_LABEL);
if (color == null) {
return StaticColorChecker.dublicateColor(SWT.COLOR_BLACK); //Display.getCurrent().getSystemColor(SWT.COLOR_BLACK);
}
return color;
}
/**
* Returns the inside length of the tick marks.
*
* @return The length.
*
* @see #getTickMarkOutsideLength()
* @see #setTickMarkInsideLength(float)
*/
public float getTickMarkInsideLength() {
return this.tickMarkInsideLength;
}
/**
* Returns the outside length of the tick marks.
*
* @return The length.
*
* @see #getTickMarkInsideLength()
* @see #setTickMarkOutsideLength(float)
*/
public float getTickMarkOutsideLength() {
return this.tickMarkOutsideLength;
}
/**
* Returns <code>true</code> if the specified object is registered with the
* dataset as a listener. Most applications won't need to call this method,
* it exists mainly for use by unit testing code.
*
* @param listener
* the listener.
*
* @return A boolean.
*/
public boolean hasListener(final AxisChangeListener listener) {
// return listenerList.contains(listener);
// FIXME Implement some kind of ListenerList#contains()
return false;
}
/**
* A flag that controls whether or not the axis line is drawn.
*
* @return A boolean.
*
* @see #getAxisLinePaint()
* @see #getAxisLineStroke()
* @see #setAxisLineVisible(boolean)
*/
public boolean isAxisLineVisible() {
return this.axisLineVisible;
}
/**
* Returns a flag indicating whether or not the tick labels are visible.
*
* @return The flag.
*
* @see #getTickLabelFont()
* @see #getTickLabelPaint()
* @see #setTickLabelsVisible(boolean)
*/
public boolean isTickLabelsVisible() {
return this.tickLabelsVisible;
}
/**
* Returns the flag that indicates whether or not the tick marks are
* showing.
*
* @return The flag that indicates whether or not the tick marks are
* showing.
*
* @see #setTickMarksVisible(boolean)
*/
public boolean isTickMarksVisible() {
return this.tickMarksVisible;
}
/**
* Returns <code>true</code> if the axis is visible, and <code>false</code>
* otherwise.
*
* @return A boolean.
*
* @see #setVisible(boolean)
*/
public boolean isVisible() {
return this.visible;
}
/**
* Notifies all registered listeners that the axis has changed. The
* AxisChangeEvent provides information about the change.
*
* @param event
* information about the change to the axis.
*/
protected void notifyListeners(final AxisChangeEvent event) {
final Object[] listeners = this.listenerList.getListeners();
for (int i = 0; i < listeners.length; i++) {
final AxisChangeListener listener = (AxisChangeListener) listeners[i];
listener.axisChanged(event);
}
}
/**
* Deregisters an object for notification of changes to the axis.
*
* @param listener
* the object to deregister.
*
* @see #addChangeListener(AxisChangeListener)
*/
public void removeChangeListener(final AxisChangeListener listener) {
this.listenerList.remove(listener);
}
/**
* Sets a flag that controls whether or not the axis line is visible and
* sends an {@link AxisChangeEvent} to all registered listeners.
*
* @param visible
* the flag.
*
* @see #isAxisLineVisible()
* @see #setAxisLinePaint(Paint)
* @see #setAxisLineStroke(Stroke)
*/
public void setAxisLineVisible(final boolean visible) {
this.axisLineVisible = visible;
this.notifyListeners(new AxisChangeEvent(this));
}
/**
* Sets the fixed dimension for the axis.
* <P>
* This is used when combining more than one plot on a chart. In this case,
* there may be several axes that need to have the same height or width so
* that they are aligned. This method is used to fix a dimension for the
* axis (the context determines whether the dimension is horizontal or
* vertical).
*
* @param dimension
* the fixed dimension.
*
* @see #getFixedDimension()
*/
public void setFixedDimension(final int dimension) {
this.fixedDimension = dimension;
}
/**
* Sets the label for the axis and sends an {@link AxisChangeEvent} to all
* registered listeners.
*
* @param label
* the new label (<code>null</code> permitted).
*
* @see #getLabel()
* @see #setLabelFont(Font)
* @see #setLabelPaint(Paint)
*/
public void setLabel(final String label) {
final String existing = this.label;
if (existing != null) {
if (!existing.equals(label)) {
this.label = label;
this.notifyListeners(new AxisChangeEvent(this));
}
} else {
if (label != null) {
this.label = label;
this.notifyListeners(new AxisChangeEvent(this));
}
}
}
/**
* Sets the angle for the label and sends an {@link AxisChangeEvent} to all
* registered listeners.
*
* @param angle
* the angle (in radians).
*
* @see #getLabelAngle()
*/
public void setLabelAngle(final double angle) {
this.labelAngle = angle;
this.notifyListeners(new AxisChangeEvent(this));
}
/**
* Sets the font for the axis label and sends an {@link AxisChangeEvent} to
* all registered listeners.
*
* @param font
* the font (<code>null</code> not permitted).
*
* @see #getLabelFont()
*/
public void setLabelFont(final Font font) {
if (font == null) {
throw new IllegalArgumentException("Null 'font' argument.");
}
if (!font.equals(this.labelFont)) {
this.labelFont = font;
this.notifyListeners(new AxisChangeEvent(this));
}
}
/**
* Sets the insets for the axis label, and sends an {@link AxisChangeEvent}
* to all registered listeners.
*
* @param insets
* the insets (<code>null</code> not permitted).
*
* @see #getLabelInsets()
*/
public void setLabelInsets(final RectangleInsets insets) {
if (insets == null) {
throw new IllegalArgumentException("Null 'insets' argument.");
}
if (!insets.equals(this.labelInsets)) {
this.labelInsets = insets;
this.notifyListeners(new AxisChangeEvent(this));
}
}
/**
* Sets a reference to the plot that the axis is assigned to.
* <P>
* This method is used internally, you shouldn't need to call it yourself.
*
* @param plot
* the plot.
*
* @see #getPlot()
*/
public void setPlot(final Plot plot) {
this.plot = plot;
this.configure();
}
/**
* Sets the font for the tick labels and sends an {@link AxisChangeEvent} to
* all registered listeners.
*
* @param font
* the font (<code>null</code> not allowed).
*
* @see #getTickLabelFont()
*/
public void setTickLabelFont(final Font font) {
if (font == null) {
throw new IllegalArgumentException("Null 'font' argument.");
}
if (!font.equals(this.tickLabelFont)) {
this.tickLabelFont = font;
this.notifyListeners(new AxisChangeEvent(this));
}
}
/**
* Sets the insets for the tick labels and sends an {@link AxisChangeEvent}
* to all registered listeners.
*
* @param insets
* the insets (<code>null</code> not permitted).
*
* @see #getTickLabelInsets()
*/
public void setTickLabelInsets(final RectangleInsets insets) {
if (insets == null) {
throw new IllegalArgumentException("Null 'insets' argument.");
}
if (!this.tickLabelInsets.equals(insets)) {
this.tickLabelInsets = insets;
this.notifyListeners(new AxisChangeEvent(this));
}
}
/**
* Sets the paint used to draw tick labels (if they are showing) and sends
* an {@link AxisChangeEvent} to all registered listeners.
*
* @param paint
* the paint (<code>null</code> not permitted).
*
* @see #getTickLabelPaint()
*/
public void setTickLabelPaint(final Color paint) {
if (paint == null) {
throw new IllegalArgumentException("Null 'paint' argument.");
}
this.notifyListeners(new AxisChangeEvent(this));
}
/**
* Sets the flag that determines whether or not the tick labels are visible
* and sends an {@link AxisChangeEvent} to all registered listeners.
*
* @param flag
* the flag.
*
* @see #isTickLabelsVisible()
* @see #setTickLabelFont(Font)
* @see #setTickLabelPaint(Paint)
*/
public void setTickLabelsVisible(final boolean flag) {
if (flag != this.tickLabelsVisible) {
this.tickLabelsVisible = flag;
this.notifyListeners(new AxisChangeEvent(this));
}
}
/**
* Sets the inside length of the tick marks and sends an
* {@link AxisChangeEvent} to all registered listeners.
*
* @param length
* the new length.
*
* @see #getTickMarkInsideLength()
*/
public void setTickMarkInsideLength(final float length) {
this.tickMarkInsideLength = length;
this.notifyListeners(new AxisChangeEvent(this));
}
/**
* Sets the outside length of the tick marks and sends an
* {@link AxisChangeEvent} to all registered listeners.
*
* @param length
* the new length.
*
* @see #getTickMarkInsideLength()
*/
public void setTickMarkOutsideLength(final float length) {
this.tickMarkOutsideLength = length;
this.notifyListeners(new AxisChangeEvent(this));
}
/**
* Sets the flag that indicates whether or not the tick marks are showing
* and sends an {@link AxisChangeEvent} to all registered listeners.
*
* @param flag
* the flag.
*
* @see #isTickMarksVisible()
*/
public void setTickMarksVisible(final boolean flag) {
if (flag != this.tickMarksVisible) {
this.tickMarksVisible = flag;
this.notifyListeners(new AxisChangeEvent(this));
}
}
/**
* Sets a flag that controls whether or not the axis is visible and sends an
* {@link AxisChangeEvent} to all registered listeners.
*
* @param flag
* the flag.
*
* @see #isVisible()
*/
public void setVisible(final boolean flag) {
if (flag != this.visible) {
this.visible = flag;
this.notifyListeners(new AxisChangeEvent(this));
}
}
private void swapWidthAndHeight(final Rectangle rect) {
final int temp = rect.width;
rect.width = rect.height;
rect.height = temp;
}
}