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}