package com.positive.charts.axis;
import java.io.Serializable;
import java.util.Iterator;
import java.util.List;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Rectangle;
import com.positive.charts.common.RectangleEdge;
import com.positive.charts.data.category.CategoryDataset;
import com.positive.charts.event.AxisChangeEvent;
import com.positive.charts.plot.CategoryPlot;
import com.positive.charts.plot.Plot;
import com.positive.charts.plot.PlotRenderingInfo;
import com.positive.charts.util.RectangleUtil;
import com.positive.charts.util.TextAnchor;
import com.positive.charts.util.TextUtilities;
/**
* A specialised category axis that can display sub-categories.
*/
public class SubCategoryAxis extends CategoryAxis implements Cloneable,
Serializable {
/** For serialization. */
private static final long serialVersionUID = -1279463299793228344L;
/** Storage for the sub-categories (these need to be set manually). */
private final List subCategories;
/** The font for the sub-category labels. */
private Font subLabelFont = null;
/**
* Creates a new axis.
*
* @param label
* the axis label.
*/
public SubCategoryAxis(final String label) {
super(label);
this.subCategories = new java.util.ArrayList();
}
/**
* Adds a sub-category to the axis.
*
* @param subCategory
* the sub-category.
*/
public void addSubCategory(final Comparable subCategory) {
this.subCategories.add(subCategory);
}
/**
* Draws the axis on a Java 2D graphics device (such as the screen or a
* printer).
*
* @param g2
* the graphics device (<code>null</code> not permitted).
* @param cursor
* the cursor location.
* @param plotArea
* the area within which the axis should be drawn (
* <code>null</code> not permitted).
* @param dataArea
* the area within which the plot is being drawn (
* <code>null</code> not permitted).
* @param edge
* the location of the axis (<code>null</code> not permitted).
* @param plotState
* collects information about the plot (<code>null</code>
* permitted).
*
* @return The axis state (never <code>null</code>).
*/
public AxisState draw(final GC g2, final double cursor,
final Rectangle plotArea, final Rectangle dataArea,
final RectangleEdge edge, final PlotRenderingInfo plotState) {
// if the axis is not visible, don't draw it...
if (!this.isVisible()) {
return new AxisState(cursor);
}
if (this.isAxisLineVisible()) {
this.drawAxisLine(g2, (int) (cursor - 1.5), dataArea, edge);
}
// draw the category labels and axis label
AxisState state = new AxisState(cursor);
state = this.drawSubCategoryLabels(g2, plotArea, dataArea, edge, state,
plotState);
state = this.drawCategoryLabels(g2, plotArea, dataArea, edge, state,
plotState);
state = this.drawLabel(this.getLabel(), g2, plotArea, dataArea, edge,
state);
return state;
}
/**
* Draws the category labels and returns the updated axis state.
*
* @param g2
* the graphics device (<code>null</code> not permitted).
* @param plotArea
* the plot area (<code>null</code> not permitted).
* @param dataArea
* the area inside the axes (<code>null</code> not permitted).
* @param edge
* the axis location (<code>null</code> not permitted).
* @param state
* the axis state (<code>null</code> not permitted).
* @param plotState
* collects information about the plot (<code>null</code>
* permitted).
*
* @return The updated axis state (never <code>null</code>).
*/
protected AxisState drawSubCategoryLabels(final GC g2,
final Rectangle plotArea, final Rectangle dataArea,
final RectangleEdge edge, final AxisState state,
final PlotRenderingInfo plotState) {
if (state == null) {
throw new IllegalArgumentException("Null 'state' argument.");
}
g2.setFont(this.subLabelFont);
g2.setForeground(this.getSubLabelPaint());
final CategoryPlot plot = (CategoryPlot) this.getPlot();
final CategoryDataset dataset = plot.getDataset();
final int categoryCount = dataset.getColumnCount();
final double maxdim = this.getMaxDim(g2, edge);
for (int categoryIndex = 0; categoryIndex < categoryCount; categoryIndex++) {
double x0 = 0.0;
double x1 = 0.0;
double y0 = 0.0;
double y1 = 0.0;
if (edge == RectangleEdge.TOP) {
x0 = this.getCategoryStart(categoryIndex, categoryCount,
dataArea, edge);
x1 = this.getCategoryEnd(categoryIndex, categoryCount,
dataArea, edge);
y1 = state.getCursor();
y0 = y1 - maxdim;
} else if (edge == RectangleEdge.BOTTOM) {
x0 = this.getCategoryStart(categoryIndex, categoryCount,
dataArea, edge);
x1 = this.getCategoryEnd(categoryIndex, categoryCount,
dataArea, edge);
y0 = state.getCursor();
y1 = y0 + maxdim;
} else if (edge == RectangleEdge.LEFT) {
y0 = this.getCategoryStart(categoryIndex, categoryCount,
dataArea, edge);
y1 = this.getCategoryEnd(categoryIndex, categoryCount,
dataArea, edge);
x1 = state.getCursor();
x0 = x1 - maxdim;
} else if (edge == RectangleEdge.RIGHT) {
y0 = this.getCategoryStart(categoryIndex, categoryCount,
dataArea, edge);
y1 = this.getCategoryEnd(categoryIndex, categoryCount,
dataArea, edge);
x0 = state.getCursor();
x1 = x0 + maxdim;
}
final Rectangle area = RectangleUtil.Double(x0, y0, (x1 - x0),
(y1 - y0));
final int subCategoryCount = this.subCategories.size();
final float width = (float) ((x1 - x0) / subCategoryCount);
final float height = (float) ((y1 - y0) / subCategoryCount);
float xx = 0.0f;
float yy = 0.0f;
for (int i = 0; i < subCategoryCount; i++) {
if (RectangleEdge.isTopOrBottom(edge)) {
xx = (float) (x0 + (i + 0.5) * width);
yy = RectangleUtil.getCenterY(area);
} else {
xx = RectangleUtil.getCenterX(area);
yy = (float) (y0 + (i + 0.5) * height);
}
final String label = this.subCategories.get(i).toString();
TextUtilities.drawRotatedString(label, g2, xx, yy,
TextAnchor.TOP_CENTER, 0.0, TextAnchor.TOP_CENTER);
}
}
if (edge.equals(RectangleEdge.TOP)) {
final double h = maxdim;
state.cursorUp(h);
} else if (edge.equals(RectangleEdge.BOTTOM)) {
final double h = maxdim;
state.cursorDown(h);
} else if (edge == RectangleEdge.LEFT) {
final double w = maxdim;
state.cursorLeft(w);
} else if (edge == RectangleEdge.RIGHT) {
final double w = maxdim;
state.cursorRight(w);
}
return state;
}
/**
* Tests the axis for equality with an arbitrary object.
*
* @param obj
* the object (<code>null</code> permitted).
*
* @return A boolean.
*/
public boolean equals(final Object obj) {
if (obj == this) {
return true;
}
if ((obj instanceof SubCategoryAxis) && super.equals(obj)) {
final SubCategoryAxis axis = (SubCategoryAxis) obj;
if (!this.subCategories.equals(axis.subCategories)) {
return false;
}
if (!this.subLabelFont.equals(axis.subLabelFont)) {
return false;
}
return true;
}
return false;
}
/**
* Returns the maximum of the relevant dimension (height or width) of the
* subcategory labels.
*
* @param g2
* the graphics device.
* @param edge
* the edge.
*
* @return The maximum dimension.
*/
private double getMaxDim(final GC g2, final RectangleEdge edge) {
double result = 0.0;
g2.setFont(this.subLabelFont);
final Iterator iterator = this.subCategories.iterator();
while (iterator.hasNext()) {
final Comparable subcategory = (Comparable) iterator.next();
final String label = subcategory.toString();
final Rectangle bounds = TextUtilities.getTextBounds(label, g2);
double dim = 0.0;
if (RectangleEdge.isLeftOrRight(edge)) {
dim = bounds.width;
} else { // must be top or bottom
dim = bounds.height;
}
result = Math.max(result, dim);
}
return result;
}
public int getSubCategoryCount() {
return this.subCategories.size();
}
/**
* Returns the font used to display the sub-category labels.
*
* @return The font (never <code>null</code>).
*/
public Font getSubLabelFont() {
return this.subLabelFont;
}
/**
* Returns the paint used to display the sub-category labels.
*
* @return The paint (never <code>null</code>).
*/
public Color getSubLabelPaint() {
return this.getPlot().getDrawingAssets().getColor(Plot.COLOR_SUB_LABEL);
}
public void removeSubcategories() {
this.subCategories.clear();
}
/**
* Estimates the space required for the axis, given a specific drawing area.
*
* @param g2
* the graphics device (used to obtain font information).
* @param plot
* the plot that the axis belongs to.
* @param plotArea
* the area within which the axis should be drawn.
* @param edge
* the axis location (top or bottom).
* @param space
* the space already reserved.
*
* @return The space required to draw the axis.
*/
public AxisSpace reserveSpace(final GC gc, final Plot plot,
final Rectangle plotArea, final RectangleEdge edge, AxisSpace space) {
// create a new space object if one wasn't supplied...
if (space == null) {
space = new AxisSpace();
}
// if the axis is not visible, no additional space is required...
if (!this.isVisible()) {
return space;
}
space = super.reserveSpace(gc, plot, plotArea, edge, space);
final double maxdim = this.getMaxDim(gc, edge);
if (RectangleEdge.isTopOrBottom(edge)) {
space.add((int) (maxdim + 0.5), edge);
} else if (RectangleEdge.isLeftOrRight(edge)) {
space.add((int) (maxdim + 0.5), edge);
}
return space;
}
/**
* Sets the font used to display the sub-category labels and sends an
* {@link AxisChangeEvent} to all registered listeners.
*
* @param font
* the font (<code>null</code> not permitted).
*/
public void setSubLabelFont(final Font font) {
if (font == null) {
throw new IllegalArgumentException("Null 'font' argument.");
}
this.subLabelFont = font;
this.notifyListeners(new AxisChangeEvent(this));
}
/**
* Sets the paint used to display the sub-category labels and sends an
* {@link AxisChangeEvent} to all registered listeners.
*
* @param paint
* the paint (<code>null</code> not permitted).
*/
public void setSubLabelPaint(final Color paint) {
if (paint == null) {
throw new IllegalArgumentException("Null 'paint' argument.");
}
this.getPlot().getDrawingAssets().setColor(Plot.COLOR_SUB_LABEL, paint);
this.notifyListeners(new AxisChangeEvent(this));
}
}