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