    001/* ===========================================================
    002 * Orson Charts : a 3D chart library for the Java(tm) platform
    003 * ===========================================================
    004 *
    005 * (C)opyright 2013-2016, by Object Refinery Limited.  All rights reserved.
    006 *
    007 * http://www.object-refinery.com/orsoncharts/index.html
    008 *
    009 * This program is free software: you can redistribute it and/or modify
    010 * it under the terms of the GNU General Public License as published by
    011 * the Free Software Foundation, either version 3 of the License, or
    012 * (at your option) any later version.
    013 *
    014 * This program is distributed in the hope that it will be useful,
    015 * but WITHOUT ANY WARRANTY; without even the implied warranty of
    016 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    017 * GNU General Public License for more details.
    018 *
    019 * You should have received a copy of the GNU General Public License
    020 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    021 *
    022 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates.
    023 * Other names may be trademarks of their respective owners.]
    024 *
    025 * If you do not wish to be bound by the terms of the GPL, an alternative
    026 * commercial license can be purchased.  For details, please see visit the
    027 * Orson Charts home page:
    028 *
    029 * http://www.object-refinery.com/orsoncharts/index.html
    030 *
    031 */
    032
    033package com.orsoncharts.axis;
    034
    035import java.awt.BasicStroke;
    036import java.awt.Color;
    037import java.awt.Font;
    038import java.awt.Graphics2D;
    039import java.awt.Shape;
    040import java.awt.Stroke;
    041import java.awt.geom.Line2D;
    042import java.awt.geom.Point2D;
    043import java.io.IOException;
    044import java.io.ObjectInputStream;
    045import java.io.ObjectOutputStream;
    046import java.io.Serializable;
    047import java.util.HashMap;
    048import java.util.List;
    049import java.util.Map;
    050
    051import javax.swing.event.EventListenerList;
    052
    053import com.orsoncharts.Chart3DHints;
    054import com.orsoncharts.ChartElementVisitor;
    055import com.orsoncharts.graphics3d.RenderedElement;
    056import com.orsoncharts.graphics3d.RenderingInfo;
    057import com.orsoncharts.graphics3d.Utils2D;
    058import com.orsoncharts.interaction.InteractiveElementType;
    059import com.orsoncharts.marker.MarkerChangeEvent;
    060import com.orsoncharts.marker.MarkerChangeListener;
    061import com.orsoncharts.plot.CategoryPlot3D;
    062import com.orsoncharts.util.ArgChecks;
    063import com.orsoncharts.util.ObjectUtils;
    064import com.orsoncharts.util.SerialUtils;
    065import com.orsoncharts.util.TextAnchor;
    066import com.orsoncharts.util.TextUtils;
    067
    068/**
    069 * A base class that can be used to create an {@link Axis3D} implementation.
    070 * This class implements the core axis attributes as well as the change
    071 * listener mechanism required to enable automatic repainting of charts.
    072 * <br><br>
    073 * NOTE: This class is serializable, but the serialization format is subject
    074 * to change in future releases and should not be relied upon for persisting
    075 * instances of this class.
    076 */
    077@SuppressWarnings("serial")
    078public abstract class AbstractAxis3D implements Axis3D, MarkerChangeListener,
    079        Serializable {
    080
    081    /**
    082     * The default axis label font (in most circumstances this will be
    083     * overridden by the chart style).
    084     *
    085     * @since 1.2
    086     */
    087    public static final Font DEFAULT_LABEL_FONT = new Font("Dialog", Font.BOLD,
    088            12);
    089
    090    /**
    091     * The default axis label color (in most circumstances this will be
    092     * overridden by the chart style).
    093     *
    094     * @since 1.2
    095     */
    096    public static final Color DEFAULT_LABEL_COLOR = Color.BLACK;
    097
    098    /**
    099     * The default label offset.
    100     *
    101     * @since 1.2
    102     */
    103    public static final double DEFAULT_LABEL_OFFSET = 10;
    104
    105    /**
    106     * The default tick label font (in most circumstances this will be
    107     * overridden by the chart style).
    108     *
    109     * @since 1.2
    110     */
    111    public static final Font DEFAULT_TICK_LABEL_FONT = new Font("Dialog",
    112            Font.PLAIN, 12);
    113
    114    /**
    115     * The default tick label color (in most circumstances this will be
    116     * overridden by the chart style).
    117     *
    118     * @since 1.2
    119     */
    120    public static final Color DEFAULT_TICK_LABEL_COLOR = Color.BLACK;
    121
    122    /**
    123     * The default stroke for the axis line.
    124     *
    125     * @since 1.2
    126     */
    127    public static final Stroke DEFAULT_LINE_STROKE = new BasicStroke(0f);
    128
    129    /**
    130     * The default color for the axis line.
    131     *
    132     * @since 1.2
    133     */
    134    public static final Color DEFAULT_LINE_COLOR = Color.GRAY;
    135
    136    /** A flag that determines whether or not the axis will be drawn. */
    137    private boolean visible;
    138
    139    /** The axis label (if {@code null}, no label is displayed). */
    140    private String label;
    141
    142    /** The label font (never {@code null}). */
    143    private Font labelFont;
    144
    145    /** The color used to draw the axis label (never {@code null}). */
    146    private Color labelColor;
    147
    148    /** The offset between the tick labels and the label. */
    149    private double labelOffset;
    150
    151    /** The stroke used to draw the axis line. */
    152    private transient Stroke lineStroke;
    153
    154    /** The color used to draw the axis line. */
    155    private Color lineColor;
    156
    157    /** Draw the tick labels? */
    158    private boolean tickLabelsVisible;
    159
    160    /** The font used to display tick labels (never {@code null}) */
    161    private Font tickLabelFont;
    162
    163    /** The tick label paint (never {@code null}). */
    164    private Color tickLabelColor;
    165
    166    /** Storage for registered change listeners. */
    167    private final transient EventListenerList listenerList;
    168
    169    /**
    170     * Creates a new label with the specified label.  If the supplied label
    171     * is {@code null}, the axis will be shown without a label.
    172     *
    173     * @param label  the axis label ({@code null} permitted).
    174     */
    175    public AbstractAxis3D(String label) {
    176        this.visible = true;
    177        this.label = label;
    178        this.labelFont = DEFAULT_LABEL_FONT;
    179        this.labelColor = DEFAULT_LABEL_COLOR;
    180        this.labelOffset = DEFAULT_LABEL_OFFSET;
    181        this.lineStroke = DEFAULT_LINE_STROKE;
    182        this.lineColor = DEFAULT_LINE_COLOR;
    183        this.tickLabelsVisible = true;
    184        this.tickLabelFont = DEFAULT_TICK_LABEL_FONT;
    185        this.tickLabelColor = DEFAULT_TICK_LABEL_COLOR;
    186        this.listenerList = new EventListenerList();
    187    }
    188
    189    /**
    190     * Returns the flag that determines whether or not the axis is drawn
    191     * on the chart.
    192     *
    193     * @return A boolean.
    194     *
    195     * @see #setVisible(boolean)
    196     */
    197    @Override
    198    public boolean isVisible() {
    199        return this.visible;
    200    }
    201
    202    /**
    203     * Sets the flag that determines whether or not the axis is drawn on the
    204     * chart and sends an {@link Axis3DChangeEvent} to all registered listeners.
    205     *
    206     * @param visible  the flag.
    207     *
    208     * @see #isVisible()
    209     */
    210    @Override
    211    public void setVisible(boolean visible) {
    212        this.visible = visible;
    213        fireChangeEvent(false);
    214    }
    215
    216    /**
    217     * Returns the axis label - the text that describes what the axis measures.
    218     * The description should usually specify the units.  When this attribute
    219     * is {@code null}, the axis is drawn without a label.
    220     *
    221     * @return The axis label (possibly {@code null}).
    222     */
    223    public String getLabel() {
    224        return this.label;
    225    }
    226
    227    /**
    228     * Sets the axis label and sends an {@link Axis3DChangeEvent} to all
    229     * registered listeners.  If the supplied label is {@code null},
    230     * the axis will be drawn without a label.
    231     *
    232     * @param label  the label ({@code null} permitted).
    233     */
    234    public void setLabel(String label) {
    235        this.label = label;
    236        fireChangeEvent(false);
    237    }
    238
    239    /**
    240     * Returns the font used to display the main axis label.  The default value
    241     * is {@code Font("SansSerif", Font.BOLD, 12)}.
    242     *
    243     * @return The font used to display the axis label (never {@code null}).
    244     */
    245    @Override
    246    public Font getLabelFont() {
    247        return this.labelFont;
    248    }
    249
    250    /**
    251     * Sets the font used to display the main axis label and sends an
    252     * {@link Axis3DChangeEvent} to all registered listeners.
    253     *
    254     * @param font  the new font ({@code null} not permitted).
    255     */
    256    @Override
    257    public void setLabelFont(Font font) {
    258        ArgChecks.nullNotPermitted(font, "font");
    259        this.labelFont = font;
    260        fireChangeEvent(false);
    261    }
    262
    263    /**
    264     * Returns the color used for the label.  The default value is
    265     * {@code Color.BLACK}.
    266     *
    267     * @return The label paint (never {@code null}).
    268     */
    269    @Override
    270    public Color getLabelColor() {
    271        return this.labelColor;
    272    }
    273
    274    /**
    275     * Sets the color used to draw the axis label and sends an
    276     * {@link Axis3DChangeEvent} to all registered listeners.
    277     *
    278     * @param color  the color ({@code null} not permitted).
    279     */
    280    @Override
    281    public void setLabelColor(Color color) {
    282        ArgChecks.nullNotPermitted(color, "color");
    283        this.labelColor = color;
    284        fireChangeEvent(false);
    285    }
    286
    287    /**
    288     * Returns the offset between the tick labels and the axis label, measured
    289     * in Java2D units.  The default value is {@link #DEFAULT_LABEL_OFFSET}.
    290     *
    291     * @return The offset.
    292     *
    293     * @since 1.2
    294     */
    295    public double getLabelOffset() {
    296        return this.labelOffset;
    297    }
    298
    299    /**
    300     * Sets the offset between the tick labels and the axis label and sends
    301     * an {@link Axis3DChangeEvent} to all registered listeners.
    302     *
    303     * @param offset  the offset.
    304     *
    305     * @since 1.2
    306     */
    307    public void setLabelOffset(double offset) {
    308        this.labelOffset = offset;
    309        fireChangeEvent(false);
    310    }
    311
    312    /**
    313     * Returns the stroke used to draw the axis line.  The default value is
    314     * {@link #DEFAULT_LINE_STROKE}.
    315     *
    316     * @return The stroke used to draw the axis line (never {@code null}).
    317     */
    318    public Stroke getLineStroke() {
    319        return this.lineStroke;
    320    }
    321
    322    /**
    323     * Sets the stroke used to draw the axis line and sends an
    324     * {@link Axis3DChangeEvent} to all registered listeners.
    325     *
    326     * @param stroke  the new stroke ({@code null} not permitted).
    327     */
    328    public void setLineStroke(Stroke stroke) {
    329        ArgChecks.nullNotPermitted(stroke, "stroke");
    330        this.lineStroke = stroke;
    331        fireChangeEvent(false);
    332    }
    333
    334    /**
    335     * Returns the color used to draw the axis line.  The default value is
    336     * {@link #DEFAULT_LINE_COLOR}.
    337     *
    338     * @return The color used to draw the axis line (never {@code null}).
    339     */
    340    public Color getLineColor() {
    341        return this.lineColor;
    342    }
    343
    344    /**
    345     * Sets the color used to draw the axis line and sends an
    346     * {@link Axis3DChangeEvent} to all registered listeners.
    347     *
    348     * @param color  the new color ({@code null} not permitted).
    349     */
    350    public void setLineColor(Color color) {
    351        ArgChecks.nullNotPermitted(color, "color");
    352        this.lineColor = color;
    353        fireChangeEvent(false);
    354    }
    355
    356    /**
    357     * Returns the flag that controls whether or not the tick labels are
    358     * drawn.  The default value is {@code true}.
    359     *
    360     * @return A boolean.
    361     */
    362    public boolean getTickLabelsVisible() {
    363        return this.tickLabelsVisible;
    364    }
    365
    366    /**
    367     * Sets the flag that controls whether or not the tick labels are drawn,
    368     * and sends a change event to all registered listeners.  You should think
    369     * carefully before setting this flag to {@code false}, because if
    370     * the tick labels are not shown it will be hard for the reader to
    371     * understand the resulting chart.
    372     *
    373     * @param visible  visible?
    374     */
    375    public void setTickLabelsVisible(boolean visible) {
    376        this.tickLabelsVisible = visible;
    377        fireChangeEvent(false);
    378    }
    379
    380    /**
    381     * Returns the font used to display the tick labels.  The default value
    382     * is {@link #DEFAULT_TICK_LABEL_FONT}.
    383     *
    384     * @return The font (never {@code null}).
    385     */
    386    @Override
    387    public Font getTickLabelFont() {
    388        return this.tickLabelFont;
    389    }
    390
    391    /**
    392     * Sets the font used to display tick labels and sends an
    393     * {@link Axis3DChangeEvent} to all registered listeners.
    394     *
    395     * @param font  the font ({@code null} not permitted).
    396     */
    397    @Override
    398    public void setTickLabelFont(Font font) {
    399        ArgChecks.nullNotPermitted(font, "font");
    400        this.tickLabelFont = font;
    401        fireChangeEvent(false);
    402    }
    403
    404    /**
    405     * Returns the foreground color for the tick labels.  The default value
    406     * is {@link #DEFAULT_LABEL_COLOR}.
    407     *
    408     * @return The foreground color (never {@code null}).
    409     */
    410    @Override
    411    public Color getTickLabelColor() {
    412        return this.tickLabelColor;
    413    }
    414
    415    /**
    416     * Sets the foreground color for the tick labels and sends an
    417     * {@link Axis3DChangeEvent} to all registered listeners.
    418     *
    419     * @param color  the color ({@code null} not permitted).
    420     */
    421    @Override
    422    public void setTickLabelColor(Color color) {
    423        ArgChecks.nullNotPermitted(color, "color");
    424        this.tickLabelColor = color;
    425        fireChangeEvent(false);
    426    }
    427
    428    /**
    429     * Receives a {@link ChartElementVisitor}.  This method is part of a general
    430     * mechanism for traversing the chart structure and performing operations
    431     * on each element in the chart.  You will not normally call this method
    432     * directly.
    433     *
    434     * @param visitor  the visitor ({@code null} not permitted).
    435     *
    436     * @since 1.2
    437     */
    438    @Override
    439    public abstract void receive(ChartElementVisitor visitor);
    440
    441    /**
    442     * Draws the specified text as the axis label and returns a bounding
    443     * shape (2D) for the text.
    444     *
    445     * @param label  the label ({@code null} not permitted).
    446     * @param g2  the graphics target ({@code null} not permitted).
    447     * @param axisLine  the axis line ({@code null} not permitted).
    448     * @param opposingPt  an opposing point ({@code null} not permitted).
    449     * @param offset  the offset.
    450     * @param info  collects rendering info ({@code null} permitted).
    451     * @param hinting  perform element hinting?
    452     *
    453     * @return A bounding shape.
    454     */
    455    protected Shape drawAxisLabel(String label, Graphics2D g2,
    456            Line2D axisLine, Point2D opposingPt, double offset,
    457            RenderingInfo info, boolean hinting) {
    458        ArgChecks.nullNotPermitted(label, "label");
    459        ArgChecks.nullNotPermitted(g2, "g2");
    460        ArgChecks.nullNotPermitted(axisLine, "axisLine");
    461        ArgChecks.nullNotPermitted(opposingPt, "opposingPt");
    462        g2.setFont(getLabelFont());
    463        g2.setPaint(getLabelColor());
    464        Line2D labelPosLine = Utils2D.createPerpendicularLine(axisLine, 0.5,
    465                offset, opposingPt);
    466        double theta = Utils2D.calculateTheta(axisLine);
    467        if (theta < -Math.PI / 2.0) {
    468            theta = theta + Math.PI;
    469        }
    470        if (theta > Math.PI / 2.0) {
    471            theta = theta - Math.PI;
    472        }
    473
    474        if (hinting) {
    475            Map m = new HashMap<String, String>();
    476            m.put("ref", "{\"type\": \"axisLabel\", \"axis\": \"" + axisStr()
    477                    + "\", \"label\": \"" + getLabel() + "\"}");
    478            g2.setRenderingHint(Chart3DHints.KEY_BEGIN_ELEMENT, m);
    479        }
    480        Shape bounds = TextUtils.drawRotatedString(getLabel(), g2,
    481                (float) labelPosLine.getX2(), (float) labelPosLine.getY2(),
    482                TextAnchor.CENTER, theta, TextAnchor.CENTER);
    483        if (hinting) {
    484            g2.setRenderingHint(Chart3DHints.KEY_END_ELEMENT, true);
    485        }
    486        if (info != null) {
    487            RenderedElement labelElement = new RenderedElement(
    488                    InteractiveElementType.AXIS_LABEL, bounds);
    489            labelElement.setProperty("axis", axisStr());
    490            labelElement.setProperty("label", getLabel());
    491            info.addOffsetElement(labelElement);
    492        }
    493        return bounds;
    494    }
    495
    496    /**
    497     * Returns a string representing the configured type of the axis ("row",
    498     * "column", "value", "x", "y" or "z" - other values may be possible in the
    499     * future).  A <em>row</em> axis on a {@link CategoryPlot3D} is in the
    500     * position of a z-axis (depth), a <em>column</em> axis is in the position
    501     * of an x-axis (width), a <em>value</em> axis is in the position of a
    502     * y-axis (height).
    503     *
    504     * @return A string (never {@code null}).
    505     *
    506     * @since 1.3
    507     */
    508    protected abstract String axisStr();
    509
    510    /**
    511     * Draws the axis along an arbitrary line (between {@code startPt}
    512     * and {@code endPt}).  The opposing point is used as a reference
    513     * point to know on which side of the axis to draw the labels.
    514     *
    515     * @param g2  the graphics target ({@code null} not permitted).
    516     * @param startPt  the starting point ({@code null} not permitted).
    517     * @param endPt  the end point ({@code null} not permitted)
    518     * @param opposingPt  an opposing point ({@code null} not permitted).
    519     * @param tickData  info about the ticks to draw ({@code null} not
    520     *     permitted).
    521     * @param info  an object to be populated with rendering info
    522     *     ({@code null} permitted).
    523     * @param hinting  a flag that controls whether or not element hinting
    524     *     should be performed.
    525     */
    526    @Override
    527    public abstract void draw(Graphics2D g2, Point2D startPt, Point2D endPt,
    528            Point2D opposingPt, List<TickData> tickData, RenderingInfo info,
    529            boolean hinting);
    530
    531    /**
    532     * Tests this instance for equality with an arbitrary object.
    533     *
    534     * @param obj  the object to test against ({@code null} permitted).
    535     *
    536     * @return A boolean.
    537     */
    538    @Override
    539    public boolean equals(Object obj) {
    540        if (obj == this) {
    541            return true;
    542        }
    543        if (!(obj instanceof AbstractAxis3D)) {
    544            return false;
    545        }
    546        AbstractAxis3D that = (AbstractAxis3D) obj;
    547        if (this.visible != that.visible) {
    548            return false;
    549        }
    550        if (!ObjectUtils.equals(this.label, that.label)) {
    551            return false;
    552        }
    553        if (!this.labelFont.equals(that.labelFont)) {
    554            return false;
    555        }
    556        if (!this.labelColor.equals(that.labelColor)) {
    557            return false;
    558        }
    559        if (!this.lineStroke.equals(that.lineStroke)) {
    560            return false;
    561        }
    562        if (!this.lineColor.equals(that.lineColor)) {
    563            return false;
    564        }
    565        if (this.tickLabelsVisible != that.tickLabelsVisible) {
    566            return false;
    567        }
    568        if (!this.tickLabelFont.equals(that.tickLabelFont)) {
    569            return false;
    570        }
    571        if (!this.tickLabelColor.equals(that.tickLabelColor)) {
    572            return false;
    573        }
    574        return true;
    575    }
    576
    577    /**
    578     * Returns a hash code for this instance.
    579     *
    580     * @return A hash code.
    581     */
    582    @Override
    583    public int hashCode() {
    584        int hash = 5;
    585        hash = 83 * hash + (this.visible ? 1 : 0);
    586        hash = 83 * hash + ObjectUtils.hashCode(this.label);
    587        hash = 83 * hash + ObjectUtils.hashCode(this.labelFont);
    588        hash = 83 * hash + ObjectUtils.hashCode(this.labelColor);
    589        hash = 83 * hash + ObjectUtils.hashCode(this.lineStroke);
    590        hash = 83 * hash + ObjectUtils.hashCode(this.lineColor);
    591        hash = 83 * hash + (this.tickLabelsVisible ? 1 : 0);
    592        hash = 83 * hash + ObjectUtils.hashCode(this.tickLabelFont);
    593        hash = 83 * hash + ObjectUtils.hashCode(this.tickLabelColor);
    594        return hash;
    595    }
    596
    597    /**
    598     * Registers a listener so that it will receive axis change events.
    599     *
    600     * @param listener  the listener ({@code null} not permitted).
    601     */
    602    @Override
    603    public void addChangeListener(Axis3DChangeListener listener) {
    604        this.listenerList.add(Axis3DChangeListener.class, listener);
    605    }
    606
    607    /**
    608     * Deregisters a listener so that it will no longer receive axis
    609     * change events.
    610     *
    611     * @param listener  the listener ({@code null} not permitted).
    612     */
    613    @Override
    614    public void removeChangeListener(Axis3DChangeListener listener) {
    615        this.listenerList.remove(Axis3DChangeListener.class, listener);
    616    }
    617
    618    /**
    619     * Notifies all registered listeners that the plot has been modified.
    620     *
    621     * @param event  information about the change event.
    622     */
    623    public void notifyListeners(Axis3DChangeEvent event) {
    624        Object[] listeners = this.listenerList.getListenerList();
    625        for (int i = listeners.length - 2; i >= 0; i -= 2) {
    626            if (listeners[i] == Axis3DChangeListener.class) {
    627                ((Axis3DChangeListener) listeners[i + 1]).axisChanged(event);
    628            }
    629        }
    630    }
    631
    632    /**
    633     * Sends an {@link Axis3DChangeEvent} to all registered listeners.
    634     *
    635     * @param requiresWorldUpdate   a flag indicating whether or not this change
    636     *     requires the 3D world to be updated.
    637     */
    638    protected void fireChangeEvent(boolean requiresWorldUpdate) {
    639        notifyListeners(new Axis3DChangeEvent(this, requiresWorldUpdate));
    640    }
    641
    642    /**
    643     * Receives notification of a change to a marker managed by this axis - the
    644     * response is to fire a change event for the axis (to eventually trigger
    645     * a repaint of the chart).  Marker changes don't require the world model
    646     * to be updated.
    647     *
    648     * @param event  the event.
    649     *
    650     * @since 1.2
    651     */
    652    @Override
    653    public void markerChanged(MarkerChangeEvent event) {
    654        fireChangeEvent(false);
    655    }
    656
    657    /**
    658     * Provides serialization support.
    659     *
    660     * @param stream  the output stream.
    661     *
    662     * @throws IOException  if there is an I/O error.
    663     */
    664    private void writeObject(ObjectOutputStream stream) throws IOException {
    665        stream.defaultWriteObject();
    666        SerialUtils.writeStroke(this.lineStroke, stream);
    667    }
    668
    669    /**
    670     * Provides serialization support.
    671     *
    672     * @param stream  the input stream.
    673     *
    674     * @throws IOException  if there is an I/O error.
    675     * @throws ClassNotFoundException  if there is a classpath problem.
    676     */
    677    private void readObject(ObjectInputStream stream)
    678        throws IOException, ClassNotFoundException {
    679        stream.defaultReadObject();
    680        this.lineStroke = SerialUtils.readStroke(stream);
    681    }
    682
    683}