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.marker;
034
035import java.awt.FontMetrics;
036import java.awt.Graphics2D;
037import java.awt.geom.Line2D;
038import java.awt.geom.Rectangle2D;
039import javax.swing.event.EventListenerList;
040import org.jfree.chart3d.Chart3DChangeListener;
041import org.jfree.chart3d.ChartElementVisitor;
042import org.jfree.chart3d.graphics2d.Anchor2D;
043import org.jfree.chart3d.graphics2d.RefPt2D;
044import org.jfree.chart3d.graphics2d.TextAnchor;
045import org.jfree.chart3d.graphics3d.internal.Utils2D;
046import org.jfree.chart3d.internal.Args;
047import org.jfree.chart3d.internal.TextUtils;
048
049/**
050 * A base class for implementing markers (includes the event notification 
051 * mechanism).
052 * 
053 * @since 1.2
054 */
055public abstract class AbstractMarker implements Marker {
056    
057    /** Storage for registered change listeners. */
058    private final transient EventListenerList listenerList;
059    
060    /**
061     * Default constructor.
062     */
063    AbstractMarker() {
064        this.listenerList = new EventListenerList();
065    }
066
067    /**
068     * Draws a marker label.
069     * 
070     * @param g2  the graphics target ({@code null} not permitted).
071     * @param label  the label.
072     * @param x  the x-coordinate for the anchor point.
073     * @param y  the y-cpordinate for the anchor point.
074     * @param anchor  the label anchor ({@code null} not permitted).
075     * @param refLine  a reference line that is used to determine the rotation 
076     *     angle for the label ({@code null} not permitted).
077     * @param reverse  a flag to indicate reverse orientation.
078     */
079    protected void drawMarkerLabel(Graphics2D g2, String label, 
080            double x, double y, Anchor2D anchor, Line2D refLine, 
081            boolean reverse) {
082        double angle = Utils2D.calculateTheta(refLine);
083        boolean vflip = false;
084        if (angle > Math.PI / 2) {
085            angle -= Math.PI;
086            vflip = true;
087        }
088        if (angle < -Math.PI / 2) {
089            angle += Math.PI;
090            vflip = true;
091        }
092        if (reverse) {
093            vflip = !vflip;
094        }
095        double lineLength = Utils2D.length(refLine);
096        FontMetrics fm = g2.getFontMetrics();
097        Rectangle2D bounds = fm.getStringBounds(label, g2);
098        if (bounds.getWidth() < lineLength) {
099            TextAnchor textAnchor = deriveTextAnchorForLine(anchor.getRefPt(), 
100                    !vflip);
101            TextUtils.drawRotatedString(label, g2, 
102                    (float) x, (float) y, textAnchor, angle, textAnchor);
103        }
104    }
105    
106    /**
107     * Draws a marker label.
108     * 
109     * @param g2  the graphics target ({@code null} not permitted).
110     * @param label  the label.
111     * @param x  the x-coordinate for the anchor point.
112     * @param y  the y-cpordinate for the anchor point.
113     * @param anchor  the label anchor ({@code null} not permitted).
114     * @param refLine1  a reference line that is used to determine the rotation 
115     *     angle for the label ({@code null} not permitted).
116     * @param refLine2  a reference line that is used to determine the rotation 
117     *     angle for the label ({@code null} not permitted).
118     * @param reverse  a flag to indicate reverse orientation.
119     */
120    protected void drawMarkerLabel(Graphics2D g2, String label, 
121            double x, double y, Anchor2D anchor, Line2D refLine1, 
122            Line2D refLine2, boolean reverse) {
123        double angle;
124        if (anchor.getRefPt().isTop()) {
125            angle = Utils2D.calculateTheta(refLine2);
126        } else if (anchor.getRefPt().isBottom()) {
127            angle = Utils2D.calculateTheta(refLine1);
128        } else {
129            angle = (Utils2D.calculateTheta(refLine1) 
130                    + Utils2D.calculateTheta(refLine2)) / 2.0;
131        }
132        boolean vflip = false;
133        if (angle > Math.PI / 2) {
134            angle -= Math.PI;
135            vflip = true;
136        }
137        if (angle < -Math.PI / 2) {
138            angle += Math.PI;
139            vflip = true;
140        }
141        if (reverse) {
142            vflip = !vflip;
143        }
144        double lineLength1 = Utils2D.length(refLine1);
145        double lineLength2 = Utils2D.length(refLine2);
146        Rectangle2D bounds = g2.getFontMetrics().getStringBounds(label, g2);
147        if (bounds.getWidth() < Math.min(lineLength1, lineLength2)) {
148            TextAnchor textAnchor = deriveTextAnchor(anchor.getRefPt(), !vflip);
149            TextUtils.drawRotatedString(label, g2, (float) x, (float) y, 
150                textAnchor, angle, textAnchor);
151        }        
152    }
153 
154    /**
155     * Receives a visitor.
156     * 
157     * @param visitor  the visitor.
158     * 
159     * @since 1.2
160     */
161    @Override
162    public void receive(ChartElementVisitor visitor) {
163        visitor.visit(this);
164    }
165 
166    /**
167     * Registers a listener to receive notification of changes to the marker.
168     * 
169     * @param listener  the listener ({@code null} not permitted). 
170     */
171    @Override
172    public void addChangeListener(MarkerChangeListener listener) {
173        this.listenerList.add(MarkerChangeListener.class, listener);
174    }
175    
176    /**
177     * Deregisters a listener so that it no longer receives notification of 
178     * changes to the marker.
179     * 
180     * @param listener  the listener ({@code null} not permitted). 
181     */
182    @Override
183    public void removeChangeListener(MarkerChangeListener listener) {
184        this.listenerList.remove(MarkerChangeListener.class, listener);        
185    }
186    
187    /**
188     * Sends a {@link MarkerChangeEvent} to all registered listeners.
189     */
190    protected void fireChangeEvent() {
191        Object[] listeners = this.listenerList.getListenerList();
192        for (int i = listeners.length - 2; i >= 0; i -= 2) {
193            if (listeners[i] == Chart3DChangeListener.class) { 
194                ((MarkerChangeListener) listeners[i + 1]).markerChanged(
195                        new MarkerChangeEvent(this, this));
196            }
197        }
198    }
199    
200    /**
201     * A utility method that returns a suitable text anchor for a given
202     * reference point. This is used for range marker label positioning.
203     * 
204     * @param refPt  the reference point ({@code null} not permitted).
205     * @param vflip  is the text flipped vertically?
206     * 
207     * @return A text anchor (never {@code null}). 
208     */
209    protected static TextAnchor deriveTextAnchor(RefPt2D refPt, boolean vflip) {
210        Args.nullNotPermitted(refPt, "refPt");
211        if (refPt.equals(RefPt2D.TOP_LEFT)) {
212            return vflip ? TextAnchor.TOP_LEFT : TextAnchor.BOTTOM_RIGHT;
213        } else if (refPt.equals(RefPt2D.TOP_CENTER)) {
214            return vflip ? TextAnchor.TOP_CENTER : TextAnchor.BOTTOM_CENTER;
215        } else if (refPt.equals(RefPt2D.TOP_RIGHT)) {
216            return vflip ? TextAnchor.TOP_RIGHT :TextAnchor.BOTTOM_LEFT;
217        } if (refPt.equals(RefPt2D.CENTER_LEFT)) {
218            return vflip ? TextAnchor.CENTER_LEFT : TextAnchor.CENTER_RIGHT;
219        } else if (refPt.equals(RefPt2D.CENTER)) {
220            return TextAnchor.CENTER;
221        } else if (refPt.equals(RefPt2D.CENTER_RIGHT)) {
222            return vflip ? TextAnchor.CENTER_RIGHT : TextAnchor.CENTER_LEFT;
223        } else if (refPt.equals(RefPt2D.BOTTOM_LEFT)) {
224            return vflip ? TextAnchor.BOTTOM_LEFT : TextAnchor.TOP_RIGHT;
225        } else if (refPt.equals(RefPt2D.BOTTOM_CENTER)) {
226            return vflip ? TextAnchor.BOTTOM_CENTER : TextAnchor.TOP_CENTER;
227        } else if (refPt.equals(RefPt2D.BOTTOM_RIGHT)) {
228            return vflip ? TextAnchor.BOTTOM_RIGHT : TextAnchor.TOP_LEFT;
229        }
230        throw new RuntimeException("Unknown refPt " + refPt);
231    }
232
233    /**
234     * A utility method that returns a suitable text anchor for a given
235     * reference point relative to a line (rather than a rectangle which is
236     * the normal case).  This is used for value marker label positioning.
237     * 
238     * @param refPt  the reference point ({@code null} not permitted).
239     * @param vflip  is the text flipped vertically?
240     * 
241     * @return A text anchor (never {@code null}). 
242     */
243    protected static TextAnchor deriveTextAnchorForLine(RefPt2D refPt, 
244            boolean vflip) {
245        if (refPt.equals(RefPt2D.TOP_LEFT)) {
246            return vflip ? TextAnchor.BOTTOM_LEFT : TextAnchor.TOP_RIGHT;
247        } else if (refPt.equals(RefPt2D.TOP_CENTER)) {
248            return vflip ? TextAnchor.BOTTOM_CENTER : TextAnchor.TOP_CENTER;
249        } else if (refPt.equals(RefPt2D.TOP_RIGHT)) {
250            return vflip ? TextAnchor.BOTTOM_RIGHT :TextAnchor.TOP_LEFT;
251        } if (refPt.equals(RefPt2D.CENTER_LEFT)) {
252            return vflip ? TextAnchor.CENTER_LEFT : TextAnchor.CENTER_RIGHT;
253        } else if (refPt.equals(RefPt2D.CENTER)) {
254            return TextAnchor.CENTER;
255        } else if (refPt.equals(RefPt2D.CENTER_RIGHT)) {
256            return vflip ? TextAnchor.CENTER_RIGHT : TextAnchor.CENTER_LEFT;
257        } else if (refPt.equals(RefPt2D.BOTTOM_LEFT)) {
258            return vflip ? TextAnchor.TOP_LEFT : TextAnchor.BOTTOM_RIGHT;
259        } else if (refPt.equals(RefPt2D.BOTTOM_CENTER)) {
260            return vflip ? TextAnchor.TOP_CENTER : TextAnchor.BOTTOM_CENTER;
261        } else if (refPt.equals(RefPt2D.BOTTOM_RIGHT)) {
262            return vflip ? TextAnchor.TOP_RIGHT : TextAnchor.BOTTOM_LEFT;
263        }
264        throw new RuntimeException("Unknown refPt " + refPt);
265    }
266
267}