001 /* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2007, by Object Refinery Limited and Contributors. 006 * 007 * Project Info: http://www.jfree.org/jfreechart/index.html 008 * 009 * This library is free software; you can redistribute it and/or modify it 010 * under the terms of the GNU Lesser General Public License as published by 011 * the Free Software Foundation; either version 2.1 of the License, or 012 * (at your option) any later version. 013 * 014 * This library is distributed in the hope that it will be useful, but 015 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 016 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 017 * License for more details. 018 * 019 * You should have received a copy of the GNU Lesser General Public 020 * License along with this library; if not, write to the Free Software 021 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, 022 * USA. 023 * 024 * [Java is a trademark or registered trademark of Sun Microsystems, Inc. 025 * in the United States and other countries.] 026 * 027 * ------------- 028 * DialPlot.java 029 * ------------- 030 * (C) Copyright 2006, 2007, by Object Refinery Limited. 031 * 032 * Original Author: David Gilbert (for Object Refinery Limited); 033 * Contributor(s): -; 034 * 035 * Changes 036 * ------- 037 * 03-Nov-2006 : Version 1 (DG); 038 * 08-Mar-2007 : Fix in hashCode() (DG); 039 * 17-Oct-2007 : Fixed listener registration/deregistration bugs (DG); 040 * 24-Oct-2007 : Maintain pointers in their own list, so they can be 041 * drawn after other layers (DG); 042 * 043 */ 044 045 package org.jfree.chart.plot.dial; 046 047 import java.awt.Graphics2D; 048 import java.awt.Shape; 049 import java.awt.geom.Point2D; 050 import java.awt.geom.Rectangle2D; 051 import java.io.IOException; 052 import java.io.ObjectInputStream; 053 import java.io.ObjectOutputStream; 054 import java.util.Iterator; 055 import java.util.List; 056 057 import org.jfree.chart.JFreeChart; 058 import org.jfree.chart.event.PlotChangeEvent; 059 import org.jfree.chart.plot.Plot; 060 import org.jfree.chart.plot.PlotRenderingInfo; 061 import org.jfree.chart.plot.PlotState; 062 import org.jfree.data.general.DatasetChangeEvent; 063 import org.jfree.data.general.ValueDataset; 064 import org.jfree.util.ObjectList; 065 import org.jfree.util.ObjectUtilities; 066 067 /** 068 * A dial plot. 069 * 070 * @since 1.0.7 071 */ 072 public class DialPlot extends Plot implements DialLayerChangeListener { 073 074 /** 075 * The background layer (optional). 076 */ 077 private DialLayer background; 078 079 /** 080 * The needle cap (optional). 081 */ 082 private DialLayer cap; 083 084 /** 085 * The dial frame. 086 */ 087 private DialFrame dialFrame; 088 089 /** 090 * The dataset(s) for the dial plot. 091 */ 092 private ObjectList datasets; 093 094 /** 095 * The scale(s) for the dial plot. 096 */ 097 private ObjectList scales; 098 099 /** Storage for keys that map datasets to scales. */ 100 private ObjectList datasetToScaleMap; 101 102 /** 103 * The drawing layers for the dial plot. 104 */ 105 private List layers; 106 107 /** 108 * The pointer(s) for the dial. 109 */ 110 private List pointers; 111 112 /** 113 * The x-coordinate for the view window. 114 */ 115 private double viewX; 116 117 /** 118 * The y-coordinate for the view window. 119 */ 120 private double viewY; 121 122 /** 123 * The width of the view window, expressed as a percentage. 124 */ 125 private double viewW; 126 127 /** 128 * The height of the view window, expressed as a percentage. 129 */ 130 private double viewH; 131 132 /** 133 * Creates a new instance of <code>DialPlot</code>. 134 */ 135 public DialPlot() { 136 this(null); 137 } 138 139 /** 140 * Creates a new instance of <code>DialPlot</code>. 141 * 142 * @param dataset the dataset (<code>null</code> permitted). 143 */ 144 public DialPlot(ValueDataset dataset) { 145 this.background = null; 146 this.cap = null; 147 this.dialFrame = new ArcDialFrame(); 148 this.datasets = new ObjectList(); 149 if (dataset != null) { 150 this.setDataset(dataset); 151 } 152 this.scales = new ObjectList(); 153 this.datasetToScaleMap = new ObjectList(); 154 this.layers = new java.util.ArrayList(); 155 this.pointers = new java.util.ArrayList(); 156 this.viewX = 0.0; 157 this.viewY = 0.0; 158 this.viewW = 1.0; 159 this.viewH = 1.0; 160 } 161 162 /** 163 * Returns the background. 164 * 165 * @return The background (possibly <code>null</code>). 166 * 167 * @see #setBackground(DialLayer) 168 */ 169 public DialLayer getBackground() { 170 return this.background; 171 } 172 173 /** 174 * Sets the background layer and sends a {@link PlotChangeEvent} to all 175 * registered listeners. 176 * 177 * @param background the background layer (<code>null</code> permitted). 178 * 179 * @see #getBackground() 180 */ 181 public void setBackground(DialLayer background) { 182 if (this.background != null) { 183 this.background.removeChangeListener(this); 184 } 185 this.background = background; 186 if (background != null) { 187 background.addChangeListener(this); 188 } 189 notifyListeners(new PlotChangeEvent(this)); 190 } 191 192 /** 193 * Returns the cap. 194 * 195 * @return The cap (possibly <code>null</code>). 196 * 197 * @see #setCap(DialLayer) 198 */ 199 public DialLayer getCap() { 200 return this.cap; 201 } 202 203 /** 204 * Sets the cap and sends a {@link PlotChangeEvent} to all registered 205 * listeners. 206 * 207 * @param cap the cap (<code>null</code> permitted). 208 * 209 * @see #getCap() 210 */ 211 public void setCap(DialLayer cap) { 212 if (this.cap != null) { 213 this.cap.removeChangeListener(this); 214 } 215 this.cap = cap; 216 if (cap != null) { 217 cap.addChangeListener(this); 218 } 219 notifyListeners(new PlotChangeEvent(this)); 220 } 221 222 /** 223 * Returns the dial's frame. 224 * 225 * @return The dial's frame (never <code>null</code>). 226 * 227 * @see #setDialFrame(DialFrame) 228 */ 229 public DialFrame getDialFrame() { 230 return this.dialFrame; 231 } 232 233 /** 234 * Sets the dial's frame and sends a {@link PlotChangeEvent} to all 235 * registered listeners. 236 * 237 * @param frame the frame (<code>null</code> not permitted). 238 * 239 * @see #getDialFrame() 240 */ 241 public void setDialFrame(DialFrame frame) { 242 if (frame == null) { 243 throw new IllegalArgumentException("Null 'frame' argument."); 244 } 245 this.dialFrame.removeChangeListener(this); 246 this.dialFrame = frame; 247 frame.addChangeListener(this); 248 notifyListeners(new PlotChangeEvent(this)); 249 } 250 251 /** 252 * Returns the x-coordinate of the viewing rectangle. This is specified 253 * in the range 0.0 to 1.0, relative to the dial's framing rectangle. 254 * 255 * @return The x-coordinate of the viewing rectangle. 256 * 257 * @see #setView(double, double, double, double) 258 */ 259 public double getViewX() { 260 return this.viewX; 261 } 262 263 /** 264 * Returns the y-coordinate of the viewing rectangle. This is specified 265 * in the range 0.0 to 1.0, relative to the dial's framing rectangle. 266 * 267 * @return The y-coordinate of the viewing rectangle. 268 * 269 * @see #setView(double, double, double, double) 270 */ 271 public double getViewY() { 272 return this.viewY; 273 } 274 275 /** 276 * Returns the width of the viewing rectangle. This is specified 277 * in the range 0.0 to 1.0, relative to the dial's framing rectangle. 278 * 279 * @return The width of the viewing rectangle. 280 * 281 * @see #setView(double, double, double, double) 282 */ 283 public double getViewWidth() { 284 return this.viewW; 285 } 286 287 /** 288 * Returns the height of the viewing rectangle. This is specified 289 * in the range 0.0 to 1.0, relative to the dial's framing rectangle. 290 * 291 * @return The height of the viewing rectangle. 292 * 293 * @see #setView(double, double, double, double) 294 */ 295 public double getViewHeight() { 296 return this.viewH; 297 } 298 299 /** 300 * Sets the viewing rectangle, relative to the dial's framing rectangle, 301 * and sends a {@link PlotChangeEvent} to all registered listeners. 302 * 303 * @param x the x-coordinate (in the range 0.0 to 1.0). 304 * @param y the y-coordinate (in the range 0.0 to 1.0). 305 * @param w the width (in the range 0.0 to 1.0). 306 * @param h the height (in the range 0.0 to 1.0). 307 * 308 * @see #getViewX() 309 * @see #getViewY() 310 * @see #getViewWidth() 311 * @see #getViewHeight() 312 */ 313 public void setView(double x, double y, double w, double h) { 314 this.viewX = x; 315 this.viewY = y; 316 this.viewW = w; 317 this.viewH = h; 318 notifyListeners(new PlotChangeEvent(this)); 319 } 320 321 /** 322 * Adds a layer to the plot and sends a {@link PlotChangeEvent} to all 323 * registered listeners. 324 * 325 * @param layer the layer (<code>null</code> not permitted). 326 */ 327 public void addLayer(DialLayer layer) { 328 if (layer == null) { 329 throw new IllegalArgumentException("Null 'layer' argument."); 330 } 331 this.layers.add(layer); 332 layer.addChangeListener(this); 333 notifyListeners(new PlotChangeEvent(this)); 334 } 335 336 /** 337 * Returns the index for the specified layer. 338 * 339 * @param layer the layer (<code>null</code> not permitted). 340 * 341 * @return The layer index. 342 */ 343 public int getLayerIndex(DialLayer layer) { 344 if (layer == null) { 345 throw new IllegalArgumentException("Null 'layer' argument."); 346 } 347 return this.layers.indexOf(layer); 348 } 349 350 /** 351 * Removes the layer at the specified index and sends a 352 * {@link PlotChangeEvent} to all registered listeners. 353 * 354 * @param index the index. 355 */ 356 public void removeLayer(int index) { 357 DialLayer layer = (DialLayer) this.layers.get(index); 358 if (layer != null) { 359 layer.removeChangeListener(this); 360 } 361 this.layers.remove(index); 362 notifyListeners(new PlotChangeEvent(this)); 363 } 364 365 /** 366 * Removes the specified layer and sends a {@link PlotChangeEvent} to all 367 * registered listeners. 368 * 369 * @param layer the layer (<code>null</code> not permitted). 370 */ 371 public void removeLayer(DialLayer layer) { 372 // defer argument checking 373 removeLayer(getLayerIndex(layer)); 374 } 375 376 /** 377 * Adds a pointer to the plot and sends a {@link PlotChangeEvent} to all 378 * registered listeners. 379 * 380 * @param pointer the pointer (<code>null</code> not permitted). 381 */ 382 public void addPointer(DialPointer pointer) { 383 if (pointer == null) { 384 throw new IllegalArgumentException("Null 'pointer' argument."); 385 } 386 this.pointers.add(pointer); 387 pointer.addChangeListener(this); 388 notifyListeners(new PlotChangeEvent(this)); 389 } 390 391 /** 392 * Returns the index for the specified pointer. 393 * 394 * @param pointer the pointer (<code>null</code> not permitted). 395 * 396 * @return The pointer index. 397 */ 398 public int getPointerIndex(DialPointer pointer) { 399 if (pointer == null) { 400 throw new IllegalArgumentException("Null 'pointer' argument."); 401 } 402 return this.pointers.indexOf(pointer); 403 } 404 405 /** 406 * Removes the pointer at the specified index and sends a 407 * {@link PlotChangeEvent} to all registered listeners. 408 * 409 * @param index the index. 410 */ 411 public void removePointer(int index) { 412 DialPointer pointer = (DialPointer) this.pointers.get(index); 413 if (pointer != null) { 414 pointer.removeChangeListener(this); 415 } 416 this.pointers.remove(index); 417 notifyListeners(new PlotChangeEvent(this)); 418 } 419 420 /** 421 * Removes the specified pointer and sends a {@link PlotChangeEvent} to all 422 * registered listeners. 423 * 424 * @param pointer the pointer (<code>null</code> not permitted). 425 */ 426 public void removePointer(DialPointer pointer) { 427 // defer argument checking 428 removeLayer(getPointerIndex(pointer)); 429 } 430 431 /** 432 * Returns the dial pointer that is associated with the specified 433 * dataset, or <code>null</code>. 434 * 435 * @param datasetIndex the dataset index. 436 * 437 * @return The pointer. 438 */ 439 public DialPointer getPointerForDataset(int datasetIndex) { 440 DialPointer result = null; 441 Iterator iterator = this.pointers.iterator(); 442 while (iterator.hasNext()) { 443 DialPointer p = (DialPointer) iterator.next(); 444 if (p.getDatasetIndex() == datasetIndex) { 445 return p; 446 } 447 } 448 return result; 449 } 450 451 /** 452 * Returns the primary dataset for the plot. 453 * 454 * @return The primary dataset (possibly <code>null</code>). 455 */ 456 public ValueDataset getDataset() { 457 return getDataset(0); 458 } 459 460 /** 461 * Returns the dataset at the given index. 462 * 463 * @param index the dataset index. 464 * 465 * @return The dataset (possibly <code>null</code>). 466 */ 467 public ValueDataset getDataset(int index) { 468 ValueDataset result = null; 469 if (this.datasets.size() > index) { 470 result = (ValueDataset) this.datasets.get(index); 471 } 472 return result; 473 } 474 475 /** 476 * Sets the dataset for the plot, replacing the existing dataset, if there 477 * is one, and sends a {@link PlotChangeEvent} to all registered 478 * listeners. 479 * 480 * @param dataset the dataset (<code>null</code> permitted). 481 */ 482 public void setDataset(ValueDataset dataset) { 483 setDataset(0, dataset); 484 } 485 486 /** 487 * Sets a dataset for the plot. 488 * 489 * @param index the dataset index. 490 * @param dataset the dataset (<code>null</code> permitted). 491 */ 492 public void setDataset(int index, ValueDataset dataset) { 493 494 ValueDataset existing = (ValueDataset) this.datasets.get(index); 495 if (existing != null) { 496 existing.removeChangeListener(this); 497 } 498 this.datasets.set(index, dataset); 499 if (dataset != null) { 500 dataset.addChangeListener(this); 501 } 502 503 // send a dataset change event to self... 504 DatasetChangeEvent event = new DatasetChangeEvent(this, dataset); 505 datasetChanged(event); 506 507 } 508 509 /** 510 * Returns the number of datasets. 511 * 512 * @return The number of datasets. 513 */ 514 public int getDatasetCount() { 515 return this.datasets.size(); 516 } 517 518 /** 519 * Draws the plot. This method is usually called by the {@link JFreeChart} 520 * instance that manages the plot. 521 * 522 * @param g2 the graphics target. 523 * @param area the area in which the plot should be drawn. 524 * @param anchor the anchor point (typically the last point that the 525 * mouse clicked on, <code>null</code> is permitted). 526 * @param parentState the state for the parent plot (if any). 527 * @param info used to collect plot rendering info (<code>null</code> 528 * permitted). 529 */ 530 public void draw(Graphics2D g2, Rectangle2D area, Point2D anchor, 531 PlotState parentState, PlotRenderingInfo info) { 532 533 // first, expand the viewing area into a drawing frame 534 Rectangle2D frame = viewToFrame(area); 535 536 // draw the background if there is one... 537 if (this.background != null && this.background.isVisible()) { 538 if (this.background.isClippedToWindow()) { 539 Shape savedClip = g2.getClip(); 540 g2.setClip(this.dialFrame.getWindow(frame)); 541 this.background.draw(g2, this, frame, area); 542 g2.setClip(savedClip); 543 } 544 else { 545 this.background.draw(g2, this, frame, area); 546 } 547 } 548 549 Iterator iterator = this.layers.iterator(); 550 while (iterator.hasNext()) { 551 DialLayer current = (DialLayer) iterator.next(); 552 if (current.isVisible()) { 553 if (current.isClippedToWindow()) { 554 Shape savedClip = g2.getClip(); 555 g2.setClip(this.dialFrame.getWindow(frame)); 556 current.draw(g2, this, frame, area); 557 g2.setClip(savedClip); 558 } 559 else { 560 current.draw(g2, this, frame, area); 561 } 562 } 563 } 564 565 // draw the pointers 566 iterator = this.pointers.iterator(); 567 while (iterator.hasNext()) { 568 DialPointer current = (DialPointer) iterator.next(); 569 if (current.isVisible()) { 570 if (current.isClippedToWindow()) { 571 Shape savedClip = g2.getClip(); 572 g2.setClip(this.dialFrame.getWindow(frame)); 573 current.draw(g2, this, frame, area); 574 g2.setClip(savedClip); 575 } 576 else { 577 current.draw(g2, this, frame, area); 578 } 579 } 580 } 581 582 // draw the cap if there is one... 583 if (this.cap != null && this.cap.isVisible()) { 584 if (this.cap.isClippedToWindow()) { 585 Shape savedClip = g2.getClip(); 586 g2.setClip(this.dialFrame.getWindow(frame)); 587 this.cap.draw(g2, this, frame, area); 588 g2.setClip(savedClip); 589 } 590 else { 591 this.cap.draw(g2, this, frame, area); 592 } 593 } 594 595 if (this.dialFrame.isVisible()) { 596 this.dialFrame.draw(g2, this, frame, area); 597 } 598 599 } 600 601 /** 602 * Returns the frame surrounding the specified view rectangle. 603 * 604 * @param view the view rectangle (<code>null</code> not permitted). 605 * 606 * @return The frame rectangle. 607 */ 608 private Rectangle2D viewToFrame(Rectangle2D view) { 609 double width = view.getWidth() / this.viewW; 610 double height = view.getHeight() / this.viewH; 611 double x = view.getX() - (width * this.viewX); 612 double y = view.getY() - (height * this.viewY); 613 return new Rectangle2D.Double(x, y, width, height); 614 } 615 616 /** 617 * Returns the value from the specified dataset. 618 * 619 * @param datasetIndex the dataset index. 620 * 621 * @return The data value. 622 */ 623 public double getValue(int datasetIndex) { 624 double result = Double.NaN; 625 ValueDataset dataset = getDataset(datasetIndex); 626 if (dataset != null) { 627 Number n = dataset.getValue(); 628 if (n != null) { 629 result = n.doubleValue(); 630 } 631 } 632 return result; 633 } 634 635 /** 636 * Adds a dial scale to the plot and sends a {@link PlotChangeEvent} to 637 * all registered listeners. 638 * 639 * @param index the scale index. 640 * @param scale the scale (<code>null</code> not permitted). 641 */ 642 public void addScale(int index, DialScale scale) { 643 if (scale == null) { 644 throw new IllegalArgumentException("Null 'scale' argument."); 645 } 646 DialScale existing = (DialScale) this.scales.get(index); 647 if (existing != null) { 648 removeLayer(existing); 649 } 650 this.layers.add(scale); 651 this.scales.set(index, scale); 652 scale.addChangeListener(this); 653 notifyListeners(new PlotChangeEvent(this)); 654 } 655 656 /** 657 * Returns the scale at the given index. 658 * 659 * @param index the scale index. 660 * 661 * @return The scale (possibly <code>null</code>). 662 */ 663 public DialScale getScale(int index) { 664 DialScale result = null; 665 if (this.scales.size() > index) { 666 result = (DialScale) this.scales.get(index); 667 } 668 return result; 669 } 670 671 /** 672 * Maps a dataset to a particular scale. 673 * 674 * @param index the dataset index (zero-based). 675 * @param scaleIndex the scale index (zero-based). 676 */ 677 public void mapDatasetToScale(int index, int scaleIndex) { 678 this.datasetToScaleMap.set(index, new Integer(scaleIndex)); 679 notifyListeners(new PlotChangeEvent(this)); 680 } 681 682 /** 683 * Returns the dial scale for a specific dataset. 684 * 685 * @param datasetIndex the dataset index. 686 * 687 * @return The dial scale. 688 */ 689 public DialScale getScaleForDataset(int datasetIndex) { 690 DialScale result = (DialScale) this.scales.get(0); 691 Integer scaleIndex = (Integer) this.datasetToScaleMap.get(datasetIndex); 692 if (scaleIndex != null) { 693 result = getScale(scaleIndex.intValue()); 694 } 695 return result; 696 } 697 698 /** 699 * A utility method that computes a rectangle using relative radius values. 700 * 701 * @param rect the reference rectangle (<code>null</code> not permitted). 702 * @param radiusW the width radius (must be > 0.0) 703 * @param radiusH the height radius. 704 * 705 * @return A new rectangle. 706 */ 707 public static Rectangle2D rectangleByRadius(Rectangle2D rect, 708 double radiusW, double radiusH) { 709 if (rect == null) { 710 throw new IllegalArgumentException("Null 'rect' argument."); 711 } 712 double x = rect.getCenterX(); 713 double y = rect.getCenterY(); 714 double w = rect.getWidth() * radiusW; 715 double h = rect.getHeight() * radiusH; 716 return new Rectangle2D.Double(x - w / 2.0, y - h / 2.0, w, h); 717 } 718 719 /** 720 * Receives notification when a layer has changed, and responds by 721 * forwarding a {@link PlotChangeEvent} to all registered listeners. 722 * 723 * @param event the event. 724 */ 725 public void dialLayerChanged(DialLayerChangeEvent event) { 726 this.notifyListeners(new PlotChangeEvent(this)); 727 } 728 729 /** 730 * Tests this <code>DialPlot</code> instance for equality with an 731 * arbitrary object. The plot's dataset(s) is (are) not included in 732 * the test. 733 * 734 * @param obj the object (<code>null</code> permitted). 735 * 736 * @return A boolean. 737 */ 738 public boolean equals(Object obj) { 739 if (obj == this) { 740 return true; 741 } 742 if (!(obj instanceof DialPlot)) { 743 return false; 744 } 745 DialPlot that = (DialPlot) obj; 746 if (!ObjectUtilities.equal(this.background, that.background)) { 747 return false; 748 } 749 if (!ObjectUtilities.equal(this.cap, that.cap)) { 750 return false; 751 } 752 if (!this.dialFrame.equals(that.dialFrame)) { 753 return false; 754 } 755 if (this.viewX != that.viewX) { 756 return false; 757 } 758 if (this.viewY != that.viewY) { 759 return false; 760 } 761 if (this.viewW != that.viewW) { 762 return false; 763 } 764 if (this.viewH != that.viewH) { 765 return false; 766 } 767 if (!this.layers.equals(that.layers)) { 768 return false; 769 } 770 if (!this.pointers.equals(that.pointers)) { 771 return false; 772 } 773 return super.equals(obj); 774 } 775 776 /** 777 * Returns a hash code for this instance. 778 * 779 * @return The hash code. 780 */ 781 public int hashCode() { 782 int result = 193; 783 result = 37 * result + ObjectUtilities.hashCode(this.background); 784 result = 37 * result + ObjectUtilities.hashCode(this.cap); 785 result = 37 * result + this.dialFrame.hashCode(); 786 long temp = Double.doubleToLongBits(this.viewX); 787 result = 37 * result + (int) (temp ^ (temp >>> 32)); 788 temp = Double.doubleToLongBits(this.viewY); 789 result = 37 * result + (int) (temp ^ (temp >>> 32)); 790 temp = Double.doubleToLongBits(this.viewW); 791 result = 37 * result + (int) (temp ^ (temp >>> 32)); 792 temp = Double.doubleToLongBits(this.viewH); 793 result = 37 * result + (int) (temp ^ (temp >>> 32)); 794 return result; 795 } 796 797 /** 798 * Returns the plot type. 799 * 800 * @return <code>"DialPlot"</code> 801 */ 802 public String getPlotType() { 803 return "DialPlot"; 804 } 805 806 /** 807 * Provides serialization support. 808 * 809 * @param stream the output stream. 810 * 811 * @throws IOException if there is an I/O error. 812 */ 813 private void writeObject(ObjectOutputStream stream) throws IOException { 814 stream.defaultWriteObject(); 815 } 816 817 /** 818 * Provides serialization support. 819 * 820 * @param stream the input stream. 821 * 822 * @throws IOException if there is an I/O error. 823 * @throws ClassNotFoundException if there is a classpath problem. 824 */ 825 private void readObject(ObjectInputStream stream) 826 throws IOException, ClassNotFoundException { 827 stream.defaultReadObject(); 828 } 829 830 831 }