Spec-Zone .ru
спецификации, руководства, описания, API
|
001/* 002 * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved. 003 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 004 * 005 * This code is free software; you can redistribute it and/or modify it 006 * under the terms of the GNU General Public License version 2 only, as 007 * published by the Free Software Foundation. Oracle designates this 008 * particular file as subject to the "Classpath" exception as provided 009 * by Oracle in the LICENSE file that accompanied this code. 010 * 011 * This code is distributed in the hope that it will be useful, but WITHOUT 012 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 013 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 014 * version 2 for more details (a copy is included in the LICENSE file that 015 * accompanied this code). 016 * 017 * You should have received a copy of the GNU General Public License version 018 * 2 along with this work; if not, write to the Free Software Foundation, 019 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 020 * 021 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 022 * or visit www.oracle.com if you need additional information or have any 023 * questions. 024 */ 025 026package javafx.scene.chart; 027 028import java.util.ArrayList; 029import java.util.HashMap; 030import java.util.List; 031import java.util.Map; 032 033import javafx.animation.FadeTransition; 034import javafx.animation.Interpolator; 035import javafx.animation.KeyFrame; 036import javafx.animation.KeyValue; 037import javafx.animation.Timeline; 038import javafx.beans.property.DoubleProperty; 039import javafx.beans.property.SimpleDoubleProperty; 040import javafx.collections.FXCollections; 041import javafx.collections.ListChangeListener; 042import javafx.collections.ObservableList; 043import javafx.event.ActionEvent; 044import javafx.event.EventHandler; 045import javafx.scene.Group; 046import javafx.scene.Node; 047import javafx.scene.layout.StackPane; 048import javafx.scene.shape.ClosePath; 049import javafx.scene.shape.LineTo; 050import javafx.scene.shape.MoveTo; 051import javafx.scene.shape.Path; 052import javafx.scene.shape.StrokeLineJoin; 053import javafx.util.Duration; 054 055import com.sun.javafx.charts.Legend; 056import com.sun.javafx.charts.Legend.LegendItem; 057import com.sun.javafx.css.converters.BooleanConverter; 058import java.util.Collections; 059import javafx.beans.property.BooleanProperty; 060import javafx.css.CssMetaData; 061import javafx.css.Styleable; 062import javafx.css.StyleableBooleanProperty; 063import javafx.css.StyleableProperty; 064import static javafx.scene.chart.LineChart.getClassCssMetaData; 065 066/** 067 * AreaChart - Plots the area between the line that connects the data points and 068 * the 0 line on the Y axis. 069 */ 070public class AreaChart<X,Y> extends XYChart<X,Y> { 071 072 // -------------- PRIVATE FIELDS ------------------------------------------ 073 074 /** A multiplier for teh Y values that we store for each series, it is used to animate in a new series */ 075 private Map<Series, DoubleProperty> seriesYMultiplierMap = new HashMap<Series, DoubleProperty>(); 076 private Legend legend = new Legend(); 077 078 // -------------- PUBLIC PROPERTIES ---------------------------------------- 079 080 /** When true, CSS styleable symbols are created for any data items that don't have a symbol node specified. */ 081 private BooleanProperty createSymbols = new StyleableBooleanProperty(true) { 082 @Override protected void invalidated() { 083 for (int seriesIndex=0; seriesIndex < getData().size(); seriesIndex ++) { 084 Series<X,Y> series = getData().get(seriesIndex); 085 for (int itemIndex=0; itemIndex < series.getData().size(); itemIndex ++) { 086 Data<X,Y> item = series.getData().get(itemIndex); 087 Node symbol = item.getNode(); 088 if(get() && symbol == null) { // create any symbols 089 symbol = createSymbol(series, getData().indexOf(series), item, itemIndex); 090 if (null != symbol) { 091 getPlotChildren().add(symbol); 092 } 093 } else if (!get() && symbol != null) { // remove symbols 094 getPlotChildren().remove(symbol); 095 symbol = null; 096 item.setNode(null); 097 } 098 } 099 } 100 requestChartLayout(); 101 } 102 103 public Object getBean() { 104 return this; 105 } 106 107 public String getName() { 108 return "createSymbols"; 109 } 110 111 public CssMetaData getCssMetaData() { 112 return StyleableProperties.CREATE_SYMBOLS; 113 } 114 }; 115 116 /** 117 * Indicates whether symbols for data points will be created or not. 118 * 119 * @return true if symbols for data points will be created and false otherwise. 120 */ 121 public final boolean getCreateSymbols() { return createSymbols.getValue(); } 122 public final void setCreateSymbols(boolean value) { createSymbols.setValue(value); } 123 public final BooleanProperty createSymbolsProperty() { return createSymbols; } 124 125 126 // -------------- CONSTRUCTORS ---------------------------------------------- 127 128 /** 129 * Construct a new Area Chart with the given axis 130 * 131 * @param xAxis The x axis to use 132 * @param yAxis The y axis to use 133 */ 134 public AreaChart(Axis<X> xAxis, Axis<Y> yAxis) { 135 this(xAxis,yAxis, FXCollections.<Series<X,Y>>observableArrayList()); 136 } 137 138 /** 139 * Construct a new Area Chart with the given axis and data 140 * 141 * @param xAxis The x axis to use 142 * @param yAxis The y axis to use 143 * @param data The data to use, this is the actual list used so any changes to it will be reflected in the chart 144 */ 145 public AreaChart(Axis<X> xAxis, Axis<Y> yAxis, ObservableList<Series<X,Y>> data) { 146 super(xAxis,yAxis); 147 setLegend(legend); 148 setData(data); 149 } 150 151 // -------------- METHODS ------------------------------------------------------------------------------------------ 152 153 private static double doubleValue(Number number) { return doubleValue(number, 0); } 154 private static double doubleValue(Number number, double nullDefault) { 155 return (number == null) ? nullDefault : number.doubleValue(); 156 } 157 158 /** @inheritDoc */ 159 @Override protected void updateAxisRange() { 160 final Axis<X> xa = getXAxis(); 161 final Axis<Y> ya = getYAxis(); 162 List<X> xData = null; 163 List<Y> yData = null; 164 if(xa.isAutoRanging()) xData = new ArrayList<X>(); 165 if(ya.isAutoRanging()) yData = new ArrayList<Y>(); 166 if(xData != null || yData != null) { 167 for(Series<X,Y> series : getData()) { 168 for(Data<X,Y> data: series.getData()) { 169 if(xData != null) xData.add(data.getXValue()); 170 if(yData != null) yData.add(data.getYValue()); 171 } 172 } 173 if(xData != null && !(xData.size() == 1 && getXAxis().toNumericValue(xData.get(0)) == 0)) { 174 xa.invalidateRange(xData); 175 } 176 if(yData != null && !(yData.size() == 1 && getYAxis().toNumericValue(yData.get(0)) == 0)) { 177 ya.invalidateRange(yData); 178 } 179 } 180 } 181 182 @Override protected void dataItemAdded(Series<X,Y> series, int itemIndex, Data<X,Y> item) { 183 final Node symbol = createSymbol(series, getData().indexOf(series), item, itemIndex); 184 if (shouldAnimate()) { 185 boolean animate = false; 186 if (itemIndex > 0 && itemIndex < (series.getData().size()-1)) { 187 animate = true; 188 Data<X,Y> p1 = series.getData().get(itemIndex - 1); 189 Data<X,Y> p2 = series.getData().get(itemIndex + 1); 190 double x1 = getXAxis().toNumericValue(p1.getXValue()); 191 double y1 = getYAxis().toNumericValue(p1.getYValue()); 192 double x3 = getXAxis().toNumericValue(p2.getXValue()); 193 double y3 = getYAxis().toNumericValue(p2.getYValue()); 194 195 double x2 = getXAxis().toNumericValue(item.getXValue()); 196 double y2 = getYAxis().toNumericValue(item.getYValue()); 197 198// //1. y intercept of the line : y = ((y3-y1)/(x3-x1)) * x2 + (x3y1 - y3x1)/(x3 -x1) 199 double y = ((y3-y1)/(x3-x1)) * x2 + (x3*y1 - y3*x1)/(x3-x1); 200 item.setCurrentY(getYAxis().toRealValue(y)); 201 item.setCurrentX(getXAxis().toRealValue(x2)); 202 //2. we can simply use the midpoint on the line as well.. 203// double x = (x3 + x1)/2; 204// double y = (y3 + y1)/2; 205// item.setCurrentX(x); 206// item.setCurrentY(y); 207 } else if (itemIndex == 0 && series.getData().size() > 1) { 208 animate = true; 209 item.setCurrentX(series.getData().get(1).getXValue()); 210 item.setCurrentY(series.getData().get(1).getYValue()); 211 } else if (itemIndex == (series.getData().size() - 1) && series.getData().size() > 1) { 212 animate = true; 213 int last = series.getData().size() - 2; 214 item.setCurrentX(series.getData().get(last).getXValue()); 215 item.setCurrentY(series.getData().get(last).getYValue()); 216 } else if (symbol != null) { 217 // fade in new symbol 218 FadeTransition ft = new FadeTransition(Duration.millis(500),symbol); 219 ft.setToValue(1); 220 ft.play(); 221 } 222 if (animate) { 223 animate( 224 new KeyFrame(Duration.ZERO, new KeyValue(item.currentYProperty(), 225 item.getCurrentY()), 226 new KeyValue(item.currentXProperty(), 227 item.getCurrentX())), 228 new KeyFrame(Duration.millis(800), new KeyValue(item.currentYProperty(), 229 item.getYValue(), Interpolator.EASE_BOTH), 230 new KeyValue(item.currentXProperty(), 231 item.getXValue(), Interpolator.EASE_BOTH)) 232 ); 233 } 234 235 } else if (symbol != null) { 236 getPlotChildren().add(symbol); 237 } 238 } 239 240 @Override protected void dataItemRemoved(final Data<X,Y> item, final Series<X,Y> series) { 241 final Node symbol = item.getNode(); 242 // remove item from sorted list 243 int itemIndex = series.getItemIndex(item); 244 if (shouldAnimate()) { 245 boolean animate = false; 246 if (itemIndex > 0 && itemIndex < series.getDataSize()-1) { 247 animate = true; 248 int index=0; Data<X,Y> d; 249 for (d = series.begin; d != null && index != itemIndex - 1; d=d.next) index++; 250 Data<X,Y> p1 = d; 251 Data<X,Y> p2 = (d.next).next; 252 double x1 = getXAxis().toNumericValue(p1.getXValue()); 253 double y1 = getYAxis().toNumericValue(p1.getYValue()); 254 double x3 = getXAxis().toNumericValue(p2.getXValue()); 255 double y3 = getYAxis().toNumericValue(p2.getYValue()); 256 257 double x2 = getXAxis().toNumericValue(item.getXValue()); 258 double y2 = getYAxis().toNumericValue(item.getYValue()); 259 260// //1. y intercept of the line : y = ((y3-y1)/(x3-x1)) * x2 + (x3y1 - y3x1)/(x3 -x1) 261 double y = ((y3-y1)/(x3-x1)) * x2 + (x3*y1 - y3*x1)/(x3-x1); 262 item.setCurrentX(getXAxis().toRealValue(x2)); 263 item.setCurrentY(getYAxis().toRealValue(y2)); 264 item.setXValue(getXAxis().toRealValue(x2)); 265 item.setYValue(getYAxis().toRealValue(y)); 266 //2. we can simply use the midpoint on the line as well.. 267// double x = (x3 + x1)/2; 268// double y = (y3 + y1)/2; 269// item.setCurrentX(x); 270// item.setCurrentY(y); 271 } else if (itemIndex == 0 && series.getDataSize() > 1) { 272 animate = true; 273 item.setXValue(series.getData().get(0).getXValue()); 274 item.setYValue(series.getData().get(0).getYValue()); 275 } else if (itemIndex == (series.getDataSize() - 1) && series.getDataSize() > 1) { 276 animate = true; 277 int last = series.getData().size() - 1; 278 item.setXValue(series.getData().get(last).getXValue()); 279 item.setYValue(series.getData().get(last).getYValue()); 280 } else { 281 // fade out symbol 282 symbol.setOpacity(0); 283 FadeTransition ft = new FadeTransition(Duration.millis(500),symbol); 284 ft.setToValue(0); 285 ft.setOnFinished(new EventHandler<ActionEvent>() { 286 @Override public void handle(ActionEvent actionEvent) { 287 getPlotChildren().remove(symbol); 288 removeDataItemFromDisplay(series, item); 289 } 290 }); 291 ft.play(); 292 } 293 if (animate) { 294 animate( new KeyFrame(Duration.ZERO, new KeyValue(item.currentYProperty(), 295 item.getCurrentY()), new KeyValue(item.currentXProperty(), 296 item.getCurrentX())), 297 new KeyFrame(Duration.millis(800), new EventHandler<ActionEvent>() { 298 @Override public void handle(ActionEvent actionEvent) { 299 item.setSeries(null); 300 getPlotChildren().remove(symbol); 301 removeDataItemFromDisplay(series, item); 302 } 303 }, 304 new KeyValue(item.currentYProperty(), 305 item.getYValue(), Interpolator.EASE_BOTH), 306 new KeyValue(item.currentXProperty(), 307 item.getXValue(), Interpolator.EASE_BOTH)) 308 ); 309 } 310 } else { 311 item.setSeries(null); 312 getPlotChildren().remove(symbol); 313 removeDataItemFromDisplay(series, item); 314 } 315 //Note: better animation here, point should move from old position to new position at center point between prev and next symbols 316 } 317 318 /** @inheritDoc */ 319 @Override protected void dataItemChanged(Data<X, Y> item) { 320 } 321 322 @Override protected void seriesChanged(ListChangeListener.Change<? extends Series> c) { 323 // Update style classes for all series lines and symbols 324 // Note: is there a more efficient way of doing this? 325 for (int i = 0; i < getDataSize(); i++) { 326 final Series<X,Y> s = getData().get(i); 327 Path seriesLine = (Path)((Group)s.getNode()).getChildren().get(1); 328 Path fillPath = (Path)((Group)s.getNode()).getChildren().get(0); 329 seriesLine.getStyleClass().setAll("chart-series-area-line", "series" + i, s.defaultColorStyleClass); 330 fillPath.getStyleClass().setAll("chart-series-area-fill", "series" + i, s.defaultColorStyleClass); 331 for (int j=0; j < s.getData().size(); j++) { 332 final Data item = s.getData().get(j); 333 final Node node = item.getNode(); 334 if(node!=null) node.getStyleClass().setAll("chart-area-symbol", "series" + i, "data" + j, s.defaultColorStyleClass); 335 } 336 } 337 } 338 339 @Override protected void seriesAdded(Series<X,Y> series, int seriesIndex) { 340 // create new paths for series 341 Path seriesLine = new Path(); 342 Path fillPath = new Path(); 343 seriesLine.setStrokeLineJoin(StrokeLineJoin.BEVEL); 344 Group areaGroup = new Group(fillPath,seriesLine); 345 series.setNode(areaGroup); 346 // create series Y multiplier 347 DoubleProperty seriesYAnimMultiplier = new SimpleDoubleProperty(this, "seriesYMultiplier"); 348 seriesYMultiplierMap.put(series, seriesYAnimMultiplier); 349 // handle any data already in series 350 if (shouldAnimate()) { 351 seriesYAnimMultiplier.setValue(0d); 352 } else { 353 seriesYAnimMultiplier.setValue(1d); 354 } 355 getPlotChildren().add(areaGroup); 356 List<KeyFrame> keyFrames = new ArrayList<KeyFrame>(); 357 if (shouldAnimate()) { 358 // animate in new series 359 keyFrames.add(new KeyFrame(Duration.ZERO, 360 new KeyValue(areaGroup.opacityProperty(), 0), 361 new KeyValue(seriesYAnimMultiplier, 0) 362 )); 363 keyFrames.add(new KeyFrame(Duration.millis(200), 364 new KeyValue(areaGroup.opacityProperty(), 1) 365 )); 366 keyFrames.add(new KeyFrame(Duration.millis(500), 367 new KeyValue(seriesYAnimMultiplier, 1) 368 )); 369 } 370 for (int j=0; j<series.getData().size(); j++) { 371 Data item = series.getData().get(j); 372 final Node symbol = createSymbol(series, seriesIndex, item, j); 373 if (symbol != null) { 374 if (shouldAnimate()) { 375 symbol.setOpacity(0); 376 getPlotChildren().add(symbol); 377 // fade in new symbol 378 keyFrames.add(new KeyFrame(Duration.ZERO, new KeyValue(symbol.opacityProperty(), 0))); 379 keyFrames.add(new KeyFrame(Duration.millis(200), new KeyValue(symbol.opacityProperty(), 1))); 380 } 381 else { 382 getPlotChildren().add(symbol); 383 } 384 } 385 } 386 if (shouldAnimate()) animate(keyFrames.toArray(new KeyFrame[keyFrames.size()])); 387 } 388 private void updateDefaultColorIndex(final Series<X,Y> series) { 389 int clearIndex = seriesColorMap.get(series); 390 colorBits.clear(clearIndex); 391 Path seriesLine = (Path)((Group)series.getNode()).getChildren().get(1); 392 Path fillPath = (Path)((Group)series.getNode()).getChildren().get(0); 393 if (seriesLine != null) { 394 seriesLine.getStyleClass().remove(DEFAULT_COLOR+clearIndex); 395 } 396 if (fillPath != null) { 397 fillPath.getStyleClass().remove(DEFAULT_COLOR+clearIndex); 398 } 399 colorBits.clear(clearIndex); 400 for (int j=0; j < series.getData().size(); j++) { 401 final Node node = series.getData().get(j).getNode(); 402 if(node!=null) { 403 node.getStyleClass().remove(DEFAULT_COLOR+clearIndex); 404 } 405 } 406 seriesColorMap.remove(series); 407 } 408 @Override protected void seriesRemoved(final Series<X,Y> series) { 409 updateDefaultColorIndex(series); 410 // remove series Y multiplier 411 seriesYMultiplierMap.remove(series); 412 // remove all symbol nodes 413 if (shouldAnimate()) { 414 // create list of all nodes we need to fade out 415 final List<Node> nodes = new ArrayList<Node>(); 416 nodes.add(series.getNode()); 417 if (getCreateSymbols()) { // RT-22124 418 // done need to fade the symbols if createSymbols is false 419 for (Data d: series.getData()) nodes.add(d.getNode()); 420 } 421 // fade out old and symbols 422 KeyValue[] startValues = new KeyValue[nodes.size()]; 423 KeyValue[] endValues = new KeyValue[nodes.size()]; 424 for (int j=0; j < nodes.size(); j++) { 425 startValues[j] = new KeyValue(nodes.get(j).opacityProperty(),1); 426 endValues[j] = new KeyValue(nodes.get(j).opacityProperty(),0); 427 } 428 Timeline tl = new Timeline(); 429 tl.getKeyFrames().addAll( 430 new KeyFrame(Duration.ZERO,startValues), 431 new KeyFrame(Duration.millis(400), new EventHandler<ActionEvent>() { 432 @Override public void handle(ActionEvent actionEvent) { 433 getPlotChildren().removeAll(nodes); 434 removeSeriesFromDisplay(series); 435 } 436 },endValues) 437 ); 438 tl.play(); 439 } else { 440 getPlotChildren().remove(series.getNode()); 441 for (Data d:series.getData()) getPlotChildren().remove(d.getNode()); 442 removeSeriesFromDisplay(series); 443 } 444 } 445 446 /** @inheritDoc */ 447 @Override protected void layoutPlotChildren() { 448 for (int seriesIndex=0; seriesIndex < getDataSize(); seriesIndex++) { 449 Series<X, Y> series = getData().get(seriesIndex); 450 DoubleProperty seriesYAnimMultiplier = seriesYMultiplierMap.get(series); 451 boolean isFirst = true; 452 double lastX = 0; 453 Path seriesLine = (Path)((Group)series.getNode()).getChildren().get(1); 454 Path fillPath = (Path)((Group)series.getNode()).getChildren().get(0); 455 seriesLine.getElements().clear(); 456 fillPath.getElements().clear(); 457 for (Data<X, Y> item = series.begin; item != null; item = item.next) { 458 double x = lastX = getXAxis().getDisplayPosition(item.getCurrentX()); 459 double y = getYAxis().getDisplayPosition( 460 getYAxis().toRealValue(getYAxis().toNumericValue(item.getCurrentY()) * seriesYAnimMultiplier.getValue())); 461 if (isFirst) { 462 isFirst = false; 463 fillPath.getElements().add(new MoveTo(x, getYAxis().getZeroPosition())); 464 seriesLine.getElements().add(new MoveTo(x, y)); 465 } else { 466 seriesLine.getElements().add(new LineTo(x, y)); 467 } 468 fillPath.getElements().add(new LineTo(x, y)); 469 Node symbol = item.getNode(); 470 if (symbol != null) { 471 final double w = symbol.prefWidth(-1); 472 final double h = symbol.prefHeight(-1); 473 symbol.resizeRelocate(x-(w/2), y-(h/2),w,h); 474 } 475 } 476 if (fillPath.getElements().size() >= 1) { 477 fillPath.getElements().add(new LineTo(lastX, getYAxis().getZeroPosition())); 478 } else { 479 fillPath.getElements().add(new MoveTo(lastX, getYAxis().getZeroPosition())); 480 } 481 fillPath.getElements().add(new ClosePath()); 482 } 483 } 484 485 private Node createSymbol(Series series, int seriesIndex, final Data item, int itemIndex) { 486 Node symbol = item.getNode(); 487 // check if symbol has already been created 488 if (symbol == null && getCreateSymbols()) { 489 symbol = new StackPane(); 490 item.setNode(symbol); 491 } 492 // set symbol styles 493 // Note: not sure if we want to add or check, ie be more careful and efficient here 494 if (symbol != null) symbol.getStyleClass().setAll("chart-area-symbol", "series" + seriesIndex, "data" + itemIndex, 495 series.defaultColorStyleClass); 496 return symbol; 497 } 498 499 /** 500 * This is called whenever a series is added or removed and the legend needs to be updated 501 * @since 2.2 502 */ 503 @Override protected void updateLegend() { 504 legend.getItems().clear(); 505 if (getData() != null) { 506 for (int seriesIndex=0; seriesIndex < getData().size(); seriesIndex++) { 507 Series<X,Y> series = getData().get(seriesIndex); 508 LegendItem legenditem = new LegendItem(series.getName()); 509 legenditem.getSymbol().getStyleClass().addAll("chart-area-symbol","series"+seriesIndex, 510 "area-legend-symbol", series.defaultColorStyleClass); 511 legend.getItems().add(legenditem); 512 } 513 } 514 if (legend.getItems().size() > 0) { 515 if (getLegend() == null) { 516 setLegend(legend); 517 } 518 } else { 519 setLegend(null); 520 } 521 } 522 523 // -------------- STYLESHEET HANDLING -------------------------------------- 524 525 private static class StyleableProperties { 526 private static final CssMetaData<AreaChart<?,?>,Boolean> CREATE_SYMBOLS = 527 new CssMetaData<AreaChart<?,?>,Boolean>("-fx-create-symbols", 528 BooleanConverter.getInstance(), Boolean.TRUE) { 529 530 @Override 531 public boolean isSettable(AreaChart node) { 532 return node.createSymbols == null || !node.createSymbols.isBound(); 533} 534 535 @Override 536 public StyleableProperty<Boolean> getStyleableProperty(AreaChart node) { 537 return (StyleableProperty<Boolean>)node.createSymbolsProperty(); 538 } 539 }; 540 541 private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES; 542 static { 543 final List<CssMetaData<? extends Styleable, ?>> styleables = 544 new ArrayList<CssMetaData<? extends Styleable, ?>>(XYChart.getClassCssMetaData()); 545 styleables.add(CREATE_SYMBOLS); 546 STYLEABLES = Collections.unmodifiableList(styleables); 547 } 548 } 549 550 /** 551 * @return The CssMetaData associated with this class, which may include the 552 * CssMetaData of its super classes. 553 */ 554 public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() { 555 return StyleableProperties.STYLEABLES; 556 } 557 558 /** 559 * {@inheritDoc} 560 */ 561 @Override 562 public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() { 563 return getClassCssMetaData(); 564 } 565 566}