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 javafx.css.Styleable;
029import javafx.css.CssMetaData;
030import javafx.css.PseudoClass;
031import javafx.css.StyleableBooleanProperty;
032import javafx.css.StyleableDoubleProperty;
033import javafx.css.StyleableObjectProperty;
034import com.sun.javafx.css.converters.BooleanConverter;
035import com.sun.javafx.css.converters.EnumConverter;
036import com.sun.javafx.css.converters.PaintConverter;
037import com.sun.javafx.css.converters.SizeConverter;
038import java.util.ArrayList;
039import java.util.Collections;
040import java.util.List;
041
042import javafx.animation.FadeTransition;
043import javafx.beans.binding.DoubleExpression;
044import javafx.beans.binding.ObjectExpression;
045import javafx.beans.binding.StringExpression;
046import javafx.beans.property.*;
047import javafx.collections.FXCollections;
048import javafx.collections.ObservableList;
049import javafx.css.FontCssMetaData;
050import javafx.css.StyleableProperty;
051import javafx.event.ActionEvent;
052import javafx.event.EventHandler;
053import javafx.geometry.Bounds;
054import javafx.geometry.Dimension2D;
055import javafx.geometry.Pos;
056import javafx.geometry.Side;
057import javafx.scene.control.Label;
058import javafx.scene.layout.Region;
059import javafx.scene.paint.Color;
060import javafx.scene.paint.Paint;
061import javafx.scene.shape.LineTo;
062import javafx.scene.shape.MoveTo;
063import javafx.scene.shape.Path;
064import javafx.scene.text.Font;
065import javafx.scene.text.Text;
066import javafx.scene.transform.Rotate;
067import javafx.scene.transform.Translate;
068import javafx.util.Duration;
069
070
071/**
072 * Base class for all axes in JavaFX that represents an axis drawn on a chart area.
073 * It holds properties for axis auto ranging, ticks and labels along the axis.
074 * <p>
075 * Some examples of concrete subclasses include {@link NumberAxis} whose axis plots data
076 * in numbers and {@link CategoryAxis} whose values / ticks represent string
077 * categories along its axis.
078 */
079public abstract class Axis<T> extends Region {
080
081    // -------------- PRIVATE FIELDS -----------------------------------------------------------------------------------
082
083    Text measure = new Text();
084    private Label axisLabel = new Label();
085    private final Path tickMarkPath = new Path();
086    private double oldLength = 0;
087    /** True when the current range invalid and all dependent calculations need to be updated */
088    boolean rangeValid = false;
089    private boolean tickPropertyChanged = false;
090    /** True when labelFormatter changes programmatically - only tick marks text needs to updated */
091    boolean formatterValid = false;
092    
093    double maxWidth = 0;
094    double maxHeight = 0;
095    // -------------- PUBLIC PROPERTIES --------------------------------------------------------------------------------
096
097    private final ObservableList<TickMark<T>> tickMarks = FXCollections.observableArrayList();
098    private final ObservableList<TickMark<T>> unmodifiableTickMarks = FXCollections.unmodifiableObservableList(tickMarks);
099    /**
100     * Unmodifiable observable list of tickmarks, each TickMark directly representing a tickmark on this axis. This is updated
101     * whenever the displayed tickmarks changes.
102     *
103     * @return Unmodifiable observable list of TickMarks on this axis
104     */
105    public ObservableList<TickMark<T>> getTickMarks() { return unmodifiableTickMarks; }
106
107    /** The side of the plot which this axis is being drawn on */
108    private ObjectProperty<Side> side = new StyleableObjectProperty<Side>(){
109        @Override protected void invalidated() {
110            // cause refreshTickMarks
111            Side edge = get();
112            pseudoClassStateChanged(TOP_PSEUDOCLASS_STATE, edge == Side.TOP);
113            pseudoClassStateChanged(RIGHT_PSEUDOCLASS_STATE, edge == Side.RIGHT);
114            pseudoClassStateChanged(BOTTOM_PSEUDOCLASS_STATE, edge == Side.BOTTOM);
115            pseudoClassStateChanged(LEFT_PSEUDOCLASS_STATE, edge == Side.LEFT);
116            requestAxisLayout();
117        }
118        
119        @Override
120        public CssMetaData<Axis<?>,Side> getCssMetaData() {
121            return StyleableProperties.SIDE;
122        }
123
124        @Override
125        public Object getBean() {
126            return Axis.this;
127        }
128
129        @Override
130        public String getName() {
131            return "side";
132        }
133    };
134    public final Side getSide() { return side.get(); }
135    public final void setSide(Side value) { side.set(value); }
136    public final ObjectProperty<Side> sideProperty() { return side; }
137
138    /** The axis label */
139    private ObjectProperty<String> label = new ObjectPropertyBase<String>() {
140        @Override protected void invalidated() {
141            axisLabel.setText(get());
142            requestAxisLayout();
143        }
144
145        @Override
146        public Object getBean() {
147            return Axis.this;
148        }
149
150        @Override
151        public String getName() {
152            return "label";
153        }
154    };
155    public final String getLabel() { return label.get(); }
156    public final void setLabel(String value) { label.set(value); }
157    public final ObjectProperty<String> labelProperty() { return label; }
158
159    /** true if tick marks should be displayed */
160    private BooleanProperty tickMarkVisible = new StyleableBooleanProperty(true) {
161        @Override protected void invalidated() {
162            tickMarkPath.setVisible(get());
163            requestAxisLayout();
164        }
165
166        @Override
167        public CssMetaData<Axis<?>,Boolean> getCssMetaData() {
168            return StyleableProperties.TICK_MARK_VISIBLE;
169        }
170        @Override
171        public Object getBean() {
172            return Axis.this;
173        }
174
175        @Override
176        public String getName() {
177            return "tickMarkVisible";
178        }
179    };
180    public final boolean isTickMarkVisible() { return tickMarkVisible.get(); }
181    public final void setTickMarkVisible(boolean value) { tickMarkVisible.set(value); }
182    public final BooleanProperty tickMarkVisibleProperty() { return tickMarkVisible; }
183
184    /** true if tick mark labels should be displayed */
185    private BooleanProperty tickLabelsVisible = new StyleableBooleanProperty(true) {
186        @Override protected void invalidated() {
187            // update textNode visibility for each tick
188            for (TickMark<T> tick : tickMarks) {
189                tick.setTextVisible(get());
190            }
191            requestAxisLayout();
192        }
193        
194        @Override
195        public CssMetaData<Axis<?>,Boolean> getCssMetaData() {
196            return StyleableProperties.TICK_LABELS_VISIBLE;
197        }
198
199        @Override
200        public Object getBean() {
201            return Axis.this;
202        }
203
204        @Override
205        public String getName() {
206            return "tickLabelsVisible";
207        }
208    };
209    public final boolean isTickLabelsVisible() { return tickLabelsVisible.get(); }
210    public final void setTickLabelsVisible(boolean value) {
211        tickLabelsVisible.set(value); }
212    public final BooleanProperty tickLabelsVisibleProperty() { return tickLabelsVisible; }
213
214    /** The length of tick mark lines */
215    private DoubleProperty tickLength = new StyleableDoubleProperty(8) {
216        @Override protected void invalidated() {
217            if (tickLength.get() < 0 && !tickLength.isBound()) {
218                tickLength.set(0);
219            }
220            // this effects preferred size so request layout
221            requestAxisLayout();
222        }
223
224        @Override
225        public CssMetaData<Axis<?>,Number> getCssMetaData() {
226            return StyleableProperties.TICK_LENGTH;
227        }        
228        @Override
229        public Object getBean() {
230            return Axis.this;
231        }
232
233        @Override
234        public String getName() {
235            return "tickLength";
236        }
237    };
238    public final double getTickLength() { return tickLength.get(); }
239    public final void setTickLength(double value) { tickLength.set(value); }
240    public final DoubleProperty tickLengthProperty() { return tickLength; }
241
242    /** This is true when the axis determines its range from the data automatically */
243    private BooleanProperty autoRanging = new BooleanPropertyBase(true) {
244        @Override protected void invalidated() {
245            if(get()) {
246                // auto range turned on, so need to auto range now
247//                autoRangeValid = false;
248                requestAxisLayout();
249            }
250        }
251
252        @Override
253        public Object getBean() {
254            return Axis.this;
255        }
256
257        @Override
258        public String getName() {
259            return "autoRanging";
260        }
261    };
262    public final boolean isAutoRanging() { return autoRanging.get(); }
263    public final void setAutoRanging(boolean value) { autoRanging.set(value); }
264    public final BooleanProperty autoRangingProperty() { return autoRanging; }
265
266    /** The font for all tick labels */
267    private ObjectProperty<Font> tickLabelFont = new StyleableObjectProperty<Font>(Font.font("System",8)) {
268        @Override protected void invalidated() {
269            Font f = get();
270            measure.setFont(f);
271            for(TickMark<T> tm : getTickMarks()) {
272                tm.textNode.setFont(f);
273            }
274            requestAxisLayout();
275        }
276
277        @Override 
278        public CssMetaData<Axis<?>,Font> getCssMetaData() {
279            return StyleableProperties.TICK_LABEL_FONT;
280        }
281        
282        @Override
283        public Object getBean() {
284            return Axis.this;
285        }
286
287        @Override
288        public String getName() {
289            return "tickLabelFont";
290        }
291    };
292    public final Font getTickLabelFont() { return tickLabelFont.get(); }
293    public final void setTickLabelFont(Font value) { tickLabelFont.set(value); }
294    public final ObjectProperty<Font> tickLabelFontProperty() { return tickLabelFont; }
295
296    /** The fill for all tick labels */
297    private ObjectProperty<Paint> tickLabelFill = new StyleableObjectProperty<Paint>(Color.BLACK) {
298        @Override protected void invalidated() {
299            tickPropertyChanged = true;
300            requestAxisLayout();
301        }
302
303        @Override
304        public CssMetaData<Axis<?>,Paint> getCssMetaData() {
305            return StyleableProperties.TICK_LABEL_FILL;
306        }
307        
308        @Override
309        public Object getBean() {
310            return Axis.this;
311        }
312
313        @Override
314        public String getName() {
315            return "tickLabelFill";
316        }
317    };
318    public final Paint getTickLabelFill() { return tickLabelFill.get(); }
319    public final void setTickLabelFill(Paint value) { tickLabelFill.set(value); }
320    public final ObjectProperty<Paint> tickLabelFillProperty() { return tickLabelFill; }
321
322    /** The gap between tick labels and the tick mark lines */
323    private DoubleProperty tickLabelGap = new StyleableDoubleProperty(3) {
324        @Override protected void invalidated() {
325           requestAxisLayout();
326        }
327
328        @Override
329        public CssMetaData<Axis<?>,Number> getCssMetaData() {
330            return StyleableProperties.TICK_LABEL_TICK_GAP;
331        }
332        
333        @Override
334        public Object getBean() {
335            return Axis.this;
336        }
337
338        @Override
339        public String getName() {
340            return "tickLabelGap";
341        }
342    };
343    public final double getTickLabelGap() { return tickLabelGap.get(); }
344    public final void setTickLabelGap(double value) { tickLabelGap.set(value); }
345    public final DoubleProperty tickLabelGapProperty() { return tickLabelGap; }
346
347    /**
348     * When true any changes to the axis and its range will be animated.
349     */
350    private BooleanProperty animated = new SimpleBooleanProperty(this, "animated", true);
351
352    /**
353     * Indicates whether the changes to axis range will be animated or not.
354     *
355     * @return true if axis range changes will be animated and false otherwise
356     */
357    public final boolean getAnimated() { return animated.get(); }
358    public final void setAnimated(boolean value) { animated.set(value); }
359    public final BooleanProperty animatedProperty() { return animated; }
360
361    /**
362     * Rotation in degrees of tick mark labels from their normal horizontal.
363     */
364    private DoubleProperty tickLabelRotation = new DoublePropertyBase(0) {
365        @Override protected void invalidated() {
366            requestAxisLayout();
367        }
368
369        @Override
370        public Object getBean() {
371            return Axis.this;
372        }
373
374        @Override
375        public String getName() {
376            return "tickLabelRotation";
377        }
378    };
379    public final double getTickLabelRotation() { return tickLabelRotation.getValue(); }
380    public final void setTickLabelRotation(double value) { tickLabelRotation.setValue(value); }
381    public final DoubleProperty tickLabelRotationProperty() { return tickLabelRotation; }
382
383    // -------------- CONSTRUCTOR --------------------------------------------------------------------------------------
384
385    /**
386     * Creates and initializes a new instance of the Axis class.
387     */
388    public Axis() {
389        getStyleClass().setAll("axis");
390        axisLabel.getStyleClass().add("axis-label");
391        axisLabel.setAlignment(Pos.CENTER);
392        tickMarkPath.getStyleClass().add("axis-tick-mark");
393        getChildren().addAll(axisLabel, tickMarkPath);
394    }
395
396    // -------------- METHODS ------------------------------------------------------------------------------------------
397
398    /**
399     * See if the current range is valid, if it is not then any range dependent calulcations need to redone on the next layout pass
400     *
401     * @return true if current range calculations are valid
402     */
403    protected final boolean isRangeValid() { return rangeValid; }
404
405    /**
406     * Mark the current range invalid, this will cause anything that depends on the range to be recalculated on the
407     * next layout.
408     */
409    protected final void invalidateRange() { rangeValid = false; }
410
411    /**
412     * This is used to check if any given animation should run. It returns true if animation is enabled and the node
413     * is visible and in a scene.
414     *
415     * @return true if animations should happen
416     */
417    protected final boolean shouldAnimate(){
418        return getAnimated() && impl_isTreeVisible() && getScene() != null;
419    }
420    
421    /**
422     * We suppress requestLayout() calls here by doing nothing as we don't want changes to our children to cause
423     * layout. If you really need to request layout then call requestAxisLayout().
424     */
425    @Override public void requestLayout() {}
426
427    /**
428     * Request that the axis is laid out in the next layout pass. This replaces requestLayout() as it has been
429     * overridden to do nothing so that changes to children's bounds etc do not cause a layout. This was done as a
430     * optimization as the Axis knows the exact minimal set of changes that really need layout to be updated. So we
431     * only want to request layout then, not on any child change.
432     */
433    public void requestAxisLayout() {
434        super.requestLayout();
435    }
436
437    /**
438     * Called when data has changed and the range may not be valid any more. This is only called by the chart if
439     * isAutoRanging() returns true. If we are auto ranging it will cause layout to be requested and auto ranging to
440     * happen on next layout pass.
441     *
442     * @param data The current set of all data that needs to be plotted on this axis
443     */
444    public void invalidateRange(List<T> data) {
445        invalidateRange();
446        requestAxisLayout();
447    }
448
449    /**
450     * This calculates the upper and lower bound based on the data provided to invalidateRange() method. This must not
451     * effect the state of the axis, changing any properties of the axis. Any results of the auto-ranging should be
452     * returned in the range object. This will we passed to setRange() if it has been decided to adopt this range for
453     * this axis.
454     *
455     * @param length The length of the axis in screen coordinates
456     * @return Range information, this is implementation dependent
457     */
458    protected abstract Object autoRange(double length);
459
460    /**
461     * Called to set the current axis range to the given range. If isAnimating() is true then this method should
462     * animate the range to the new range.
463     *
464     * @param range A range object returned from autoRange()
465     * @param animate If true animate the change in range
466     */
467    protected abstract void setRange(Object range, boolean animate);
468
469    /**
470     * Called to get the current axis range.
471     *
472     * @return A range object that can be passed to setRange() and calculateTickValues()
473     */
474    protected abstract Object getRange();
475
476    /**
477     * Get the display position of the zero line along this axis.
478     *
479     * @return display position or Double.NaN if zero is not in current range;
480     */
481    public abstract double getZeroPosition();
482
483    /**
484     * Get the display position along this axis for a given value
485     *
486     * @param value The data value to work out display position for
487     * @return display position or Double.NaN if zero is not in current range;
488     */
489    public abstract double getDisplayPosition(T value);
490
491    /**
492     * Get the data value for the given display position on this axis. If the axis
493     * is a CategoryAxis this will be the nearest value.
494     *
495     * @param  displayPosition A pixel position on this axis
496     * @return the nearest data value to the given pixel position or
497     *         null if not on axis;
498     */
499    public abstract T getValueForDisplay(double displayPosition);
500
501    /**
502     * Checks if the given value is plottable on this axis
503     *
504     * @param value The value to check if its on axis
505     * @return true if the given value is plottable on this axis
506     */
507    public abstract boolean isValueOnAxis(T value);
508
509    /**
510     * All axis values must be representable by some numeric value. This gets the numeric value for a given data value.
511     *
512     * @param value The data value to convert
513     * @return Numeric value for the given data value
514     */
515    public abstract double toNumericValue(T value);
516
517    /**
518     * All axis values must be representable by some numeric value. This gets the data value for a given numeric value.
519     *
520     * @param value The numeric value to convert
521     * @return Data value for given numeric value
522     */
523    public abstract T toRealValue(double value);
524
525    /**
526     * Calculate a list of all the data values for each tick mark in range
527     *
528     * @param length The length of the axis in display units
529     * @param range A range object returned from autoRange()
530     * @return A list of tick marks that fit along the axis if it was the given length
531     */
532    protected abstract List<T> calculateTickValues(double length, Object range);
533
534    /**
535     * Computes the preferred height of this axis for the given width. If axis orientation
536     * is horizontal, it takes into account the tick mark length, tick label gap and
537     * label height.
538     *
539     * @return the computed preferred width for this axis
540     */
541    @Override protected double computePrefHeight(double width) {
542        final Side side = getSide();
543        if (side == null) {
544            return 50;
545        } else if (side.equals(Side.TOP) || side.equals(Side.BOTTOM)) { // HORIZONTAL
546            // we need to first auto range as this may/will effect tick marks
547            Object range = autoRange(width);
548            // calculate max tick label height
549            double maxLabelHeight = 0;
550            // calculate the new tick marks
551            if (isTickLabelsVisible()) {
552                final List<T> newTickValues = calculateTickValues(width, range);
553                for (T value: newTickValues) {
554                    maxLabelHeight = Math.max(maxLabelHeight,measureTickMarkSize(value, range).getHeight());
555                }
556            }
557            // calculate tick mark length
558            final double tickMarkLength = isTickMarkVisible() ? (getTickLength() > 0) ? getTickLength() : 0 : 0;
559            // calculate label height
560            final double labelHeight =
561                    axisLabel.getText() == null || axisLabel.getText().length() == 0 ?
562                    0 : axisLabel.prefHeight(-1);
563            return maxLabelHeight + getTickLabelGap() + tickMarkLength + labelHeight;
564        } else { // VERTICAL
565            // TODO for now we have no hard and fast answer here, I guess it should work
566            // TODO out the minimum size needed to display min, max and zero tick mark labels.
567            return 100;
568        }
569    }
570
571    /**
572     * Computes the preferred width of this axis for the given height. If axis orientation
573     * is vertical, it takes into account the tick mark length, tick label gap and
574     * label height.
575     *
576     * @return the computed preferred width for this axis
577     */
578    @Override protected double computePrefWidth(double height) {
579        final Side side = getSide();
580        if (side == null) {
581            return 50;
582        } else if (side.equals(Side.TOP) || side.equals(Side.BOTTOM)) { // HORIZONTAL
583            // TODO for now we have no hard and fast answer here, I guess it should work
584            // TODO out the minimum size needed to display min, max and zero tick mark labels.
585            return 100;
586        } else { // VERTICAL
587            // we need to first auto range as this may/will effect tick marks
588            Object range = autoRange(height);
589            // calculate max tick label width
590            double maxLabelWidth = 0;
591            // calculate the new tick marks
592            if (isTickLabelsVisible()) {
593                final List<T> newTickValues = calculateTickValues(height,range);
594                for (T value: newTickValues) {
595                    maxLabelWidth = Math.max(maxLabelWidth, measureTickMarkSize(value, range).getWidth());
596                }
597            }
598            // calculate tick mark length
599            final double tickMarkLength = isTickMarkVisible() ? (getTickLength() > 0) ? getTickLength() : 0 : 0;
600            // calculate label height
601            final double labelHeight =
602                    axisLabel.getText() == null || axisLabel.getText().length() == 0 ?
603                    0 : axisLabel.prefHeight(-1);
604            return maxLabelWidth + getTickLabelGap() + tickMarkLength + labelHeight;
605        }
606    }
607
608    /**
609     * Called during layout if the tickmarks have been updated, allowing subclasses to do anything they need to
610     * in reaction.
611     */
612    protected void tickMarksUpdated(){}
613
614    /**
615     * Invoked during the layout pass to layout this axis and all its content.
616     */
617    @Override protected void layoutChildren() {
618        final double width = getWidth();
619        final double height = getHeight();
620        final double tickMarkLength = (getTickLength() > 0) ? getTickLength() : 0;
621        final boolean isFirstPass = oldLength == 0;
622        // auto range if it is not valid
623        final Side side = getSide();
624        final double length = (Side.TOP.equals(side) || Side.BOTTOM.equals(side)) ? width : height;
625        int numLabelsToSkip = 1;
626        int tickIndex = 0;
627        if (oldLength != length || !isRangeValid() || tickPropertyChanged || formatterValid) {
628            // get range
629            Object range;
630            if(isAutoRanging()) {
631                // auto range
632                range = autoRange(length);
633                // set current range to new range
634                setRange(range, getAnimated() && !isFirstPass && impl_isTreeVisible() && !isRangeValid());
635            } else {
636                range = getRange();
637            }
638            // calculate new tick marks
639            List<T> newTickValues = calculateTickValues(length, range);
640
641             // calculate maxLabelWidth / maxLabelHeight for respective orientations
642            maxWidth = 0; maxHeight = 0;
643            if (side != null) {
644                if (Side.TOP.equals(side) || Side.BOTTOM.equals(side)) {
645                    for (T value: newTickValues) {
646                        maxWidth = Math.round(Math.max(maxWidth, measureTickMarkSize(value, range).getWidth()));
647                    }
648                } else {
649                    for (T value: newTickValues) {
650                        maxHeight = Math.round(Math.max(maxHeight, measureTickMarkSize(value, range).getHeight()));
651                    }
652                }
653            }
654           
655            // we have to work out what new or removed tick marks there are, then create new tick marks and their
656            // text nodes where needed
657            // find everything added or removed
658            List<T> added = new ArrayList<T>();
659            List<TickMark<T>> removed = new ArrayList<TickMark<T>>();
660            if(tickMarks.isEmpty()) {
661                added.addAll(newTickValues);
662            } else {
663                // find removed
664                for (TickMark<T> tick: tickMarks) {
665                    if(!newTickValues.contains(tick.getValue())) removed.add(tick);
666                }
667                // find added
668                for(T newValue: newTickValues) {
669                    boolean found = false;
670                    for (TickMark<T> tick: tickMarks) {
671                        if(tick.getValue().equals(newValue)) {
672                            found = true;
673                            break;
674                        }
675                    }
676                    if(!found) added.add(newValue);
677                }
678            }
679            // remove everything that needs to go
680            for(TickMark<T> tick: removed) {
681                final TickMark<T> tm = tick;
682                if (shouldAnimate()) {
683                    FadeTransition ft = new FadeTransition(Duration.millis(250),tick.textNode);
684                    ft.setToValue(0);
685                    ft.setOnFinished(new EventHandler<ActionEvent>() {
686                        @Override public void handle(ActionEvent actionEvent) {
687                            getChildren().remove(tm.textNode);
688                        }
689                    });
690                    ft.play();
691                } else {
692                    getChildren().remove(tm.textNode);
693                }
694                // we have to remove the tick mark immediately so we don't draw tick line for it or grid lines and fills
695                tickMarks.remove(tm);
696            }
697            // add new tick marks for new values
698            for(T newValue: added) {
699                final TickMark<T> tick = new TickMark<T>();
700                tick.setValue(newValue);
701                tick.textNode.setText(getTickMarkLabel(newValue));
702                tick.textNode.setFont(getTickLabelFont());
703                tick.textNode.setFill(getTickLabelFill());
704                tick.setTextVisible(isTickLabelsVisible());
705                if (shouldAnimate()) tick.textNode.setOpacity(0);
706                getChildren().add(tick.textNode);
707                tickMarks.add(tick);
708                if (shouldAnimate()) {
709                    FadeTransition ft = new FadeTransition(Duration.millis(750),tick.textNode);
710                    ft.setFromValue(0);
711                    ft.setToValue(1);
712                    ft.play();
713                }
714            }
715            if (tickPropertyChanged) {
716                tickPropertyChanged = false;
717                for (TickMark<T> tick : tickMarks) {
718                    tick.textNode.setFill(getTickLabelFill());
719                }
720            }
721            if (formatterValid) {
722                // update tick's textNode text for all ticks as formatter has changed.
723                formatterValid = false;
724                for (TickMark<T> tick : tickMarks) {
725                    tick.textNode.setText(getTickMarkLabel(tick.getValue()));
726                }
727            }
728           
729            // call tick marks updated to inform subclasses that we have updated tick marks
730            tickMarksUpdated();
731            // mark all done
732            oldLength = length;
733            rangeValid = true;
734        }
735
736        // RT-12272 : tick labels overlapping
737        int numLabels = 0;
738        if (side != null) {
739            if (Side.TOP.equals(side) || Side.BOTTOM.equals(side)) {
740                numLabels = (maxWidth > 0) ? (int)(length/maxWidth) : 0;
741            } else {
742                numLabels = (maxHeight > 0) ? (int) (length/maxHeight) : 0;
743            }
744        }
745       
746        if (numLabels > 0) {
747            numLabelsToSkip = ((int)(tickMarks.size()/numLabels)) + 1;
748        }
749        // clear tick mark path elements as we will recreate
750        tickMarkPath.getElements().clear();
751        // do layout of axis label, tick mark lines and text
752        if (getSide().equals(Side.LEFT)) {
753            // offset path to make strokes snap to pixel
754            tickMarkPath.setLayoutX(-0.5);
755            tickMarkPath.setLayoutY(0.5);
756            if (getLabel() != null) {
757                axisLabel.getTransforms().setAll(new Translate(0, height), new Rotate(-90, 0, 0));
758                axisLabel.setLayoutX(0);
759                axisLabel.setLayoutY(0);
760                //noinspection SuspiciousNameCombination
761                axisLabel.resize(height, Math.ceil(axisLabel.prefHeight(width)));
762            }
763            tickIndex = 0;
764            for (TickMark<T> tick : tickMarks) {
765                tick.setPosition(getDisplayPosition(tick.getValue()));
766                positionTextNode(tick.textNode, width - getTickLabelGap() - tickMarkLength,
767                                 tick.getPosition(),getTickLabelRotation(),side);
768
769                // check if position is inside bounds
770                if(tick.getPosition() >= 0 && tick.getPosition() <= Math.ceil(length)) {
771                    if (isTickLabelsVisible()) {
772                        tick.textNode.setVisible((tickIndex % numLabelsToSkip) == 0);
773                        tickIndex++;
774                    }
775                    // add tick mark line
776                    tickMarkPath.getElements().addAll(
777                        new MoveTo(width - tickMarkLength, tick.getPosition()),
778                        new LineTo(width, tick.getPosition())
779                    );
780                } else {
781                    tick.textNode.setVisible(false);
782                }
783            }
784        } else if (getSide().equals(Side.RIGHT)) {
785            // offset path to make strokes snap to pixel
786            tickMarkPath.setLayoutX(0.5);
787            tickMarkPath.setLayoutY(0.5);
788            tickIndex = 0;
789            for (TickMark<T> tick : tickMarks) {
790                tick.setPosition(getDisplayPosition(tick.getValue()));
791                positionTextNode(tick.textNode, getTickLabelGap() + tickMarkLength,
792                                 tick.getPosition(),getTickLabelRotation(),side);
793                // check if position is inside bounds
794                if(tick.getPosition() >= 0 && tick.getPosition() <= Math.ceil(length)) {
795                    if (isTickLabelsVisible()) {
796                        tick.textNode.setVisible((tickIndex % numLabelsToSkip) == 0);
797                        tickIndex++;
798                    }
799                    // add tick mark line
800                    tickMarkPath.getElements().addAll(
801                        new MoveTo(0, tick.getPosition()),
802                        new LineTo(tickMarkLength, tick.getPosition())
803                    );
804                } else {
805                    tick.textNode.setVisible(false);
806                }
807            }
808            if (getLabel() != null) {
809                final double axisLabelWidth = Math.ceil(axisLabel.prefHeight(width));
810                axisLabel.getTransforms().setAll(new Translate(0, height), new Rotate(-90, 0, 0));
811                axisLabel.setLayoutX(width-axisLabelWidth);
812                axisLabel.setLayoutY(0);
813                //noinspection SuspiciousNameCombination
814                axisLabel.resize(height, axisLabelWidth);
815            }
816        } else if (getSide().equals(Side.TOP)) {
817            // offset path to make strokes snap to pixel
818            tickMarkPath.setLayoutX(0.5);
819            tickMarkPath.setLayoutY(-0.5);
820            if (getLabel() != null) {
821                axisLabel.getTransforms().clear();
822                axisLabel.setLayoutX(0);
823                axisLabel.setLayoutY(0);
824                axisLabel.resize(width, Math.ceil(axisLabel.prefHeight(width)));
825            }
826            tickIndex = 0;
827            for (TickMark<T> tick : tickMarks) {
828                tick.setPosition(getDisplayPosition(tick.getValue()));
829                positionTextNode(tick.textNode, tick.getPosition(), height - tickMarkLength - getTickLabelGap(),
830                        getTickLabelRotation(), side);
831                // check if position is inside bounds
832                if(tick.getPosition() >= 0 && tick.getPosition() <= Math.ceil(length)) {
833                    if (isTickLabelsVisible()) {
834                        tick.textNode.setVisible((tickIndex % numLabelsToSkip) == 0);
835                        tickIndex++;
836                    }
837                    // add tick mark line
838                    tickMarkPath.getElements().addAll(
839                        new MoveTo(tick.getPosition(), height),
840                        new LineTo(tick.getPosition(), height - tickMarkLength)
841                    );
842                } else {
843                    tick.textNode.setVisible(false);
844                }
845            }
846        } else {
847            // BOTTOM
848            // offset path to make strokes snap to pixel
849            tickMarkPath.setLayoutX(0.5);
850            tickMarkPath.setLayoutY(0.5);
851            tickIndex = 0;
852            for (TickMark<T> tick : tickMarks) {
853                final double xPos = Math.round(getDisplayPosition(tick.getValue()));
854                tick.setPosition(xPos);
855//                System.out.println("tick pos at : "+tickIndex+" = "+xPos);
856                positionTextNode(tick.textNode,xPos, tickMarkLength + getTickLabelGap(),
857                                getTickLabelRotation(),side);
858                // check if position is inside bounds
859                if(xPos >= 0 && xPos <= Math.ceil(length)) {
860                    if (isTickLabelsVisible()) {
861                        tick.textNode.setVisible((tickIndex % numLabelsToSkip) == 0);
862                        tickIndex++;
863                    }
864                    // add tick mark line
865                    tickMarkPath.getElements().addAll(
866                        new MoveTo(xPos, 0),
867                        new LineTo(xPos, tickMarkLength)
868                    );
869                } else {
870                    tick.textNode.setVisible(false);
871                }
872            }
873            if (getLabel() != null) {
874                axisLabel.getTransforms().clear();
875                final double labelHeight = Math.ceil(axisLabel.prefHeight(width));
876                axisLabel.setLayoutX(0);
877                axisLabel.setLayoutY(height-labelHeight);
878                axisLabel.resize(width, labelHeight);
879            }
880        }
881    }
882
883    /**
884     * Positions a text node to one side of the given point, it X height is vertically centered on point if LEFT or
885     * RIGHT and its centered horizontally if TOP ot BOTTOM.
886     *
887     * @param node The text node to position
888     * @param posX The x position, to place text next to
889     * @param posY The y position, to place text next to
890     * @param angle The text rotation
891     * @param side The side to place text next to position x,y at
892     */
893    private void positionTextNode(Text node, double posX, double posY, double angle, Side side) {
894        node.setLayoutX(0);
895        node.setLayoutY(0);
896        node.setRotate(angle);
897        final Bounds bounds = node.getBoundsInParent();
898        if (side.equals(Side.LEFT)) {
899            node.setLayoutX(posX-bounds.getWidth()-bounds.getMinX());
900            node.setLayoutY(posY - (bounds.getHeight() / 2d) - bounds.getMinY());
901        } else if (side.equals(Side.RIGHT)) {
902            node.setLayoutX(posX-bounds.getMinX());
903            node.setLayoutY(posY-(bounds.getHeight()/2d)-bounds.getMinY());
904        } else if (side.equals(Side.TOP)) {
905            node.setLayoutX(posX-(bounds.getWidth()/2d)-bounds.getMinX());
906            node.setLayoutY(posY-bounds.getHeight()-bounds.getMinY());
907        } else {
908            node.setLayoutX(posX-(bounds.getWidth()/2d)-bounds.getMinX());
909            node.setLayoutY(posY-bounds.getMinY());
910        }
911    }
912
913    /**
914     * Get the string label name for a tick mark with the given value
915     *
916     * @param value The value to format into a tick label string
917     * @return A formatted string for the given value
918     */
919    protected abstract String getTickMarkLabel(T value);
920
921    /**
922     * Measure the size of the label for given tick mark value. This uses the font that is set for the tick marks
923     *
924     *
925     * @param labelText     tick mark label text
926     * @param rotation  The text rotation
927     * @return size of tick mark label for given value
928     */
929    protected final Dimension2D measureTickMarkLabelSize(String labelText, double rotation) {
930        measure.setRotate(rotation);
931        measure.setText(labelText);
932        Bounds bounds = measure.getBoundsInParent();
933        return new Dimension2D(bounds.getWidth(), bounds.getHeight());
934    }
935
936    /**
937     * Measure the size of the label for given tick mark value. This uses the font that is set for the tick marks
938     *
939     * @param value     tick mark value
940     * @param rotation  The text rotation
941     * @return size of tick mark label for given value
942     */
943    protected final Dimension2D measureTickMarkSize(T value, double rotation) {
944        return measureTickMarkLabelSize(getTickMarkLabel(value), rotation);
945    }
946
947    /**
948     * Measure the size of the label for given tick mark value. This uses the font that is set for the tick marks
949     *
950     * @param value tick mark value
951     * @param range range to use during calculations
952     * @return size of tick mark label for given value
953     */
954    protected Dimension2D measureTickMarkSize(T value, Object range) {
955        return measureTickMarkSize(value,getTickLabelRotation());
956    }
957
958    // -------------- TICKMARK INNER CLASS -----------------------------------------------------------------------------
959
960    /**
961     * TickMark represents the label text, its associated properties for each tick
962     * along the Axis.
963     */
964    public static final class TickMark<T> {
965        /**
966         * The display text for tick mark
967         */
968        private StringProperty label = new StringPropertyBase() {
969            @Override protected void invalidated() {
970                textNode.setText(getValue());
971            }
972
973            @Override
974            public Object getBean() {
975                return TickMark.this;
976            }
977
978            @Override
979            public String getName() {
980                return "label";
981            }
982        };
983        public final String getLabel() { return label.get(); }
984        public final void setLabel(String value) { label.set(value); }
985        public final StringExpression labelProperty() { return label; }
986
987        /**
988         * The value for this tick mark in data units
989         */
990        private ObjectProperty<T> value = new SimpleObjectProperty<T>(this, "value");
991        public final T getValue() { return value.get(); }
992        public final void setValue(T v) { value.set(v); }
993        public final ObjectExpression<T> valueProperty() { return value; }
994
995        /**
996         * The display position along the axis from axis origin in display units
997         */
998        private DoubleProperty position = new SimpleDoubleProperty(this, "position");
999        public final double getPosition() { return position.get(); }
1000        public final void setPosition(double value) { position.set(value); }
1001        public final DoubleExpression positionProperty() { return position; }
1002
1003        Text textNode = new Text();
1004
1005        /** true if tick mark labels should be displayed */
1006        private BooleanProperty textVisible = new BooleanPropertyBase(true) {
1007            @Override protected void invalidated() {
1008                if(!get()) {
1009                    textNode.setVisible(false);
1010                }
1011            }
1012
1013            @Override
1014            public Object getBean() {
1015                return TickMark.this;
1016            }
1017
1018            @Override
1019            public String getName() {
1020                return "textVisible";
1021            }
1022        };
1023
1024        /**
1025         * Indicates whether this tick mark label text is displayed or not.
1026         * @return true if tick mark label text is visible and false otherwise
1027         */
1028        public final boolean isTextVisible() { return textVisible.get(); }
1029        public final void setTextVisible(boolean value) { textVisible.set(value); }
1030
1031        /**
1032         * Creates and initializes an instance of TickMark. 
1033         */
1034        public TickMark() {
1035        }
1036
1037        /**
1038         * Returns a string representation of this {@code TickMark} object.
1039         * @return a string representation of this {@code TickMark} object.
1040         */ 
1041        @Override public String toString() {
1042            return value.get().toString();
1043        }
1044    }
1045
1046    // -------------- STYLESHEET HANDLING ------------------------------------------------------------------------------
1047
1048    /** @treatAsPrivate implementation detail */
1049    private static class StyleableProperties {
1050        private static final CssMetaData<Axis<?>,Side> SIDE =
1051            new CssMetaData<Axis<?>,Side>("-fx-side",
1052                new EnumConverter<Side>(Side.class)) {
1053
1054            @Override
1055            public boolean isSettable(Axis n) {
1056                return n.side == null || !n.side.isBound();
1057            }
1058
1059            @SuppressWarnings("unchecked") // sideProperty() is StyleableProperty<Side>              
1060            @Override
1061            public StyleableProperty<Side> getStyleableProperty(Axis n) {
1062                return (StyleableProperty<Side>)n.sideProperty();
1063            }
1064        };
1065        
1066        private static final CssMetaData<Axis<?>,Number> TICK_LENGTH =
1067            new CssMetaData<Axis<?>,Number>("-fx-tick-length",
1068                SizeConverter.getInstance(), 8.0) {
1069
1070            @Override
1071            public boolean isSettable(Axis n) {
1072                return n.tickLength == null || !n.tickLength.isBound();
1073            }
1074
1075            @Override
1076            public StyleableProperty<Number> getStyleableProperty(Axis n) {
1077                return (StyleableProperty<Number>)n.tickLengthProperty();
1078            }
1079        };
1080        
1081        private static final CssMetaData<Axis<?>,Font> TICK_LABEL_FONT =
1082            new FontCssMetaData<Axis<?>>("-fx-tick-label-font",
1083                Font.font("system", 8.0)) {
1084
1085            @Override
1086            public boolean isSettable(Axis n) {
1087                return n.tickLabelFont == null || !n.tickLabelFont.isBound();
1088            }
1089
1090            @SuppressWarnings("unchecked") // tickLabelFontProperty() is StyleableProperty<Font>              
1091            @Override
1092            public StyleableProperty<Font> getStyleableProperty(Axis n) {
1093                return (StyleableProperty<Font>)n.tickLabelFontProperty();
1094            }
1095        };
1096
1097        private static final CssMetaData<Axis<?>,Paint> TICK_LABEL_FILL =
1098            new CssMetaData<Axis<?>,Paint>("-fx-tick-label-fill",
1099                PaintConverter.getInstance(), Color.BLACK) {
1100
1101            @Override
1102            public boolean isSettable(Axis n) {
1103                return n.tickLabelFill == null | !n.tickLabelFill.isBound();
1104            }
1105
1106            @SuppressWarnings("unchecked") // tickLabelFillProperty() is StyleableProperty<Paint>            
1107            @Override
1108            public StyleableProperty<Paint> getStyleableProperty(Axis n) {
1109                return (StyleableProperty<Paint>)n.tickLabelFillProperty();
1110            }
1111        };
1112        
1113        private static final CssMetaData<Axis<?>,Number> TICK_LABEL_TICK_GAP =
1114            new CssMetaData<Axis<?>,Number>("-fx-tick-label-gap",
1115                SizeConverter.getInstance(), 3.0) {
1116
1117            @Override
1118            public boolean isSettable(Axis n) {
1119                return n.tickLabelGap == null || !n.tickLabelGap.isBound();
1120            }
1121
1122            @Override
1123            public StyleableProperty<Number> getStyleableProperty(Axis n) {
1124                return (StyleableProperty<Number>)n.tickLabelGapProperty();
1125            }
1126        };
1127        
1128        private static final CssMetaData<Axis<?>,Boolean> TICK_MARK_VISIBLE =
1129            new CssMetaData<Axis<?>,Boolean>("-fx-tick-mark-visible",
1130                BooleanConverter.getInstance(), Boolean.TRUE) {
1131
1132            @Override
1133            public boolean isSettable(Axis n) {
1134                return n.tickMarkVisible == null || !n.tickMarkVisible.isBound();
1135            }
1136
1137            @Override
1138            public StyleableProperty<Boolean> getStyleableProperty(Axis n) {
1139                return (StyleableProperty<Boolean>)n.tickMarkVisibleProperty();
1140            }
1141        };
1142        
1143        private static final CssMetaData<Axis<?>,Boolean> TICK_LABELS_VISIBLE =
1144            new CssMetaData<Axis<?>,Boolean>("-fx-tick-labels-visible",
1145                BooleanConverter.getInstance(), Boolean.TRUE) {
1146
1147            @Override
1148            public boolean isSettable(Axis n) {
1149                return n.tickLabelsVisible == null || !n.tickLabelsVisible.isBound();
1150            }
1151
1152            @Override
1153            public StyleableProperty<Boolean> getStyleableProperty(Axis n) {
1154                return (StyleableProperty<Boolean>)n.tickLabelsVisibleProperty();
1155            }
1156        };
1157
1158        private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
1159        static {
1160        final List<CssMetaData<? extends Styleable, ?>> styleables =
1161            new ArrayList<CssMetaData<? extends Styleable, ?>>(Region.getClassCssMetaData());
1162            styleables.add(SIDE);
1163            styleables.add(TICK_LENGTH);
1164            styleables.add(TICK_LABEL_FONT);
1165            styleables.add(TICK_LABEL_FILL);
1166            styleables.add(TICK_LABEL_TICK_GAP);
1167            styleables.add(TICK_MARK_VISIBLE);
1168            styleables.add(TICK_LABELS_VISIBLE);
1169            STYLEABLES = Collections.unmodifiableList(styleables);
1170        }
1171    }
1172
1173    /**
1174     * @return The CssMetaData associated with this class, which may include the
1175     * CssMetaData of its super classes.
1176     */
1177    public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
1178        return StyleableProperties.STYLEABLES;
1179    }
1180
1181    /**
1182     * {@inheritDoc}
1183     */
1184    @Override
1185    public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
1186        return getClassCssMetaData();
1187    }
1188
1189    /** pseudo-class indicating this is a vertical Top side Axis. */
1190    private static final PseudoClass TOP_PSEUDOCLASS_STATE =
1191            PseudoClass.getPseudoClass("top");
1192    /** pseudo-class indicating this is a vertical Bottom side Axis. */
1193    private static final PseudoClass BOTTOM_PSEUDOCLASS_STATE =
1194            PseudoClass.getPseudoClass("bottom");
1195    /** pseudo-class indicating this is a vertical Left side Axis. */
1196    private static final PseudoClass LEFT_PSEUDOCLASS_STATE =
1197            PseudoClass.getPseudoClass("left");
1198    /** pseudo-class indicating this is a vertical Right side Axis. */
1199    private static final PseudoClass RIGHT_PSEUDOCLASS_STATE =
1200            PseudoClass.getPseudoClass("right");
1201
1202}