    001    /* ===========================================================
    002     * JFreeChart : a free chart library for the Java(tm) platform
    003     * ===========================================================
    004     *
    005     * (C) Copyright 2000-2005, by Object Refinery Limited and Contributors.
    006     *
    007     * Project Info:  http://www.jfree.org/jfreechart/index.html
    008     *
    009     * This library is free software; you can redistribute it and/or modify it
    010     * under the terms of the GNU Lesser General Public License as published by
    011     * the Free Software Foundation; either version 2.1 of the License, or
    012     * (at your option) any later version.
    013     *
    014     * This library is distributed in the hope that it will be useful, but
    015     * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
    016     * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
    017     * License for more details.
    018     *
    019     * You should have received a copy of the GNU Lesser General Public
    020     * License along with this library; if not, write to the Free Software
    021     * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
    022     * USA.
    023     *
    024     * [Java is a trademark or registered trademark of Sun Microsystems, Inc.
    025     * in the United States and other countries.]
    026     *
    027     * ---------------
    028     * ModuloAxis.java
    029     * ---------------
    030     * (C) Copyright 2004, by Object Refinery Limited.
    031     *
    032     * Original Author:  David Gilbert (for Object Refinery Limited);
    033     * Contributor(s):   -;
    034     *
    035     * $Id: ModuloAxis.java,v 1.3.2.1 2005/10/25 20:37:34 mungady Exp $
    036     *
    037     * Changes
    038     * -------
    039     * 13-Aug-2004 : Version 1 (DG);
    040     *
    041     */
    042
    043    package org.jfree.chart.axis;
    044
    045    import java.awt.geom.Rectangle2D;
    046
    047    import org.jfree.chart.event.AxisChangeEvent;
    048    import org.jfree.data.Range;
    049    import org.jfree.ui.RectangleEdge;
    050
    051    /**
    052     * An axis that displays numerical values within a fixed range using a modulo
    053     * calculation.
    054     */
    055    public class ModuloAxis extends NumberAxis {
    056
    057        /**
    058         * The fixed range for the axis - all data values will be mapped to this
    059         * range using a modulo calculation.
    060         */
    061        private Range fixedRange;
    062
    063        /**
    064         * The display start value (this will sometimes be > displayEnd, in which
    065         * case the axis wraps around at some point in the middle of the axis).
    066         */
    067        private double displayStart;
    068
    069        /**
    070         * The display end value.
    071         */
    072        private double displayEnd;
    073
    074        /**
    075         * Creates a new axis.
    076         *
    077         * @param label  the axis label (<code>null</code> permitted).
    078         * @param fixedRange  the fixed range (<code>null</code> not permitted).
    079         */
    080        public ModuloAxis(String label, Range fixedRange) {
    081            super(label);
    082            this.fixedRange = fixedRange;
    083            this.displayStart = 270.0;
    084            this.displayEnd = 90.0;
    085        }
    086
    087        /**
    088         * Returns the display start value.
    089         *
    090         * @return The display start value.
    091         */
    092        public double getDisplayStart() {
    093            return this.displayStart;
    094        }
    095
    096        /**
    097         * Returns the display end value.
    098         *
    099         * @return The display end value.
    100         */
    101        public double getDisplayEnd() {
    102            return this.displayEnd;
    103        }
    104
    105        /**
    106         * Sets the display range.  The values will be mapped to the fixed range if
    107         * necessary.
    108         *
    109         * @param start  the start value.
    110         * @param end  the end value.
    111         */
    112        public void setDisplayRange(double start, double end) {
    113            this.displayStart = mapValueToFixedRange(start);
    114            this.displayEnd = mapValueToFixedRange(end);
    115            if (this.displayStart < this.displayEnd) {
    116                setRange(this.displayStart, this.displayEnd);
    117            }
    118            else {
    119                setRange(
    120                    this.displayStart,
    121                    this.fixedRange.getUpperBound()
    122                      + (this.displayEnd - this.fixedRange.getLowerBound())
    123                );
    124            }
    125            notifyListeners(new AxisChangeEvent(this));
    126        }
    127
    128        /**
    129         * This method should calculate a range that will show all the data values.
    130         * For now, it just sets the axis range to the fixedRange.
    131         */
    132        protected void autoAdjustRange() {
    133            setRange(this.fixedRange, false, false);
    134        }
    135
    136        /**
    137         * Translates a data value to a Java2D coordinate.
    138         *
    139         * @param value  the value.
    140         * @param area  the area.
    141         * @param edge  the edge.
    142         *
    143         * @return A Java2D coordinate.
    144         */
    145        public double valueToJava2D(double value, Rectangle2D area,
    146                                    RectangleEdge edge) {
    147            double result = 0.0;
    148            double v = mapValueToFixedRange(value);
    149            if (this.displayStart < this.displayEnd) {  // regular number axis
    150                result = trans(v, area, edge);
    151            }
    152            else {  // displayStart > displayEnd, need to handle split
    153                double cutoff = (this.displayStart + this.displayEnd) / 2.0;
    154                double length1 = this.fixedRange.getUpperBound()
    155                                 - this.displayStart;
    156                double length2 = this.displayEnd - this.fixedRange.getLowerBound();
    157                if (v > cutoff) {
    158                    result = transStart(v, area, edge, length1, length2);
    159                }
    160                else {
    161                    result = transEnd(v, area, edge, length1, length2);
    162                }
    163            }
    164            return result;
    165        }
    166
    167        /**
    168         * A regular translation from a data value to a Java2D value.
    169         *
    170         * @param value  the value.
    171         * @param area  the data area.
    172         * @param edge  the edge along which the axis lies.
    173         *
    174         * @return The Java2D coordinate.
    175         */
    176        private double trans(double value, Rectangle2D area, RectangleEdge edge) {
    177            double min = 0.0;
    178            double max = 0.0;
    179            if (RectangleEdge.isTopOrBottom(edge)) {
    180                min = area.getX();
    181                max = area.getX() + area.getWidth();
    182            }
    183            else if (RectangleEdge.isLeftOrRight(edge)) {
    184                min = area.getMaxY();
    185                max = area.getMaxY() - area.getHeight();
    186            }
    187            if (isInverted()) {
    188                return max - ((value - this.displayStart)
    189                       / (this.displayEnd - this.displayStart)) * (max - min);
    190            }
    191            else {
    192                return min + ((value - this.displayStart)
    193                       / (this.displayEnd - this.displayStart)) * (max - min);
    194            }
    195
    196        }
    197
    198        /**
    199         * Translates a data value to a Java2D value for the first section of the
    200         * axis.
    201         *
    202         * @param value  the value.
    203         * @param area  the data area.
    204         * @param edge  the edge along which the axis lies.
    205         * @param length1  the length of the first section.
    206         * @param length2  the length of the second section.
    207         *
    208         * @return The Java2D coordinate.
    209         */
    210        private double transStart(double value, Rectangle2D area,
    211                                  RectangleEdge edge,
    212                                  double length1, double length2) {
    213            double min = 0.0;
    214            double max = 0.0;
    215            if (RectangleEdge.isTopOrBottom(edge)) {
    216                min = area.getX();
    217                max = area.getX() + area.getWidth() * length1 / (length1 + length2);
    218            }
    219            else if (RectangleEdge.isLeftOrRight(edge)) {
    220                min = area.getMaxY();
    221                max = area.getMaxY() - area.getHeight() * length1
    222                      / (length1 + length2);
    223            }
    224            if (isInverted()) {
    225                return max - ((value - this.displayStart)
    226                    / (this.fixedRange.getUpperBound() - this.displayStart))
    227                    * (max - min);
    228            }
    229            else {
    230                return min + ((value - this.displayStart)
    231                    / (this.fixedRange.getUpperBound() - this.displayStart))
    232                    * (max - min);
    233            }
    234
    235        }
    236
    237        /**
    238         * Translates a data value to a Java2D value for the second section of the
    239         * axis.
    240         *
    241         * @param value  the value.
    242         * @param area  the data area.
    243         * @param edge  the edge along which the axis lies.
    244         * @param length1  the length of the first section.
    245         * @param length2  the length of the second section.
    246         *
    247         * @return The Java2D coordinate.
    248         */
    249        private double transEnd(double value, Rectangle2D area, RectangleEdge edge,
    250                                double length1, double length2) {
    251            double min = 0.0;
    252            double max = 0.0;
    253            if (RectangleEdge.isTopOrBottom(edge)) {
    254                max = area.getMaxX();
    255                min = area.getMaxX() - area.getWidth() * length2
    256                      / (length1 + length2);
    257            }
    258            else if (RectangleEdge.isLeftOrRight(edge)) {
    259                max = area.getMinY();
    260                min = area.getMinY() + area.getHeight() * length2
    261                      / (length1 + length2);
    262            }
    263            if (isInverted()) {
    264                return max - ((value - this.fixedRange.getLowerBound())
    265                        / (this.displayEnd - this.fixedRange.getLowerBound()))
    266                        * (max - min);
    267            }
    268            else {
    269                return min + ((value - this.fixedRange.getLowerBound())
    270                        / (this.displayEnd - this.fixedRange.getLowerBound()))
    271                        * (max - min);
    272            }
    273
    274        }
    275
    276        /**
    277         * Maps a data value into the fixed range.
    278         *
    279         * @param value  the value.
    280         *
    281         * @return The mapped value.
    282         */
    283        private double mapValueToFixedRange(double value) {
    284            double lower = this.fixedRange.getLowerBound();
    285            double length = this.fixedRange.getLength();
    286            if (value < lower) {
    287                return lower + length + ((value - lower) % length);
    288            }
    289            else {
    290                return lower + ((value - lower) % length);
    291            }
    292        }
    293
    294        /**
    295         * Translates a Java2D coordinate into a data value.
    296         *
    297         * @param java2DValue  the Java2D coordinate.
    298         * @param area  the area.
    299         * @param edge  the edge.
    300         *
    301         * @return The Java2D coordinate.
    302         */
    303        public double java2DToValue(double java2DValue, Rectangle2D area,
    304                                    RectangleEdge edge) {
    305            double result = 0.0;
    306            if (this.displayStart < this.displayEnd) {  // regular number axis
    307                result = super.java2DToValue(java2DValue, area, edge);
    308            }
    309            else {  // displayStart > displayEnd, need to handle split
    310
    311            }
    312            return result;
    313        }
    314
    315        /**
    316         * Returns the display length for the axis.
    317         *
    318         * @return The display length.
    319         */
    320        private double getDisplayLength() {
    321            if (this.displayStart < this.displayEnd) {
    322                return (this.displayEnd - this.displayStart);
    323            }
    324            else {
    325                return (this.fixedRange.getUpperBound() - this.displayStart)
    326                    + (this.displayEnd - this.fixedRange.getLowerBound());
    327            }
    328        }
    329
    330        /**
    331         * Returns the central value of the current display range.
    332         *
    333         * @return The central value.
    334         */
    335        private double getDisplayCentralValue() {
    336            return mapValueToFixedRange(
    337                this.displayStart + (getDisplayLength() / 2)
    338            );
    339        }
    340
    341        /**
    342         * Increases or decreases the axis range by the specified percentage about
    343         * the central value and sends an {@link AxisChangeEvent} to all registered
    344         * listeners.
    345         * <P>
    346         * To double the length of the axis range, use 200% (2.0).
    347         * To halve the length of the axis range, use 50% (0.5).
    348         *
    349         * @param percent  the resize factor.
    350         */
    351        public void resizeRange(double percent) {
    352            resizeRange(percent, getDisplayCentralValue());
    353        }
    354
    355        /**
    356         * Increases or decreases the axis range by the specified percentage about
    357         * the specified anchor value and sends an {@link AxisChangeEvent} to all
    358         * registered listeners.
    359         * <P>
    360         * To double the length of the axis range, use 200% (2.0).
    361         * To halve the length of the axis range, use 50% (0.5).
    362         *
    363         * @param percent  the resize factor.
    364         * @param anchorValue  the new central value after the resize.
    365         */
    366        public void resizeRange(double percent, double anchorValue) {
    367
    368            if (percent > 0.0) {
    369                double halfLength = getDisplayLength() * percent / 2;
    370                setDisplayRange(anchorValue - halfLength, anchorValue + halfLength);
    371            }
    372            else {
    373                setAutoRange(true);
    374            }
    375
    376        }
    377
    378        /**
    379         * Converts a length in data coordinates into the corresponding length in
    380         * Java2D coordinates.
    381         *
    382         * @param length  the length.
    383         * @param area  the plot area.
    384         * @param edge  the edge along which the axis lies.
    385         *
    386         * @return The length in Java2D coordinates.
    387         */
    388        public double lengthToJava2D(double length, Rectangle2D area,
    389                                     RectangleEdge edge) {
    390            double axisLength = 0.0;
    391            if (this.displayEnd > this.displayStart) {
    392                axisLength = this.displayEnd - this.displayStart;
    393            }
    394            else {
    395                axisLength = (this.fixedRange.getUpperBound() - this.displayStart)
    396                    + (this.displayEnd - this.fixedRange.getLowerBound());
    397            }
    398            double areaLength = 0.0;
    399            if (RectangleEdge.isLeftOrRight(edge)) {
    400                areaLength = area.getHeight();
    401            }
    402            else {
    403                areaLength = area.getWidth();
    404            }
    405            return (length / axisLength) * areaLength;
    406        }
    407
    408    }