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