/* ===========================================================
* Orson Charts : a 3D chart library for the Java(tm) platform
* ===========================================================
*
* (C)opyright 2013-2016, by Object Refinery Limited. All rights reserved.
*
* http://www.object-refinery.com/orsoncharts/index.html
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* [Oracle and Java are registered trademarks of Oracle and/or its affiliates.
* Other names may be trademarks of their respective owners.]
*
* If you do not wish to be bound by the terms of the GPL, an alternative
* commercial license can be purchased. For details, please see visit the
* Orson Charts home page:
*
* http://www.object-refinery.com/orsoncharts/index.html
*
*/
package com.orsoncharts.axis;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.swing.event.EventListenerList;
import com.orsoncharts.Chart3DHints;
import com.orsoncharts.ChartElementVisitor;
import com.orsoncharts.graphics3d.RenderedElement;
import com.orsoncharts.graphics3d.RenderingInfo;
import com.orsoncharts.graphics3d.Utils2D;
import com.orsoncharts.interaction.InteractiveElementType;
import com.orsoncharts.marker.MarkerChangeEvent;
import com.orsoncharts.marker.MarkerChangeListener;
import com.orsoncharts.plot.CategoryPlot3D;
import com.orsoncharts.util.ArgChecks;
import com.orsoncharts.util.ObjectUtils;
import com.orsoncharts.util.SerialUtils;
import com.orsoncharts.util.TextAnchor;
import com.orsoncharts.util.TextUtils;
/**
* A base class that can be used to create an {@link Axis3D} implementation.
* This class implements the core axis attributes as well as the change
* listener mechanism required to enable automatic repainting of charts.
* <br><br>
* NOTE: This class is serializable, but the serialization format is subject
* to change in future releases and should not be relied upon for persisting
* instances of this class.
*/
@SuppressWarnings("serial")
public abstract class AbstractAxis3D implements Axis3D, MarkerChangeListener,
Serializable {
/**
* The default axis label font (in most circumstances this will be
* overridden by the chart style).
*
* @since 1.2
*/
public static final Font DEFAULT_LABEL_FONT = new Font("Dialog", Font.BOLD,
12);
/**
* The default axis label color (in most circumstances this will be
* overridden by the chart style).
*
* @since 1.2
*/
public static final Color DEFAULT_LABEL_COLOR = Color.BLACK;
/**
* The default label offset.
*
* @since 1.2
*/
public static final double DEFAULT_LABEL_OFFSET = 10;
/**
* The default tick label font (in most circumstances this will be
* overridden by the chart style).
*
* @since 1.2
*/
public static final Font DEFAULT_TICK_LABEL_FONT = new Font("Dialog",
Font.PLAIN, 12);
/**
* The default tick label color (in most circumstances this will be
* overridden by the chart style).
*
* @since 1.2
*/
public static final Color DEFAULT_TICK_LABEL_COLOR = Color.BLACK;
/**
* The default stroke for the axis line.
*
* @since 1.2
*/
public static final Stroke DEFAULT_LINE_STROKE = new BasicStroke(0f);
/**
* The default color for the axis line.
*
* @since 1.2
*/
public static final Color DEFAULT_LINE_COLOR = Color.GRAY;
/** A flag that determines whether or not the axis will be drawn. */
private boolean visible;
/** The axis label (if {@code null}, no label is displayed). */
private String label;
/** The label font (never {@code null}). */
private Font labelFont;
/** The color used to draw the axis label (never {@code null}). */
private Color labelColor;
/** The offset between the tick labels and the label. */
private double labelOffset;
/** The stroke used to draw the axis line. */
private transient Stroke lineStroke;
/** The color used to draw the axis line. */
private Color lineColor;
/** Draw the tick labels? */
private boolean tickLabelsVisible;
/** The font used to display tick labels (never {@code null}) */
private Font tickLabelFont;
/** The tick label paint (never {@code null}). */
private Color tickLabelColor;
/** Storage for registered change listeners. */
private final transient EventListenerList listenerList;
/**
* Creates a new label with the specified label. If the supplied label
* is {@code null}, the axis will be shown without a label.
*
* @param label the axis label ({@code null} permitted).
*/
public AbstractAxis3D(String label) {
this.visible = true;
this.label = label;
this.labelFont = DEFAULT_LABEL_FONT;
this.labelColor = DEFAULT_LABEL_COLOR;
this.labelOffset = DEFAULT_LABEL_OFFSET;
this.lineStroke = DEFAULT_LINE_STROKE;
this.lineColor = DEFAULT_LINE_COLOR;
this.tickLabelsVisible = true;
this.tickLabelFont = DEFAULT_TICK_LABEL_FONT;
this.tickLabelColor = DEFAULT_TICK_LABEL_COLOR;
this.listenerList = new EventListenerList();
}
/**
* Returns the flag that determines whether or not the axis is drawn
* on the chart.
*
* @return A boolean.
*
* @see #setVisible(boolean)
*/
@Override
public boolean isVisible() {
return this.visible;
}
/**
* Sets the flag that determines whether or not the axis is drawn on the
* chart and sends an {@link Axis3DChangeEvent} to all registered listeners.
*
* @param visible the flag.
*
* @see #isVisible()
*/
@Override
public void setVisible(boolean visible) {
this.visible = visible;
fireChangeEvent(false);
}
/**
* Returns the axis label - the text that describes what the axis measures.
* The description should usually specify the units. When this attribute
* is {@code null}, the axis is drawn without a label.
*
* @return The axis label (possibly {@code null}).
*/
public String getLabel() {
return this.label;
}
/**
* Sets the axis label and sends an {@link Axis3DChangeEvent} to all
* registered listeners. If the supplied label is {@code null},
* the axis will be drawn without a label.
*
* @param label the label ({@code null} permitted).
*/
public void setLabel(String label) {
this.label = label;
fireChangeEvent(false);
}
/**
* Returns the font used to display the main axis label. The default value
* is {@code Font("SansSerif", Font.BOLD, 12)}.
*
* @return The font used to display the axis label (never {@code null}).
*/
@Override
public Font getLabelFont() {
return this.labelFont;
}
/**
* Sets the font used to display the main axis label and sends an
* {@link Axis3DChangeEvent} to all registered listeners.
*
* @param font the new font ({@code null} not permitted).
*/
@Override
public void setLabelFont(Font font) {
ArgChecks.nullNotPermitted(font, "font");
this.labelFont = font;
fireChangeEvent(false);
}
/**
* Returns the color used for the label. The default value is
* {@code Color.BLACK}.
*
* @return The label paint (never {@code null}).
*/
@Override
public Color getLabelColor() {
return this.labelColor;
}
/**
* Sets the color used to draw the axis label and sends an
* {@link Axis3DChangeEvent} to all registered listeners.
*
* @param color the color ({@code null} not permitted).
*/
@Override
public void setLabelColor(Color color) {
ArgChecks.nullNotPermitted(color, "color");
this.labelColor = color;
fireChangeEvent(false);
}
/**
* Returns the offset between the tick labels and the axis label, measured
* in Java2D units. The default value is {@link #DEFAULT_LABEL_OFFSET}.
*
* @return The offset.
*
* @since 1.2
*/
public double getLabelOffset() {
return this.labelOffset;
}
/**
* Sets the offset between the tick labels and the axis label and sends
* an {@link Axis3DChangeEvent} to all registered listeners.
*
* @param offset the offset.
*
* @since 1.2
*/
public void setLabelOffset(double offset) {
this.labelOffset = offset;
fireChangeEvent(false);
}
/**
* Returns the stroke used to draw the axis line. The default value is
* {@link #DEFAULT_LINE_STROKE}.
*
* @return The stroke used to draw the axis line (never {@code null}).
*/
public Stroke getLineStroke() {
return this.lineStroke;
}
/**
* Sets the stroke used to draw the axis line and sends an
* {@link Axis3DChangeEvent} to all registered listeners.
*
* @param stroke the new stroke ({@code null} not permitted).
*/
public void setLineStroke(Stroke stroke) {
ArgChecks.nullNotPermitted(stroke, "stroke");
this.lineStroke = stroke;
fireChangeEvent(false);
}
/**
* Returns the color used to draw the axis line. The default value is
* {@link #DEFAULT_LINE_COLOR}.
*
* @return The color used to draw the axis line (never {@code null}).
*/
public Color getLineColor() {
return this.lineColor;
}
/**
* Sets the color used to draw the axis line and sends an
* {@link Axis3DChangeEvent} to all registered listeners.
*
* @param color the new color ({@code null} not permitted).
*/
public void setLineColor(Color color) {
ArgChecks.nullNotPermitted(color, "color");
this.lineColor = color;
fireChangeEvent(false);
}
/**
* Returns the flag that controls whether or not the tick labels are
* drawn. The default value is {@code true}.
*
* @return A boolean.
*/
public boolean getTickLabelsVisible() {
return this.tickLabelsVisible;
}
/**
* Sets the flag that controls whether or not the tick labels are drawn,
* and sends a change event to all registered listeners. You should think
* carefully before setting this flag to {@code false}, because if
* the tick labels are not shown it will be hard for the reader to
* understand the resulting chart.
*
* @param visible visible?
*/
public void setTickLabelsVisible(boolean visible) {
this.tickLabelsVisible = visible;
fireChangeEvent(false);
}
/**
* Returns the font used to display the tick labels. The default value
* is {@link #DEFAULT_TICK_LABEL_FONT}.
*
* @return The font (never {@code null}).
*/
@Override
public Font getTickLabelFont() {
return this.tickLabelFont;
}
/**
* Sets the font used to display tick labels and sends an
* {@link Axis3DChangeEvent} to all registered listeners.
*
* @param font the font ({@code null} not permitted).
*/
@Override
public void setTickLabelFont(Font font) {
ArgChecks.nullNotPermitted(font, "font");
this.tickLabelFont = font;
fireChangeEvent(false);
}
/**
* Returns the foreground color for the tick labels. The default value
* is {@link #DEFAULT_LABEL_COLOR}.
*
* @return The foreground color (never {@code null}).
*/
@Override
public Color getTickLabelColor() {
return this.tickLabelColor;
}
/**
* Sets the foreground color for the tick labels and sends an
* {@link Axis3DChangeEvent} to all registered listeners.
*
* @param color the color ({@code null} not permitted).
*/
@Override
public void setTickLabelColor(Color color) {
ArgChecks.nullNotPermitted(color, "color");
this.tickLabelColor = color;
fireChangeEvent(false);
}
/**
* Receives a {@link ChartElementVisitor}. This method is part of a general
* mechanism for traversing the chart structure and performing operations
* on each element in the chart. You will not normally call this method
* directly.
*
* @param visitor the visitor ({@code null} not permitted).
*
* @since 1.2
*/
@Override
public abstract void receive(ChartElementVisitor visitor);
/**
* Draws the specified text as the axis label and returns a bounding
* shape (2D) for the text.
*
* @param label the label ({@code null} not permitted).
* @param g2 the graphics target ({@code null} not permitted).
* @param axisLine the axis line ({@code null} not permitted).
* @param opposingPt an opposing point ({@code null} not permitted).
* @param offset the offset.
* @param info collects rendering info ({@code null} permitted).
* @param hinting perform element hinting?
*
* @return A bounding shape.
*/
protected Shape drawAxisLabel(String label, Graphics2D g2,
Line2D axisLine, Point2D opposingPt, double offset,
RenderingInfo info, boolean hinting) {
ArgChecks.nullNotPermitted(label, "label");
ArgChecks.nullNotPermitted(g2, "g2");
ArgChecks.nullNotPermitted(axisLine, "axisLine");
ArgChecks.nullNotPermitted(opposingPt, "opposingPt");
g2.setFont(getLabelFont());
g2.setPaint(getLabelColor());
Line2D labelPosLine = Utils2D.createPerpendicularLine(axisLine, 0.5,
offset, opposingPt);
double theta = Utils2D.calculateTheta(axisLine);
if (theta < -Math.PI / 2.0) {
theta = theta + Math.PI;
}
if (theta > Math.PI / 2.0) {
theta = theta - Math.PI;
}
if (hinting) {
Map<String, String> m = new HashMap<String, String>();
m.put("ref", "{\"type\": \"axisLabel\", \"axis\": \"" + axisStr()
+ "\", \"label\": \"" + getLabel() + "\"}");
g2.setRenderingHint(Chart3DHints.KEY_BEGIN_ELEMENT, m);
}
Shape bounds = TextUtils.drawRotatedString(getLabel(), g2,
(float) labelPosLine.getX2(), (float) labelPosLine.getY2(),
TextAnchor.CENTER, theta, TextAnchor.CENTER);
if (hinting) {
g2.setRenderingHint(Chart3DHints.KEY_END_ELEMENT, true);
}
if (info != null) {
RenderedElement labelElement = new RenderedElement(
InteractiveElementType.AXIS_LABEL, bounds);
labelElement.setProperty("axis", axisStr());
labelElement.setProperty("label", getLabel());
info.addOffsetElement(labelElement);
}
return bounds;
}
/**
* Returns a string representing the configured type of the axis ("row",
* "column", "value", "x", "y" or "z" - other values may be possible in the
* future). A <em>row</em> axis on a {@link CategoryPlot3D} is in the
* position of a z-axis (depth), a <em>column</em> axis is in the position
* of an x-axis (width), a <em>value</em> axis is in the position of a
* y-axis (height).
*
* @return A string (never {@code null}).
*
* @since 1.3
*/
protected abstract String axisStr();
/**
* Draws the axis along an arbitrary line (between {@code startPt}
* and {@code endPt}). The opposing point is used as a reference
* point to know on which side of the axis to draw the labels.
*
* @param g2 the graphics target ({@code null} not permitted).
* @param startPt the starting point ({@code null} not permitted).
* @param endPt the end point ({@code null} not permitted)
* @param opposingPt an opposing point ({@code null} not permitted).
* @param tickData info about the ticks to draw ({@code null} not
* permitted).
* @param info an object to be populated with rendering info
* ({@code null} permitted).
* @param hinting a flag that controls whether or not element hinting
* should be performed.
*/
@Override
public abstract void draw(Graphics2D g2, Point2D startPt, Point2D endPt,
Point2D opposingPt, List<TickData> tickData, RenderingInfo info,
boolean hinting);
/**
* Tests this instance for equality with an arbitrary object.
*
* @param obj the object to test against ({@code null} permitted).
*
* @return A boolean.
*/
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (!(obj instanceof AbstractAxis3D)) {
return false;
}
AbstractAxis3D that = (AbstractAxis3D) obj;
if (this.visible != that.visible) {
return false;
}
if (!ObjectUtils.equals(this.label, that.label)) {
return false;
}
if (!this.labelFont.equals(that.labelFont)) {
return false;
}
if (!this.labelColor.equals(that.labelColor)) {
return false;
}
if (!this.lineStroke.equals(that.lineStroke)) {
return false;
}
if (!this.lineColor.equals(that.lineColor)) {
return false;
}
if (this.tickLabelsVisible != that.tickLabelsVisible) {
return false;
}
if (!this.tickLabelFont.equals(that.tickLabelFont)) {
return false;
}
if (!this.tickLabelColor.equals(that.tickLabelColor)) {
return false;
}
return true;
}
/**
* Returns a hash code for this instance.
*
* @return A hash code.
*/
@Override
public int hashCode() {
int hash = 5;
hash = 83 * hash + (this.visible ? 1 : 0);
hash = 83 * hash + ObjectUtils.hashCode(this.label);
hash = 83 * hash + ObjectUtils.hashCode(this.labelFont);
hash = 83 * hash + ObjectUtils.hashCode(this.labelColor);
hash = 83 * hash + ObjectUtils.hashCode(this.lineStroke);
hash = 83 * hash + ObjectUtils.hashCode(this.lineColor);
hash = 83 * hash + (this.tickLabelsVisible ? 1 : 0);
hash = 83 * hash + ObjectUtils.hashCode(this.tickLabelFont);
hash = 83 * hash + ObjectUtils.hashCode(this.tickLabelColor);
return hash;
}
/**
* Registers a listener so that it will receive axis change events.
*
* @param listener the listener ({@code null} not permitted).
*/
@Override
public void addChangeListener(Axis3DChangeListener listener) {
this.listenerList.add(Axis3DChangeListener.class, listener);
}
/**
* Deregisters a listener so that it will no longer receive axis
* change events.
*
* @param listener the listener ({@code null} not permitted).
*/
@Override
public void removeChangeListener(Axis3DChangeListener listener) {
this.listenerList.remove(Axis3DChangeListener.class, listener);
}
/**
* Notifies all registered listeners that the plot has been modified.
*
* @param event information about the change event.
*/
public void notifyListeners(Axis3DChangeEvent event) {
Object[] listeners = this.listenerList.getListenerList();
for (int i = listeners.length - 2; i >= 0; i -= 2) {
if (listeners[i] == Axis3DChangeListener.class) {
((Axis3DChangeListener) listeners[i + 1]).axisChanged(event);
}
}
}
/**
* Sends an {@link Axis3DChangeEvent} to all registered listeners.
*
* @param requiresWorldUpdate a flag indicating whether or not this change
* requires the 3D world to be updated.
*/
protected void fireChangeEvent(boolean requiresWorldUpdate) {
notifyListeners(new Axis3DChangeEvent(this, requiresWorldUpdate));
}
/**
* Receives notification of a change to a marker managed by this axis - the
* response is to fire a change event for the axis (to eventually trigger
* a repaint of the chart). Marker changes don't require the world model
* to be updated.
*
* @param event the event.
*
* @since 1.2
*/
@Override
public void markerChanged(MarkerChangeEvent event) {
fireChangeEvent(false);
}
/**
* Provides serialization support.
*
* @param stream the output stream.
*
* @throws IOException if there is an I/O error.
*/
private void writeObject(ObjectOutputStream stream) throws IOException {
stream.defaultWriteObject();
SerialUtils.writeStroke(this.lineStroke, stream);
}
/**
* Provides serialization support.
*
* @param stream the input stream.
*
* @throws IOException if there is an I/O error.
* @throws ClassNotFoundException if there is a classpath problem.
*/
private void readObject(ObjectInputStream stream)
throws IOException, ClassNotFoundException {
stream.defaultReadObject();
this.lineStroke = SerialUtils.readStroke(stream);
}
}