001/* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-present, by David Gilbert 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 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 025 * Other names may be trademarks of their respective owners.] 026 * 027 * --------------- 028 * ChartUtils.java 029 * --------------- 030 * (C) Copyright 2001-present, by David Gilbert and Contributors. 031 * 032 * Original Author: David Gilbert; 033 * Contributor(s): Wolfgang Irler; 034 * Richard Atkinson; 035 * Xavier Poinsard; 036 * 037 */ 038 039package org.jfree.chart; 040 041import java.awt.Graphics2D; 042import java.awt.geom.AffineTransform; 043import java.awt.geom.Rectangle2D; 044import java.awt.image.BufferedImage; 045import java.io.BufferedOutputStream; 046import java.io.File; 047import java.io.FileOutputStream; 048import java.io.IOException; 049import java.io.OutputStream; 050import java.io.PrintWriter; 051 052import org.jfree.chart.encoders.EncoderUtil; 053import org.jfree.chart.encoders.ImageFormat; 054import org.jfree.chart.imagemap.ImageMapUtils; 055import org.jfree.chart.imagemap.OverLIBToolTipTagFragmentGenerator; 056import org.jfree.chart.imagemap.StandardToolTipTagFragmentGenerator; 057import org.jfree.chart.imagemap.StandardURLTagFragmentGenerator; 058import org.jfree.chart.imagemap.ToolTipTagFragmentGenerator; 059import org.jfree.chart.imagemap.URLTagFragmentGenerator; 060import org.jfree.chart.util.Args; 061 062/** 063 * A collection of utility methods for JFreeChart. Includes methods for 064 * converting charts to image formats (PNG and JPEG) plus creating simple HTML 065 * image maps. 066 * 067 * @see ImageMapUtils 068 */ 069public abstract class ChartUtils { 070 071 /** 072 * Applies the current theme to the specified chart. This method is 073 * provided for convenience, the theme itself is stored in the 074 * {@link ChartFactory} class. 075 * 076 * @param chart the chart ({@code null} not permitted). 077 */ 078 public static void applyCurrentTheme(JFreeChart chart) { 079 ChartFactory.getChartTheme().apply(chart); 080 } 081 082 /** 083 * Writes a chart to an output stream in PNG format. 084 * 085 * @param out the output stream ({@code null} not permitted). 086 * @param chart the chart ({@code null} not permitted). 087 * @param width the image width. 088 * @param height the image height. 089 * 090 * @throws IOException if there are any I/O errors. 091 */ 092 public static void writeChartAsPNG(OutputStream out, JFreeChart chart, 093 int width, int height) throws IOException { 094 095 // defer argument checking... 096 writeChartAsPNG(out, chart, width, height, null); 097 098 } 099 100 /** 101 * Writes a chart to an output stream in PNG format. 102 * 103 * @param out the output stream ({@code null} not permitted). 104 * @param chart the chart ({@code null} not permitted). 105 * @param width the image width. 106 * @param height the image height. 107 * @param encodeAlpha encode alpha? 108 * @param compression the compression level (0-9). 109 * 110 * @throws IOException if there are any I/O errors. 111 */ 112 public static void writeChartAsPNG(OutputStream out, JFreeChart chart, 113 int width, int height, boolean encodeAlpha, int compression) 114 throws IOException { 115 116 // defer argument checking... 117 ChartUtils.writeChartAsPNG(out, chart, width, height, null, 118 encodeAlpha, compression); 119 120 } 121 122 /** 123 * Writes a chart to an output stream in PNG format. This method allows 124 * you to pass in a {@link ChartRenderingInfo} object, to collect 125 * information about the chart dimensions/entities. You will need this 126 * info if you want to create an HTML image map. 127 * 128 * @param out the output stream ({@code null} not permitted). 129 * @param chart the chart ({@code null} not permitted). 130 * @param width the image width. 131 * @param height the image height. 132 * @param info the chart rendering info ({@code null} permitted). 133 * 134 * @throws IOException if there are any I/O errors. 135 */ 136 public static void writeChartAsPNG(OutputStream out, JFreeChart chart, 137 int width, int height, ChartRenderingInfo info) 138 throws IOException { 139 140 Args.nullNotPermitted(chart, "chart"); 141 BufferedImage bufferedImage 142 = chart.createBufferedImage(width, height, info); 143 EncoderUtil.writeBufferedImage(bufferedImage, ImageFormat.PNG, out); 144 } 145 146 /** 147 * Writes a chart to an output stream in PNG format. This method allows 148 * you to pass in a {@link ChartRenderingInfo} object, to collect 149 * information about the chart dimensions/entities. You will need this 150 * info if you want to create an HTML image map. 151 * 152 * @param out the output stream ({@code null} not permitted). 153 * @param chart the chart ({@code null} not permitted). 154 * @param width the image width. 155 * @param height the image height. 156 * @param info carries back chart rendering info ({@code null} 157 * permitted). 158 * @param encodeAlpha encode alpha? 159 * @param compression the PNG compression level (0-9). 160 * 161 * @throws IOException if there are any I/O errors. 162 */ 163 public static void writeChartAsPNG(OutputStream out, JFreeChart chart, 164 int width, int height, ChartRenderingInfo info, 165 boolean encodeAlpha, int compression) throws IOException { 166 167 Args.nullNotPermitted(out, "out"); 168 Args.nullNotPermitted(chart, "chart"); 169 BufferedImage chartImage = chart.createBufferedImage(width, height, 170 BufferedImage.TYPE_INT_ARGB, info); 171 ChartUtils.writeBufferedImageAsPNG(out, chartImage, encodeAlpha, 172 compression); 173 174 } 175 176 /** 177 * Writes a scaled version of a chart to an output stream in PNG format. 178 * 179 * @param out the output stream ({@code null} not permitted). 180 * @param chart the chart ({@code null} not permitted). 181 * @param width the unscaled chart width. 182 * @param height the unscaled chart height. 183 * @param widthScaleFactor the horizontal scale factor. 184 * @param heightScaleFactor the vertical scale factor. 185 * 186 * @throws IOException if there are any I/O problems. 187 */ 188 public static void writeScaledChartAsPNG(OutputStream out, 189 JFreeChart chart, int width, int height, int widthScaleFactor, 190 int heightScaleFactor) throws IOException { 191 192 Args.nullNotPermitted(out, "out"); 193 Args.nullNotPermitted(chart, "chart"); 194 195 double desiredWidth = width * widthScaleFactor; 196 double desiredHeight = height * heightScaleFactor; 197 double defaultWidth = width; 198 double defaultHeight = height; 199 boolean scale = false; 200 201 // get desired width and height from somewhere then... 202 if ((widthScaleFactor != 1) || (heightScaleFactor != 1)) { 203 scale = true; 204 } 205 206 double scaleX = desiredWidth / defaultWidth; 207 double scaleY = desiredHeight / defaultHeight; 208 209 BufferedImage image = new BufferedImage((int) desiredWidth, 210 (int) desiredHeight, BufferedImage.TYPE_INT_ARGB); 211 Graphics2D g2 = image.createGraphics(); 212 213 if (scale) { 214 AffineTransform saved = g2.getTransform(); 215 g2.transform(AffineTransform.getScaleInstance(scaleX, scaleY)); 216 chart.draw(g2, new Rectangle2D.Double(0, 0, defaultWidth, 217 defaultHeight), null, null); 218 g2.setTransform(saved); 219 g2.dispose(); 220 } 221 else { 222 chart.draw(g2, new Rectangle2D.Double(0, 0, defaultWidth, 223 defaultHeight), null, null); 224 } 225 out.write(encodeAsPNG(image)); 226 227 } 228 229 /** 230 * Saves a chart to the specified file in PNG format. 231 * 232 * @param file the file name ({@code null} not permitted). 233 * @param chart the chart ({@code null} not permitted). 234 * @param width the image width. 235 * @param height the image height. 236 * 237 * @throws IOException if there are any I/O errors. 238 */ 239 public static void saveChartAsPNG(File file, JFreeChart chart, 240 int width, int height) throws IOException { 241 242 // defer argument checking... 243 saveChartAsPNG(file, chart, width, height, null); 244 245 } 246 247 /** 248 * Saves a chart to a file in PNG format. This method allows you to pass 249 * in a {@link ChartRenderingInfo} object, to collect information about the 250 * chart dimensions/entities. You will need this info if you want to 251 * create an HTML image map. 252 * 253 * @param file the file ({@code null} not permitted). 254 * @param chart the chart ({@code null} not permitted). 255 * @param width the image width. 256 * @param height the image height. 257 * @param info the chart rendering info ({@code null} permitted). 258 * 259 * @throws IOException if there are any I/O errors. 260 */ 261 public static void saveChartAsPNG(File file, JFreeChart chart, 262 int width, int height, ChartRenderingInfo info) 263 throws IOException { 264 265 Args.nullNotPermitted(file, "file"); 266 OutputStream out = new BufferedOutputStream(new FileOutputStream(file)); 267 try { 268 ChartUtils.writeChartAsPNG(out, chart, width, height, info); 269 } 270 finally { 271 out.close(); 272 } 273 } 274 275 /** 276 * Saves a chart to a file in PNG format. This method allows you to pass 277 * in a {@link ChartRenderingInfo} object, to collect information about the 278 * chart dimensions/entities. You will need this info if you want to 279 * create an HTML image map. 280 * 281 * @param file the file ({@code null} not permitted). 282 * @param chart the chart ({@code null} not permitted). 283 * @param width the image width. 284 * @param height the image height. 285 * @param info the chart rendering info ({@code null} permitted). 286 * @param encodeAlpha encode alpha? 287 * @param compression the PNG compression level (0-9). 288 * 289 * @throws IOException if there are any I/O errors. 290 */ 291 public static void saveChartAsPNG(File file, JFreeChart chart, 292 int width, int height, ChartRenderingInfo info, boolean encodeAlpha, 293 int compression) throws IOException { 294 295 Args.nullNotPermitted(file, "file"); 296 Args.nullNotPermitted(chart, "chart"); 297 OutputStream out = new BufferedOutputStream(new FileOutputStream(file)); 298 try { 299 writeChartAsPNG(out, chart, width, height, info, encodeAlpha, 300 compression); 301 } 302 finally { 303 out.close(); 304 } 305 306 } 307 308 /** 309 * Writes a chart to an output stream in JPEG format. Please note that 310 * JPEG is a poor format for chart images, use PNG if possible. 311 * 312 * @param out the output stream ({@code null} not permitted). 313 * @param chart the chart ({@code null} not permitted). 314 * @param width the image width. 315 * @param height the image height. 316 * 317 * @throws IOException if there are any I/O errors. 318 */ 319 public static void writeChartAsJPEG(OutputStream out, 320 JFreeChart chart, int width, int height) throws IOException { 321 322 // defer argument checking... 323 writeChartAsJPEG(out, chart, width, height, null); 324 325 } 326 327 /** 328 * Writes a chart to an output stream in JPEG format. Please note that 329 * JPEG is a poor format for chart images, use PNG if possible. 330 * 331 * @param out the output stream ({@code null} not permitted). 332 * @param quality the quality setting. 333 * @param chart the chart ({@code null} not permitted). 334 * @param width the image width. 335 * @param height the image height. 336 * 337 * @throws IOException if there are any I/O errors. 338 */ 339 public static void writeChartAsJPEG(OutputStream out, float quality, 340 JFreeChart chart, int width, int height) throws IOException { 341 342 // defer argument checking... 343 ChartUtils.writeChartAsJPEG(out, quality, chart, width, height, 344 null); 345 346 } 347 348 /** 349 * Writes a chart to an output stream in JPEG format. This method allows 350 * you to pass in a {@link ChartRenderingInfo} object, to collect 351 * information about the chart dimensions/entities. You will need this 352 * info if you want to create an HTML image map. 353 * 354 * @param out the output stream ({@code null} not permitted). 355 * @param chart the chart ({@code null} not permitted). 356 * @param width the image width. 357 * @param height the image height. 358 * @param info the chart rendering info ({@code null} permitted). 359 * 360 * @throws IOException if there are any I/O errors. 361 */ 362 public static void writeChartAsJPEG(OutputStream out, JFreeChart chart, 363 int width, int height, ChartRenderingInfo info) 364 throws IOException { 365 366 Args.nullNotPermitted(out, "out"); 367 Args.nullNotPermitted(chart, "chart"); 368 BufferedImage image = chart.createBufferedImage(width, height, 369 BufferedImage.TYPE_INT_RGB, info); 370 EncoderUtil.writeBufferedImage(image, ImageFormat.JPEG, out); 371 372 } 373 374 /** 375 * Writes a chart to an output stream in JPEG format. This method allows 376 * you to pass in a {@link ChartRenderingInfo} object, to collect 377 * information about the chart dimensions/entities. You will need this 378 * info if you want to create an HTML image map. 379 * 380 * @param out the output stream ({@code null} not permitted). 381 * @param quality the output quality (0.0f to 1.0f). 382 * @param chart the chart ({@code null} not permitted). 383 * @param width the image width. 384 * @param height the image height. 385 * @param info the chart rendering info ({@code null} permitted). 386 * 387 * @throws IOException if there are any I/O errors. 388 */ 389 public static void writeChartAsJPEG(OutputStream out, float quality, 390 JFreeChart chart, int width, int height, ChartRenderingInfo info) 391 throws IOException { 392 393 Args.nullNotPermitted(out, "out"); 394 Args.nullNotPermitted(chart, "chart"); 395 BufferedImage image = chart.createBufferedImage(width, height, 396 BufferedImage.TYPE_INT_RGB, info); 397 EncoderUtil.writeBufferedImage(image, ImageFormat.JPEG, out, quality); 398 399 } 400 401 /** 402 * Saves a chart to a file in JPEG format. 403 * 404 * @param file the file ({@code null} not permitted). 405 * @param chart the chart ({@code null} not permitted). 406 * @param width the image width. 407 * @param height the image height. 408 * 409 * @throws IOException if there are any I/O errors. 410 */ 411 public static void saveChartAsJPEG(File file, JFreeChart chart, 412 int width, int height) throws IOException { 413 414 // defer argument checking... 415 saveChartAsJPEG(file, chart, width, height, null); 416 417 } 418 419 /** 420 * Saves a chart to a file in JPEG format. 421 * 422 * @param file the file ({@code null} not permitted). 423 * @param quality the JPEG quality setting. 424 * @param chart the chart ({@code null} not permitted). 425 * @param width the image width. 426 * @param height the image height. 427 * 428 * @throws IOException if there are any I/O errors. 429 */ 430 public static void saveChartAsJPEG(File file, float quality, 431 JFreeChart chart, int width, int height) throws IOException { 432 433 // defer argument checking... 434 saveChartAsJPEG(file, quality, chart, width, height, null); 435 436 } 437 438 /** 439 * Saves a chart to a file in JPEG format. This method allows you to pass 440 * in a {@link ChartRenderingInfo} object, to collect information about the 441 * chart dimensions/entities. You will need this info if you want to 442 * create an HTML image map. 443 * 444 * @param file the file name ({@code null} not permitted). 445 * @param chart the chart ({@code null} not permitted). 446 * @param width the image width. 447 * @param height the image height. 448 * @param info the chart rendering info ({@code null} permitted). 449 * 450 * @throws IOException if there are any I/O errors. 451 */ 452 public static void saveChartAsJPEG(File file, JFreeChart chart, 453 int width, int height, ChartRenderingInfo info) throws IOException { 454 455 Args.nullNotPermitted(file, "file"); 456 Args.nullNotPermitted(chart, "chart"); 457 OutputStream out = new BufferedOutputStream(new FileOutputStream(file)); 458 try { 459 writeChartAsJPEG(out, chart, width, height, info); 460 } 461 finally { 462 out.close(); 463 } 464 465 } 466 467 /** 468 * Saves a chart to a file in JPEG format. This method allows you to pass 469 * in a {@link ChartRenderingInfo} object, to collect information about the 470 * chart dimensions/entities. You will need this info if you want to 471 * create an HTML image map. 472 * 473 * @param file the file name ({@code null} not permitted). 474 * @param quality the quality setting. 475 * @param chart the chart ({@code null} not permitted). 476 * @param width the image width. 477 * @param height the image height. 478 * @param info the chart rendering info ({@code null} permitted). 479 * 480 * @throws IOException if there are any I/O errors. 481 */ 482 public static void saveChartAsJPEG(File file, float quality, 483 JFreeChart chart, int width, int height, 484 ChartRenderingInfo info) throws IOException { 485 486 Args.nullNotPermitted(file, "file"); 487 Args.nullNotPermitted(chart, "chart"); 488 OutputStream out = new BufferedOutputStream(new FileOutputStream( 489 file)); 490 try { 491 writeChartAsJPEG(out, quality, chart, width, height, info); 492 } 493 finally { 494 out.close(); 495 } 496 497 } 498 499 /** 500 * Writes a {@link BufferedImage} to an output stream in JPEG format. 501 * 502 * @param out the output stream ({@code null} not permitted). 503 * @param image the image ({@code null} not permitted). 504 * 505 * @throws IOException if there are any I/O errors. 506 */ 507 public static void writeBufferedImageAsJPEG(OutputStream out, 508 BufferedImage image) throws IOException { 509 510 // defer argument checking... 511 writeBufferedImageAsJPEG(out, 0.75f, image); 512 513 } 514 515 /** 516 * Writes a {@link BufferedImage} to an output stream in JPEG format. 517 * 518 * @param out the output stream ({@code null} not permitted). 519 * @param quality the image quality (0.0f to 1.0f). 520 * @param image the image ({@code null} not permitted). 521 * 522 * @throws IOException if there are any I/O errors. 523 */ 524 public static void writeBufferedImageAsJPEG(OutputStream out, float quality, 525 BufferedImage image) throws IOException { 526 527 EncoderUtil.writeBufferedImage(image, ImageFormat.JPEG, out, quality); 528 529 } 530 531 /** 532 * Writes a {@link BufferedImage} to an output stream in PNG format. 533 * 534 * @param out the output stream ({@code null} not permitted). 535 * @param image the image ({@code null} not permitted). 536 * 537 * @throws IOException if there are any I/O errors. 538 */ 539 public static void writeBufferedImageAsPNG(OutputStream out, 540 BufferedImage image) throws IOException { 541 542 EncoderUtil.writeBufferedImage(image, ImageFormat.PNG, out); 543 544 } 545 546 /** 547 * Writes a {@link BufferedImage} to an output stream in PNG format. 548 * 549 * @param out the output stream ({@code null} not permitted). 550 * @param image the image ({@code null} not permitted). 551 * @param encodeAlpha encode alpha? 552 * @param compression the compression level (0-9). 553 * 554 * @throws IOException if there are any I/O errors. 555 */ 556 public static void writeBufferedImageAsPNG(OutputStream out, 557 BufferedImage image, boolean encodeAlpha, int compression) 558 throws IOException { 559 560 EncoderUtil.writeBufferedImage(image, ImageFormat.PNG, out, 561 compression, encodeAlpha); 562 } 563 564 /** 565 * Encodes a {@link BufferedImage} to PNG format. 566 * 567 * @param image the image ({@code null} not permitted). 568 * 569 * @return A byte array in PNG format. 570 * 571 * @throws IOException if there is an I/O problem. 572 */ 573 public static byte[] encodeAsPNG(BufferedImage image) throws IOException { 574 return EncoderUtil.encode(image, ImageFormat.PNG); 575 } 576 577 /** 578 * Encodes a {@link BufferedImage} to PNG format. 579 * 580 * @param image the image ({@code null} not permitted). 581 * @param encodeAlpha encode alpha? 582 * @param compression the PNG compression level (0-9). 583 * 584 * @return The byte array in PNG format. 585 * 586 * @throws IOException if there is an I/O problem. 587 */ 588 public static byte[] encodeAsPNG(BufferedImage image, boolean encodeAlpha, 589 int compression) throws IOException { 590 return EncoderUtil.encode(image, ImageFormat.PNG, compression, 591 encodeAlpha); 592 } 593 594 /** 595 * Writes an image map to an output stream. 596 * 597 * @param writer the writer ({@code null} not permitted). 598 * @param name the map name ({@code null} not permitted). 599 * @param info the chart rendering info ({@code null} not permitted). 600 * @param useOverLibForToolTips whether to use OverLIB for tooltips 601 * (http://www.bosrup.com/web/overlib/). 602 * 603 * @throws IOException if there are any I/O errors. 604 */ 605 public static void writeImageMap(PrintWriter writer, String name, 606 ChartRenderingInfo info, boolean useOverLibForToolTips) 607 throws IOException { 608 609 ToolTipTagFragmentGenerator toolTipTagFragmentGenerator; 610 if (useOverLibForToolTips) { 611 toolTipTagFragmentGenerator 612 = new OverLIBToolTipTagFragmentGenerator(); 613 } 614 else { 615 toolTipTagFragmentGenerator 616 = new StandardToolTipTagFragmentGenerator(); 617 } 618 ImageMapUtils.writeImageMap(writer, name, info, 619 toolTipTagFragmentGenerator, 620 new StandardURLTagFragmentGenerator()); 621 622 } 623 624 /** 625 * Writes an image map to the specified writer. 626 * 627 * @param writer the writer ({@code null} not permitted). 628 * @param name the map name ({@code null} not permitted). 629 * @param info the chart rendering info ({@code null} not permitted). 630 * @param toolTipTagFragmentGenerator a generator for the HTML fragment 631 * that will contain the tooltip text ({@code null} not permitted 632 * if {@code info} contains tooltip information). 633 * @param urlTagFragmentGenerator a generator for the HTML fragment that 634 * will contain the URL reference ({@code null} not permitted if 635 * {@code info} contains URLs). 636 * 637 * @throws IOException if there are any I/O errors. 638 */ 639 public static void writeImageMap(PrintWriter writer, String name, 640 ChartRenderingInfo info, 641 ToolTipTagFragmentGenerator toolTipTagFragmentGenerator, 642 URLTagFragmentGenerator urlTagFragmentGenerator) 643 throws IOException { 644 645 writer.println(ImageMapUtils.getImageMap(name, info, 646 toolTipTagFragmentGenerator, urlTagFragmentGenerator)); 647 } 648 649 /** 650 * Creates an HTML image map. This method maps to 651 * {@link ImageMapUtils#getImageMap(String, ChartRenderingInfo, 652 * ToolTipTagFragmentGenerator, URLTagFragmentGenerator)}, using default 653 * generators. 654 * 655 * @param name the map name ({@code null} not permitted). 656 * @param info the chart rendering info ({@code null} not permitted). 657 * 658 * @return The map tag. 659 */ 660 public static String getImageMap(String name, ChartRenderingInfo info) { 661 return ImageMapUtils.getImageMap(name, info, 662 new StandardToolTipTagFragmentGenerator(), 663 new StandardURLTagFragmentGenerator()); 664 } 665 666 /** 667 * Creates an HTML image map. This method maps directly to 668 * {@link ImageMapUtils#getImageMap(String, ChartRenderingInfo, 669 * ToolTipTagFragmentGenerator, URLTagFragmentGenerator)}. 670 * 671 * @param name the map name ({@code null} not permitted). 672 * @param info the chart rendering info ({@code null} not permitted). 673 * @param toolTipTagFragmentGenerator a generator for the HTML fragment 674 * that will contain the tooltip text ({@code null} not permitted 675 * if {@code info} contains tooltip information). 676 * @param urlTagFragmentGenerator a generator for the HTML fragment that 677 * will contain the URL reference ({@code null} not permitted if 678 * {@code info} contains URLs). 679 * 680 * @return The map tag. 681 */ 682 public static String getImageMap(String name, ChartRenderingInfo info, 683 ToolTipTagFragmentGenerator toolTipTagFragmentGenerator, 684 URLTagFragmentGenerator urlTagFragmentGenerator) { 685 686 return ImageMapUtils.getImageMap(name, info, 687 toolTipTagFragmentGenerator, urlTagFragmentGenerator); 688 689 } 690 691}