    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}