Spec-Zone .ru
спецификации, руководства, описания, API
001/*
002 * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved.
003 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
004 *
005 * This code is free software; you can redistribute it and/or modify it
006 * under the terms of the GNU General Public License version 2 only, as
007 * published by the Free Software Foundation.  Oracle designates this
008 * particular file as subject to the "Classpath" exception as provided
009 * by Oracle in the LICENSE file that accompanied this code.
010 *
011 * This code is distributed in the hope that it will be useful, but WITHOUT
012 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
013 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
014 * version 2 for more details (a copy is included in the LICENSE file that
015 * accompanied this code).
016 *
017 * You should have received a copy of the GNU General Public License version
018 * 2 along with this work; if not, write to the Free Software Foundation,
019 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
020 *
021 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
022 * or visit www.oracle.com if you need additional information or have any
023 * questions.
024 */
025
026package javafx.scene.chart;
027
028import java.util.ArrayList;
029import java.util.List;
030
031import javafx.animation.KeyFrame;
032import javafx.animation.KeyValue;
033import javafx.beans.property.BooleanProperty;
034import javafx.beans.property.DoubleProperty;
035import javafx.beans.property.ObjectProperty;
036import javafx.beans.property.ObjectPropertyBase;
037import javafx.beans.property.ReadOnlyDoubleProperty;
038import javafx.beans.property.ReadOnlyDoubleWrapper;
039import javafx.beans.property.SimpleDoubleProperty;
040import javafx.collections.FXCollections;
041import javafx.collections.ListChangeListener;
042import javafx.collections.ObservableList;
043import javafx.geometry.Dimension2D;
044import javafx.geometry.Side;
045import javafx.util.Duration;
046
047import com.sun.javafx.charts.ChartLayoutAnimator;
048import javafx.css.StyleableBooleanProperty;
049import javafx.css.StyleableDoubleProperty;
050import javafx.css.CssMetaData;
051import com.sun.javafx.css.converters.BooleanConverter;
052import com.sun.javafx.css.converters.SizeConverter;
053import java.util.Collections;
054import javafx.css.Styleable;
055import javafx.css.StyleableProperty;
056
057/**
058 * A axis implementation that will works on string categories where each 
059 * value as a unique category(tick mark) along the axis.
060 */
061public final class CategoryAxis extends Axis<String> {
062
063    // -------------- PRIVATE FIELDS -------------------------------------------
064    private List<String> allDataCategories = new ArrayList<String>();
065    private boolean changeIsLocal = false;
066    /** This is the gap between one category and the next along this axis */
067    private final DoubleProperty firstCategoryPos = new SimpleDoubleProperty(this, "firstCategoryPos", 0);
068    private Object currentAnimationID;
069    private final ChartLayoutAnimator animator = new ChartLayoutAnimator(this);
070    private ListChangeListener<String> itemsListener = new ListChangeListener<String>() {
071        @Override public void onChanged(Change<? extends String> c) {
072            while (c.next()) {
073                if(!c.getAddedSubList().isEmpty()) {
074                    // remove duplicates else they will get rendered on the chart.
075                    // Ideally we should be using a Set for categories.
076                    for (String addedStr : c.getAddedSubList())
077                        checkAndRemoveDuplicates(addedStr);
078                    }
079                if (!isAutoRanging()) {
080                    allDataCategories.clear();
081                    allDataCategories.addAll(getCategories());
082                    rangeValid = false;
083                }
084                requestAxisLayout();
085            }
086        }
087    };
088    
089    // -------------- PUBLIC PROPERTIES ----------------------------------------
090
091    /** The margin between the axis start and the first tick-mark */
092    private DoubleProperty startMargin = new StyleableDoubleProperty(5) {
093        @Override protected void invalidated() {
094            requestAxisLayout();
095        }
096
097        @Override public CssMetaData<CategoryAxis,Number> getCssMetaData() {
098            return StyleableProperties.START_MARGIN;
099        }
100        
101        @Override
102        public Object getBean() {
103            return CategoryAxis.this;
104        }
105
106        @Override
107        public String getName() {
108            return "startMargin";
109        }
110    };
111    public final double getStartMargin() { return startMargin.getValue(); }
112    public final void setStartMargin(double value) { startMargin.setValue(value); }
113    public final DoubleProperty startMarginProperty() { return startMargin; }
114
115    /** The margin between the last tick mark and the axis end */
116    private DoubleProperty endMargin = new StyleableDoubleProperty(5) {
117        @Override protected void invalidated() {
118            requestAxisLayout();
119        }
120
121
122        @Override public CssMetaData<CategoryAxis,Number> getCssMetaData() {
123            return StyleableProperties.END_MARGIN;
124        }
125
126        @Override
127        public Object getBean() {
128            return CategoryAxis.this;
129        }
130
131        @Override
132        public String getName() {
133            return "endMargin";
134        }
135    };
136    public final double getEndMargin() { return endMargin.getValue(); }
137    public final void setEndMargin(double value) { endMargin.setValue(value); }
138    public final DoubleProperty endMarginProperty() { return endMargin; }
139
140    /** If this is true then half the space between ticks is left at the start
141     * and end
142     */
143    private BooleanProperty gapStartAndEnd = new StyleableBooleanProperty(true) {
144        @Override protected void invalidated() {
145            requestAxisLayout();
146        }
147
148
149        @Override public CssMetaData<CategoryAxis,Boolean> getCssMetaData() {
150            return StyleableProperties.GAP_START_AND_END;
151        }
152        
153        @Override
154        public Object getBean() {
155            return CategoryAxis.this;
156        }
157
158        @Override
159        public String getName() {
160            return "gapStartAndEnd";
161        }
162    };
163    public final boolean isGapStartAndEnd() { return gapStartAndEnd.getValue(); }
164    public final void setGapStartAndEnd(boolean value) { gapStartAndEnd.setValue(value); }
165    public final BooleanProperty gapStartAndEndProperty() { return gapStartAndEnd; }
166
167    /**
168     * The ordered list of categories plotted on this axis. This is set automatically 
169     * based on the charts data if autoRanging is true. If the application sets the categories
170     * then auto ranging is turned off. If there is an attempt to add duplicate entry into this list, 
171     * an {@link IllegalArgumentException} is thrown.
172     */
173    private ObjectProperty<ObservableList<String>> categories = new ObjectPropertyBase<ObservableList<String>>() {
174        ObservableList<String> old;
175        @Override protected void invalidated() {
176            if (getDuplicate() != null) {
177                throw new IllegalArgumentException("Duplicate category added; "+getDuplicate()+" already present");
178            }
179            final ObservableList<String> newItems = get();
180            if (old != newItems) {
181                // Add and remove listeners
182                if (old != null) old.removeListener(itemsListener);
183                if (newItems != null) newItems.addListener(itemsListener);
184                old = newItems;
185            }
186        }
187
188        @Override
189        public Object getBean() {
190            return CategoryAxis.this;
191        }
192
193        @Override
194        public String getName() {
195            return "categories";
196        }
197    };
198    public final void setCategories(ObservableList<String> value) {
199        categories.set(value);
200        if (!changeIsLocal) {
201            setAutoRanging(false);
202            allDataCategories.clear();
203            allDataCategories.addAll(getCategories());
204        }
205        requestAxisLayout();
206    }
207    
208    private void checkAndRemoveDuplicates(String category) {
209        if (getDuplicate() != null) {
210            getCategories().remove(category);
211            throw new IllegalArgumentException("Duplicate category ; "+category+" already present");
212        }
213    }
214
215    private String getDuplicate() {
216        if (getCategories() != null) { 
217            for (int i = 0; i < getCategories().size(); i++) {
218                for (int j = 0; j < getCategories().size(); j++) {
219                    if (getCategories().get(i).equals(getCategories().get(j)) && i != j) {
220                        return getCategories().get(i);
221                    }
222                }
223            }
224        }
225        return null;
226    }
227    /**
228     * Returns a {@link ObservableList} of categories plotted on this axis.
229     *
230     * @return ObservableList of categories for this axis.
231     * @see #categories
232     */
233    public final ObservableList<String> getCategories() {
234        return categories.get();
235    }
236
237    /** This is the gap between one category and the next along this axis */
238    private final ReadOnlyDoubleWrapper categorySpacing = new ReadOnlyDoubleWrapper(this, "categorySpacing", 1);
239    public final double getCategorySpacing() {
240        return categorySpacing.get();
241    }
242    public final ReadOnlyDoubleProperty categorySpacingProperty() {
243        return categorySpacing.getReadOnlyProperty();
244    }
245
246    // -------------- CONSTRUCTORS -------------------------------------------------------------------------------------
247
248    /**
249     * Create a auto-ranging category axis with an empty list of categories.
250     */
251    public CategoryAxis() { 
252        changeIsLocal = true;
253        setCategories(FXCollections.<String>observableArrayList());
254        changeIsLocal = false;
255    }
256
257    /**
258     * Create a category axis with the given categories. This will not auto-range but be fixed with the given categories.
259     *
260     * @param categories List of the categories for this axis
261     */
262    public CategoryAxis(ObservableList<String> categories) {
263        setCategories(categories);
264    }
265
266    // -------------- PRIVATE METHODS ----------------------------------------------------------------------------------
267
268    private double calculateNewSpacing(double length, List<String> categories) {
269        final Side side = getSide();
270        double newCategorySpacing = 1;
271        if(side != null && categories != null) {
272            double bVal = (isGapStartAndEnd() ? (categories.size()) : (categories.size() - 1));
273            // RT-14092 flickering  : check if bVal is 0
274            newCategorySpacing = (bVal == 0) ? 1 : (length-getStartMargin()-getEndMargin()) / bVal;
275        }
276        // if autoranging is off setRange is not called so we update categorySpacing
277        if (!isAutoRanging()) categorySpacing.set(newCategorySpacing);
278        return newCategorySpacing;
279    }
280
281    private double calculateNewFirstPos(double length, double catSpacing) {
282        final Side side = getSide();
283        double newPos = 1;
284        if(side != null) {
285            double offset = ((isGapStartAndEnd()) ? (catSpacing / 2) : (0));
286            if (side.equals(Side.TOP) || side.equals(Side.BOTTOM)) { // HORIZONTAL
287                newPos = 0 + getStartMargin() + offset;
288            }  else { // VERTICAL
289                newPos = length - getStartMargin() - offset;
290            }
291        }
292        // if autoranging is off setRange is not called so we update first cateogory pos.
293        if (!isAutoRanging()) firstCategoryPos.set(newPos);
294        return newPos;
295    }
296
297    // -------------- PROTECTED METHODS --------------------------------------------------------------------------------
298
299    /**
300     * Called to get the current axis range.
301     *
302     * @return A range object that can be passed to setRange() and calculateTickValues()
303     */
304    @Override protected Object getRange() {
305        return new Object[]{ getCategories(), categorySpacing.get(), firstCategoryPos.get(), getTickLabelRotation() };
306    }
307
308    /**
309     * Called to set the current axis range to the given range. If isAnimating() is true then this method should
310     * animate the range to the new range.
311     *
312     * @param range A range object returned from autoRange()
313     * @param animate If true animate the change in range
314     */
315    @Override protected void setRange(Object range, boolean animate) {
316        Object[] rangeArray = (Object[]) range;
317        @SuppressWarnings({"unchecked"}) List<String> categories = (List<String>)rangeArray[0];
318//        if (categories.isEmpty()) new java.lang.Throwable().printStackTrace();
319        double newCategorySpacing = (Double)rangeArray[1];
320        double newFirstCategoryPos = (Double)rangeArray[2];
321        double tickLabelRotation = (Double)rangeArray[3];
322        setTickLabelRotation(tickLabelRotation);
323        changeIsLocal = true;
324        setCategories(FXCollections.<String>observableArrayList(categories));
325        changeIsLocal = false;
326        if (animate) {
327            animator.stop(currentAnimationID);
328            currentAnimationID = animator.animate(
329                new KeyFrame(Duration.ZERO,
330                    new KeyValue(firstCategoryPos, firstCategoryPos.get()),
331                    new KeyValue(categorySpacing, categorySpacing.get())
332                ),
333                new KeyFrame(Duration.millis(1000),
334                    new KeyValue(firstCategoryPos,newFirstCategoryPos),
335                    new KeyValue(categorySpacing,newCategorySpacing)
336                )
337            );
338        } else {
339            categorySpacing.set(newCategorySpacing);
340            firstCategoryPos.set(newFirstCategoryPos);
341        }
342    }
343
344    /**
345     * This calculates the categories based on the data provided to invalidateRange() method. This must not
346     * effect the state of the axis, changing any properties of the axis. Any results of the auto-ranging should be
347     * returned in the range object. This will we passed to setRange() if it has been decided to adopt this range for
348     * this axis.
349     *
350     * @param length The length of the axis in screen coordinates
351     * @return Range information, this is implementation dependent
352     */
353    @Override protected Object autoRange(double length) {
354        final Side side = getSide();
355        final boolean vertical = Side.LEFT.equals(side) || Side.RIGHT.equals(side);
356        // TODO check if we can display all categories
357        final double newCategorySpacing = calculateNewSpacing(length,allDataCategories);
358        final double newFirstPos = calculateNewFirstPos(length, newCategorySpacing);
359        double tickLabelRotation = getTickLabelRotation();
360        if (length >= 0) {
361            double requiredLengthToDisplay = calculateRequiredSize(vertical,tickLabelRotation);
362            if (requiredLengthToDisplay > length) {
363                // change text to vertical
364                tickLabelRotation = 90;
365            }
366        }
367        return new Object[]{allDataCategories, newCategorySpacing, newFirstPos, tickLabelRotation};
368    }
369
370    private double calculateRequiredSize(boolean axisVertical, double tickLabelRotation) {
371        double requiredLengthToDisplay = Double.MAX_VALUE;
372        // Calculate the max space required between categories labels
373        double maxReqTickGap = 0;
374        double last = 0;
375        boolean first = true;
376        for (String category: allDataCategories) {
377            Dimension2D textSize = measureTickMarkSize(category, tickLabelRotation);
378            double size = (axisVertical || (tickLabelRotation != 0)) ? textSize.getHeight() : textSize.getWidth();
379            // TODO better handle calculations for rotated text, overlapping text etc
380            if (first) {
381                first = false;
382                last = size/2;
383            } else {
384                maxReqTickGap = Math.max(maxReqTickGap, last + 6 + (size/2) );
385            }
386        }
387        return getStartMargin() + maxReqTickGap*allDataCategories.size() + getEndMargin();
388    }
389
390    /**
391     * Calculate a list of all the data values for each tick mark in range
392     *
393     * @param length The length of the axis in display units
394     * @return A list of tick marks that fit along the axis if it was the given length
395     */
396    @Override protected List<String> calculateTickValues(double length, Object range) {
397        Object[] rangeArray = (Object[]) range;
398        //noinspection unchecked
399        return (List<String>)rangeArray[0];
400    }
401
402    /**
403     * Get the string label name for a tick mark with the given value
404     *
405     * @param value The value to format into a tick label string
406     * @return A formatted string for the given value
407     */
408    @Override protected String getTickMarkLabel(String value) {
409        // TODO use formatter
410        return value;
411    }
412
413    /**
414     * Measure the size of the label for given tick mark value. This uses the font that is set for the tick marks
415     *
416     * @param value tick mark value
417     * @param range range to use during calculations
418     * @return size of tick mark label for given value
419     */
420    @Override protected Dimension2D measureTickMarkSize(String value, Object range) {
421        final Object[] rangeArray = (Object[]) range;
422        final double tickLabelRotation = (Double)rangeArray[3];
423        return measureTickMarkSize(value,tickLabelRotation);
424    }
425
426    // -------------- METHODS ------------------------------------------------------------------------------------------
427
428    /**
429     * Called when data has changed and the range may not be valid any more. This is only called by the chart if
430     * isAutoRanging() returns true. If we are auto ranging it will cause layout to be requested and auto ranging to
431     * happen on next layout pass.
432     *
433     * @param data The current set of all data that needs to be plotted on this axis
434     */
435    @Override public void invalidateRange(List<String> data) {
436        super.invalidateRange(data);
437        // Create unique set of category names        
438        List<String> categoryNames = new ArrayList<String>();
439        categoryNames.addAll(allDataCategories);
440        //RT-21141 allDataCategories needs to be updated based on data -
441        // and should maintain the order it originally had for the categories already present.
442        // and remove categories not present in data
443        for(String cat : allDataCategories) {
444            if (!data.contains(cat)) categoryNames.remove(cat); 
445        }
446        // add any new category found in data
447//        for(String cat : data) {
448        for (int i = 0; i < data.size(); i++) {    
449           int len = categoryNames.size();
450           if (!categoryNames.contains(data.get(i))) categoryNames.add((i > len) ? len : i, data.get(i));
451        }
452        allDataCategories.clear();
453        allDataCategories.addAll(categoryNames);
454    }
455
456    /**
457     * Get the display position along this axis for a given value
458     *
459     * @param value The data value to work out display position for
460     * @return display position or Double.NaN if zero is not in current range;
461     */
462    @Override public double getDisplayPosition(String value) {
463        // find index of value
464        if (Side.TOP.equals(getSide()) || Side.BOTTOM.equals(getSide())) { // HORIZONTAL
465            return firstCategoryPos.get() + getCategories().indexOf("" + value) * categorySpacing.get();
466        } else {
467            return firstCategoryPos.get() + getCategories().indexOf("" + value) * categorySpacing.get() * -1;
468        }
469    }
470
471    /**
472     * Get the data value for the given display position on this axis. If the axis
473     * is a CategoryAxis this will be the nearest value.
474     *
475     * @param  displayPosition A pixel position on this axis
476     * @return the nearest data value to the given pixel position or
477     *         null if not on axis;
478     */
479    @Override public String getValueForDisplay(double displayPosition) {
480        if (getSide().equals(Side.TOP) || getSide().equals(Side.BOTTOM)) { // HORIZONTAL
481            if (displayPosition < 0 || displayPosition > getWidth()) return null;
482            double d = (displayPosition - firstCategoryPos.get()) /   categorySpacing.get();
483            return toRealValue(d);
484        } else { // VERTICAL
485            if (displayPosition < 0 || displayPosition > getHeight()) return null;
486            double d = (displayPosition - firstCategoryPos.get()) /   (categorySpacing.get() * -1);
487            return toRealValue(d);
488        }
489    }
490
491    /**
492     * Checks if the given value is plottable on this axis
493     *
494     * @param value The value to check if its on axis
495     * @return true if the given value is plottable on this axis
496     */
497    @Override public boolean isValueOnAxis(String value) {
498        return getCategories().indexOf("" + value) != -1;
499    }
500
501    /**
502     * All axis values must be representable by some numeric value. This gets the numeric value for a given data value.
503     *
504     * @param value The data value to convert
505     * @return Numeric value for the given data value
506     */
507    @Override public double toNumericValue(String value) {
508        return getCategories().indexOf(value);
509    }
510
511    /**
512     * All axis values must be representable by some numeric value. This gets the data value for a given numeric value.
513     *
514     * @param value The numeric value to convert
515     * @return Data value for given numeric value
516     */
517    @Override public String toRealValue(double value) {
518        int index = (int)Math.round(value);
519        List<String> categories = getCategories();
520        if (index >= 0 && index < categories.size()) {
521            return getCategories().get(index);
522        } else {
523            return null;
524        }
525    }
526
527    /**
528     * Get the display position of the zero line along this axis. As there is no concept of zero on a CategoryAxis
529     * this is always Double.NaN.
530     *
531     * @return always Double.NaN for CategoryAxis
532     */
533    @Override public double getZeroPosition() {
534        return Double.NaN;
535    }
536
537    // -------------- STYLESHEET HANDLING ------------------------------------------------------------------------------
538
539    /** @treatAsPrivate implementation detail */
540    private static class StyleableProperties {
541        private static final CssMetaData<CategoryAxis,Number> START_MARGIN =
542            new CssMetaData<CategoryAxis,Number>("-fx-start-margin",
543                SizeConverter.getInstance(), 5.0) {
544
545            @Override
546            public boolean isSettable(CategoryAxis n) {
547                return n.startMargin == null || !n.startMargin.isBound();
548            }
549
550            @Override
551            public StyleableProperty<Number> getStyleableProperty(CategoryAxis n) {
552                return (StyleableProperty<Number>)n.startMarginProperty();
553            }
554        };
555        
556        private static final CssMetaData<CategoryAxis,Number> END_MARGIN =
557            new CssMetaData<CategoryAxis,Number>("-fx-end-margin",
558                SizeConverter.getInstance(), 5.0) {
559
560            @Override
561            public boolean isSettable(CategoryAxis n) {
562                return n.endMargin == null || !n.endMargin.isBound();
563            }
564
565            @Override
566            public StyleableProperty<Number> getStyleableProperty(CategoryAxis n) {
567                return (StyleableProperty<Number>)n.endMarginProperty();
568            }
569        };
570        
571        private static final CssMetaData<CategoryAxis,Boolean> GAP_START_AND_END =
572            new CssMetaData<CategoryAxis,Boolean>("-fx-gap-start-and-end",
573                BooleanConverter.getInstance(), Boolean.TRUE) {
574
575            @Override
576            public boolean isSettable(CategoryAxis n) {
577                return n.gapStartAndEnd == null || !n.gapStartAndEnd.isBound();
578            }
579
580            @Override
581            public StyleableProperty<Boolean> getStyleableProperty(CategoryAxis n) {
582                return (StyleableProperty<Boolean>)n.gapStartAndEndProperty();
583            }
584        };
585
586        private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
587        static {
588        final List<CssMetaData<? extends Styleable, ?>> styleables =
589            new ArrayList<CssMetaData<? extends Styleable, ?>>(Axis.getClassCssMetaData());
590            styleables.add(START_MARGIN);
591            styleables.add(END_MARGIN);
592            styleables.add(GAP_START_AND_END);
593            STYLEABLES = Collections.unmodifiableList(styleables);
594        }
595    }
596
597    /**
598     * @return The CssMetaData associated with this class, which may include the
599     * CssMetaData of its super classes.
600     */
601    public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
602        return StyleableProperties.STYLEABLES;
603    }
604
605    /**
606     * {@inheritDoc}
607     */
608    @Override
609    public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
610        return getClassCssMetaData();
611    }
612
613}
614