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<String, String> 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}