1 /*
   2  * Copyright (c) 2010, 2017, 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 java.util.ArrayList;
  29 import java.util.List;
  30 
  31 import javafx.animation.KeyFrame;
  32 import javafx.animation.KeyValue;
  33 import javafx.beans.property.BooleanProperty;
  34 import javafx.beans.property.DoubleProperty;
  35 import javafx.beans.property.ObjectProperty;
  36 import javafx.beans.property.ObjectPropertyBase;
  37 import javafx.beans.property.ReadOnlyDoubleProperty;
  38 import javafx.beans.property.ReadOnlyDoubleWrapper;
  39 import javafx.beans.property.SimpleDoubleProperty;
  40 import javafx.beans.value.WritableValue;
  41 import javafx.collections.FXCollections;
  42 import javafx.collections.ListChangeListener;
  43 import javafx.collections.ObservableList;
  44 import javafx.geometry.Dimension2D;
  45 import javafx.geometry.Side;
  46 import javafx.util.Duration;
  47 
  48 import com.sun.javafx.charts.ChartLayoutAnimator;
  49 
  50 import javafx.css.StyleableBooleanProperty;
  51 import javafx.css.StyleableDoubleProperty;
  52 import javafx.css.CssMetaData;
  53 
  54 import javafx.css.converter.BooleanConverter;
  55 import javafx.css.converter.SizeConverter;
  56 
  57 import java.util.Collections;
  58 
  59 import javafx.css.Styleable;
  60 import javafx.css.StyleableProperty;
  61 
  62 /**
  63  * A axis implementation that will works on string categories where each
  64  * value as a unique category(tick mark) along the axis.
  65  * @since JavaFX 2.0
  66  */
  67 public final class CategoryAxis extends Axis<String> {
  68 
  69     // -------------- PRIVATE FIELDS -------------------------------------------
  70     private List<String> allDataCategories = new ArrayList<String>();
  71     private boolean changeIsLocal = false;
  72     /** This is the gap between one category and the next along this axis */
  73     private final DoubleProperty firstCategoryPos = new SimpleDoubleProperty(this, "firstCategoryPos", 0);
  74     private Object currentAnimationID;
  75     private final ChartLayoutAnimator animator = new ChartLayoutAnimator(this);
  76     private ListChangeListener<String> itemsListener = c -> {
  77         while (c.next()) {
  78             if(!c.getAddedSubList().isEmpty()) {
  79                 // remove duplicates else they will get rendered on the chart.
  80                 // Ideally we should be using a Set for categories.
  81                 for (String addedStr : c.getAddedSubList())
  82                     checkAndRemoveDuplicates(addedStr);
  83                 }
  84             if (!isAutoRanging()) {
  85                 allDataCategories.clear();
  86                 allDataCategories.addAll(getCategories());
  87                 rangeValid = false;
  88             }
  89             requestAxisLayout();
  90         }
  91     };
  92 
  93     // -------------- PUBLIC PROPERTIES ----------------------------------------
  94 
  95     /** The margin between the axis start and the first tick-mark */
  96     private DoubleProperty startMargin = new StyleableDoubleProperty(5) {
  97         @Override protected void invalidated() {
  98             requestAxisLayout();
  99         }
 100 
 101         @Override public CssMetaData<CategoryAxis,Number> getCssMetaData() {
 102             return StyleableProperties.START_MARGIN;
 103         }
 104 
 105         @Override
 106         public Object getBean() {
 107             return CategoryAxis.this;
 108         }
 109 
 110         @Override
 111         public String getName() {
 112             return "startMargin";
 113         }
 114     };
 115     public final double getStartMargin() { return startMargin.getValue(); }
 116     public final void setStartMargin(double value) { startMargin.setValue(value); }
 117     public final DoubleProperty startMarginProperty() { return startMargin; }
 118 
 119     /** The margin between the last tick mark and the axis end */
 120     private DoubleProperty endMargin = new StyleableDoubleProperty(5) {
 121         @Override protected void invalidated() {
 122             requestAxisLayout();
 123         }
 124 
 125 
 126         @Override public CssMetaData<CategoryAxis,Number> getCssMetaData() {
 127             return StyleableProperties.END_MARGIN;
 128         }
 129 
 130         @Override
 131         public Object getBean() {
 132             return CategoryAxis.this;
 133         }
 134 
 135         @Override
 136         public String getName() {
 137             return "endMargin";
 138         }
 139     };
 140     public final double getEndMargin() { return endMargin.getValue(); }
 141     public final void setEndMargin(double value) { endMargin.setValue(value); }
 142     public final DoubleProperty endMarginProperty() { return endMargin; }
 143 
 144     /** If this is true then half the space between ticks is left at the start
 145      * and end
 146      */
 147     private BooleanProperty gapStartAndEnd = new StyleableBooleanProperty(true) {
 148         @Override protected void invalidated() {
 149             requestAxisLayout();
 150         }
 151 
 152 
 153         @Override public CssMetaData<CategoryAxis,Boolean> getCssMetaData() {
 154             return StyleableProperties.GAP_START_AND_END;
 155         }
 156 
 157         @Override
 158         public Object getBean() {
 159             return CategoryAxis.this;
 160         }
 161 
 162         @Override
 163         public String getName() {
 164             return "gapStartAndEnd";
 165         }
 166     };
 167     public final boolean isGapStartAndEnd() { return gapStartAndEnd.getValue(); }
 168     public final void setGapStartAndEnd(boolean value) { gapStartAndEnd.setValue(value); }
 169     public final BooleanProperty gapStartAndEndProperty() { return gapStartAndEnd; }
 170 
 171     private ObjectProperty<ObservableList<String>> categories = new ObjectPropertyBase<ObservableList<String>>() {
 172         ObservableList<String> old;
 173         @Override protected void invalidated() {
 174             if (getDuplicate() != null) {
 175                 throw new IllegalArgumentException("Duplicate category added; "+getDuplicate()+" already present");
 176             }
 177             final ObservableList<String> newItems = get();
 178             if (old != newItems) {
 179                 // Add and remove listeners
 180                 if (old != null) old.removeListener(itemsListener);
 181                 if (newItems != null) newItems.addListener(itemsListener);
 182                 old = newItems;
 183             }
 184         }
 185 
 186         @Override
 187         public Object getBean() {
 188             return CategoryAxis.this;
 189         }
 190 
 191         @Override
 192         public String getName() {
 193             return "categories";
 194         }
 195     };
 196 
 197     /**
 198      * The ordered list of categories plotted on this axis. This is set automatically
 199      * based on the charts data if autoRanging is true. If the application sets the categories
 200      * then auto ranging is turned off. If there is an attempt to add duplicate entry into this list,
 201      * an {@link IllegalArgumentException} is thrown.
 202      * @param value the ordered list of categories plotted on this axis
 203      */
 204     public final void setCategories(ObservableList<String> value) {
 205         categories.set(value);
 206         if (!changeIsLocal) {
 207             setAutoRanging(false);
 208             allDataCategories.clear();
 209             allDataCategories.addAll(getCategories());
 210         }
 211         requestAxisLayout();
 212     }
 213 
 214     private void checkAndRemoveDuplicates(String category) {
 215         if (getDuplicate() != null) {
 216             getCategories().remove(category);
 217             throw new IllegalArgumentException("Duplicate category ; "+category+" already present");
 218         }
 219     }
 220 
 221     private String getDuplicate() {
 222         if (getCategories() != null) {
 223             for (int i = 0; i < getCategories().size(); i++) {
 224                 for (int j = 0; j < getCategories().size(); j++) {
 225                     if (getCategories().get(i).equals(getCategories().get(j)) && i != j) {
 226                         return getCategories().get(i);
 227                     }
 228                 }
 229             }
 230         }
 231         return null;
 232     }
 233     /**
 234      * Returns a {@link ObservableList} of categories plotted on this axis.
 235      *
 236      * @return ObservableList of categories for this axis.
 237      * @see #categories
 238      */
 239     public final ObservableList<String> getCategories() {
 240         return categories.get();
 241     }
 242 
 243     /** This is the gap between one category and the next along this axis */
 244     private final ReadOnlyDoubleWrapper categorySpacing = new ReadOnlyDoubleWrapper(this, "categorySpacing", 1);
 245     public final double getCategorySpacing() {
 246         return categorySpacing.get();
 247     }
 248     public final ReadOnlyDoubleProperty categorySpacingProperty() {
 249         return categorySpacing.getReadOnlyProperty();
 250     }
 251 
 252     // -------------- CONSTRUCTORS -------------------------------------------------------------------------------------
 253 
 254     /**
 255      * Create a auto-ranging category axis with an empty list of categories.
 256      */
 257     public CategoryAxis() {
 258         changeIsLocal = true;
 259         setCategories(FXCollections.<String>observableArrayList());
 260         changeIsLocal = false;
 261     }
 262 
 263     /**
 264      * Create a category axis with the given categories. This will not auto-range but be fixed with the given categories.
 265      *
 266      * @param categories List of the categories for this axis
 267      */
 268     public CategoryAxis(ObservableList<String> categories) {
 269         setCategories(categories);
 270     }
 271 
 272     // -------------- PRIVATE METHODS ----------------------------------------------------------------------------------
 273 
 274     private double calculateNewSpacing(double length, List<String> categories) {
 275         final Side side = getEffectiveSide();
 276         double newCategorySpacing = 1;
 277         if(categories != null) {
 278             double bVal = (isGapStartAndEnd() ? (categories.size()) : (categories.size() - 1));
 279             // RT-14092 flickering  : check if bVal is 0
 280             newCategorySpacing = (bVal == 0) ? 1 : (length-getStartMargin()-getEndMargin()) / bVal;
 281         }
 282         // if autoranging is off setRange is not called so we update categorySpacing
 283         if (!isAutoRanging()) categorySpacing.set(newCategorySpacing);
 284         return newCategorySpacing;
 285     }
 286 
 287     private double calculateNewFirstPos(double length, double catSpacing) {
 288         final Side side = getEffectiveSide();
 289         double newPos = 1;
 290         double offset = ((isGapStartAndEnd()) ? (catSpacing / 2) : (0));
 291         if (side.isHorizontal()) {
 292             newPos = 0 + getStartMargin() + offset;
 293         }  else { // VERTICAL
 294             newPos = length - getStartMargin() - offset;
 295         }
 296         // if autoranging is off setRange is not called so we update first cateogory pos.
 297         if (!isAutoRanging()) firstCategoryPos.set(newPos);
 298         return newPos;
 299     }
 300 
 301     // -------------- PROTECTED METHODS --------------------------------------------------------------------------------
 302 
 303     /**
 304      * Called to get the current axis range.
 305      *
 306      * @return A range object that can be passed to setRange() and calculateTickValues()
 307      */
 308     @Override protected Object getRange() {
 309         return new Object[]{ getCategories(), categorySpacing.get(), firstCategoryPos.get(), getEffectiveTickLabelRotation() };
 310     }
 311 
 312     /**
 313      * Called to set the current axis range to the given range. If isAnimating() is true then this method should
 314      * animate the range to the new range.
 315      *
 316      * @param range A range object returned from autoRange()
 317      * @param animate If true animate the change in range
 318      */
 319     @Override protected void setRange(Object range, boolean animate) {
 320         Object[] rangeArray = (Object[]) range;
 321         @SuppressWarnings({"unchecked"}) List<String> categories = (List<String>)rangeArray[0];
 322 //        if (categories.isEmpty()) new java.lang.Throwable().printStackTrace();
 323         double newCategorySpacing = (Double)rangeArray[1];
 324         double newFirstCategoryPos = (Double)rangeArray[2];
 325         setEffectiveTickLabelRotation((Double)rangeArray[3]);
 326 
 327         changeIsLocal = true;
 328         setCategories(FXCollections.<String>observableArrayList(categories));
 329         changeIsLocal = false;
 330         if (animate) {
 331             animator.stop(currentAnimationID);
 332             currentAnimationID = animator.animate(
 333                 new KeyFrame(Duration.ZERO,
 334                     new KeyValue(firstCategoryPos, firstCategoryPos.get()),
 335                     new KeyValue(categorySpacing, categorySpacing.get())
 336                 ),
 337                 new KeyFrame(Duration.millis(1000),
 338                     new KeyValue(firstCategoryPos,newFirstCategoryPos),
 339                     new KeyValue(categorySpacing,newCategorySpacing)
 340                 )
 341             );
 342         } else {
 343             categorySpacing.set(newCategorySpacing);
 344             firstCategoryPos.set(newFirstCategoryPos);
 345         }
 346     }
 347 
 348     /**
 349      * This calculates the categories based on the data provided to invalidateRange() method. This must not
 350      * effect the state of the axis, changing any properties of the axis. Any results of the auto-ranging should be
 351      * returned in the range object. This will we passed to setRange() if it has been decided to adopt this range for
 352      * this axis.
 353      *
 354      * @param length The length of the axis in screen coordinates
 355      * @return Range information, this is implementation dependent
 356      */
 357     @Override protected Object autoRange(double length) {
 358         final Side side = getEffectiveSide();
 359         // TODO check if we can display all categories
 360         final double newCategorySpacing = calculateNewSpacing(length,allDataCategories);
 361         final double newFirstPos = calculateNewFirstPos(length, newCategorySpacing);
 362         double tickLabelRotation = getTickLabelRotation();
 363         if (length >= 0) {
 364             double requiredLengthToDisplay = calculateRequiredSize(side.isVertical(), tickLabelRotation);
 365             if (requiredLengthToDisplay > length) {
 366                 // try to rotate the text to increase the density
 367                 if (side.isHorizontal() && tickLabelRotation != 90) {
 368                     tickLabelRotation = 90;
 369                 }
 370                 if (side.isVertical() && tickLabelRotation != 0) {
 371                     tickLabelRotation = 0;
 372                 }
 373             }
 374         }
 375         return new Object[]{allDataCategories, newCategorySpacing, newFirstPos, tickLabelRotation};
 376     }
 377 
 378     private double calculateRequiredSize(boolean axisVertical, double tickLabelRotation) {
 379         // Calculate the max space required between categories labels
 380         double maxReqTickGap = 0;
 381         double last = 0;
 382         boolean first = true;
 383         for (String category: allDataCategories) {
 384             Dimension2D textSize = measureTickMarkSize(category, tickLabelRotation);
 385             double size = (axisVertical || (tickLabelRotation != 0)) ? textSize.getHeight() : textSize.getWidth();
 386             // TODO better handle calculations for rotated text, overlapping text etc
 387             if (first) {
 388                 first = false;
 389                 last = size/2;
 390             } else {
 391                 maxReqTickGap = Math.max(maxReqTickGap, last + 6 + (size/2) );
 392             }
 393         }
 394         return getStartMargin() + maxReqTickGap*allDataCategories.size() + getEndMargin();
 395     }
 396 
 397     /**
 398      * Calculate a list of all the data values for each tick mark in range
 399      *
 400      * @param length The length of the axis in display units
 401      * @return A list of tick marks that fit along the axis if it was the given length
 402      */
 403     @Override protected List<String> calculateTickValues(double length, Object range) {
 404         Object[] rangeArray = (Object[]) range;
 405         //noinspection unchecked
 406         return (List<String>)rangeArray[0];
 407     }
 408 
 409     /**
 410      * Get the string label name for a tick mark with the given value
 411      *
 412      * @param value The value to format into a tick label string
 413      * @return A formatted string for the given value
 414      */
 415     @Override protected String getTickMarkLabel(String value) {
 416         // TODO use formatter
 417         return value;
 418     }
 419 
 420     /**
 421      * Measure the size of the label for given tick mark value. This uses the font that is set for the tick marks
 422      *
 423      * @param value tick mark value
 424      * @param range range to use during calculations
 425      * @return size of tick mark label for given value
 426      */
 427     @Override protected Dimension2D measureTickMarkSize(String value, Object range) {
 428         final Object[] rangeArray = (Object[]) range;
 429         final double tickLabelRotation = (Double)rangeArray[3];
 430         return measureTickMarkSize(value,tickLabelRotation);
 431     }
 432 
 433     // -------------- METHODS ------------------------------------------------------------------------------------------
 434 
 435     /**
 436      * Called when data has changed and the range may not be valid any more. This is only called by the chart if
 437      * isAutoRanging() returns true. If we are auto ranging it will cause layout to be requested and auto ranging to
 438      * happen on next layout pass.
 439      *
 440      * @param data The current set of all data that needs to be plotted on this axis
 441      */
 442     @Override public void invalidateRange(List<String> data) {
 443         super.invalidateRange(data);
 444         // Create unique set of category names
 445         List<String> categoryNames = new ArrayList<String>();
 446         categoryNames.addAll(allDataCategories);
 447         //RT-21141 allDataCategories needs to be updated based on data -
 448         // and should maintain the order it originally had for the categories already present.
 449         // and remove categories not present in data
 450         for(String cat : allDataCategories) {
 451             if (!data.contains(cat)) categoryNames.remove(cat);
 452         }
 453         // add any new category found in data
 454 //        for(String cat : data) {
 455         for (int i = 0; i < data.size(); i++) {
 456            int len = categoryNames.size();
 457            if (!categoryNames.contains(data.get(i))) categoryNames.add((i > len) ? len : i, data.get(i));
 458         }
 459         allDataCategories.clear();
 460         allDataCategories.addAll(categoryNames);
 461     }
 462 
 463     final List<String> getAllDataCategories() {
 464         return allDataCategories;
 465     }
 466 
 467     /**
 468      * Get the display position along this axis for a given value.
 469      *
 470      * If the value is not equal to any of the categories, Double.NaN is returned
 471      *
 472      * @param value The data value to work out display position for
 473      * @return display position or Double.NaN if value not one of the categories
 474      */
 475     @Override public double getDisplayPosition(String value) {
 476         // find index of value
 477         final ObservableList<String> cat = getCategories();
 478         if (!cat.contains(value)) {
 479             return Double.NaN;
 480         }
 481         if (getEffectiveSide().isHorizontal()) {
 482             return firstCategoryPos.get() + cat.indexOf(value) * categorySpacing.get();
 483         } else {
 484             return firstCategoryPos.get() + cat.indexOf(value) * categorySpacing.get() * -1;
 485         }
 486     }
 487 
 488     /**
 489      * Get the data value for the given display position on this axis. If the axis
 490      * is a CategoryAxis this will be the nearest value.
 491      *
 492      * @param  displayPosition A pixel position on this axis
 493      * @return the nearest data value to the given pixel position or
 494      *         null if not on axis;
 495      */
 496     @Override public String getValueForDisplay(double displayPosition) {
 497         if (getEffectiveSide().isHorizontal()) {
 498             if (displayPosition < 0 || displayPosition > getWidth()) return null;
 499             double d = (displayPosition - firstCategoryPos.get()) /   categorySpacing.get();
 500             return toRealValue(d);
 501         } else { // VERTICAL
 502             if (displayPosition < 0 || displayPosition > getHeight()) return null;
 503             double d = (displayPosition - firstCategoryPos.get()) /   (categorySpacing.get() * -1);
 504             return toRealValue(d);
 505         }
 506     }
 507 
 508     /**
 509      * Checks if the given value is plottable on this axis
 510      *
 511      * @param value The value to check if its on axis
 512      * @return true if the given value is plottable on this axis
 513      */
 514     @Override public boolean isValueOnAxis(String value) {
 515         return getCategories().indexOf("" + value) != -1;
 516     }
 517 
 518     /**
 519      * All axis values must be representable by some numeric value. This gets the numeric value for a given data value.
 520      *
 521      * @param value The data value to convert
 522      * @return Numeric value for the given data value
 523      */
 524     @Override public double toNumericValue(String value) {
 525         return getCategories().indexOf(value);
 526     }
 527 
 528     /**
 529      * All axis values must be representable by some numeric value. This gets the data value for a given numeric value.
 530      *
 531      * @param value The numeric value to convert
 532      * @return Data value for given numeric value
 533      */
 534     @Override public String toRealValue(double value) {
 535         int index = (int)Math.round(value);
 536         List<String> categories = getCategories();
 537         if (index >= 0 && index < categories.size()) {
 538             return getCategories().get(index);
 539         } else {
 540             return null;
 541         }
 542     }
 543 
 544     /**
 545      * Get the display position of the zero line along this axis. As there is no concept of zero on a CategoryAxis
 546      * this is always Double.NaN.
 547      *
 548      * @return always Double.NaN for CategoryAxis
 549      */
 550     @Override public double getZeroPosition() {
 551         return Double.NaN;
 552     }
 553 
 554     // -------------- STYLESHEET HANDLING ------------------------------------------------------------------------------
 555 
 556     private static class StyleableProperties {
 557         private static final CssMetaData<CategoryAxis,Number> START_MARGIN =
 558             new CssMetaData<CategoryAxis,Number>("-fx-start-margin",
 559                 SizeConverter.getInstance(), 5.0) {
 560 
 561             @Override
 562             public boolean isSettable(CategoryAxis n) {
 563                 return n.startMargin == null || !n.startMargin.isBound();
 564             }
 565 
 566             @Override
 567             public StyleableProperty<Number> getStyleableProperty(CategoryAxis n) {
 568                 return (StyleableProperty<Number>)(WritableValue<Number>)n.startMarginProperty();
 569             }
 570         };
 571 
 572         private static final CssMetaData<CategoryAxis,Number> END_MARGIN =
 573             new CssMetaData<CategoryAxis,Number>("-fx-end-margin",
 574                 SizeConverter.getInstance(), 5.0) {
 575 
 576             @Override
 577             public boolean isSettable(CategoryAxis n) {
 578                 return n.endMargin == null || !n.endMargin.isBound();
 579             }
 580 
 581             @Override
 582             public StyleableProperty<Number> getStyleableProperty(CategoryAxis n) {
 583                 return (StyleableProperty<Number>)(WritableValue<Number>)n.endMarginProperty();
 584             }
 585         };
 586 
 587         private static final CssMetaData<CategoryAxis,Boolean> GAP_START_AND_END =
 588             new CssMetaData<CategoryAxis,Boolean>("-fx-gap-start-and-end",
 589                 BooleanConverter.getInstance(), Boolean.TRUE) {
 590 
 591             @Override
 592             public boolean isSettable(CategoryAxis n) {
 593                 return n.gapStartAndEnd == null || !n.gapStartAndEnd.isBound();
 594             }
 595 
 596             @Override
 597             public StyleableProperty<Boolean> getStyleableProperty(CategoryAxis n) {
 598                 return (StyleableProperty<Boolean>)(WritableValue<Boolean>)n.gapStartAndEndProperty();
 599             }
 600         };
 601 
 602         private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
 603         static {
 604         final List<CssMetaData<? extends Styleable, ?>> styleables =
 605             new ArrayList<CssMetaData<? extends Styleable, ?>>(Axis.getClassCssMetaData());
 606             styleables.add(START_MARGIN);
 607             styleables.add(END_MARGIN);
 608             styleables.add(GAP_START_AND_END);
 609             STYLEABLES = Collections.unmodifiableList(styleables);
 610         }
 611     }
 612 
 613     /**
 614      * @return The CssMetaData associated with this class, which may include the
 615      * CssMetaData of its superclasses.
 616      * @since JavaFX 8.0
 617      */
 618     public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
 619         return StyleableProperties.STYLEABLES;
 620     }
 621 
 622     /**
 623      * {@inheritDoc}
 624      * @since JavaFX 8.0
 625      */
 626     @Override
 627     public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
 628         return getClassCssMetaData();
 629     }
 630 
 631 }
 632