001 /* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2008, 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 * MeterPlot.java 029 * -------------- 030 * (C) Copyright 2000-2008, by Hari and Contributors. 031 * 032 * Original Author: Hari (ourhari@hotmail.com); 033 * Contributor(s): David Gilbert (for Object Refinery Limited); 034 * Bob Orchard; 035 * Arnaud Lelievre; 036 * Nicolas Brodu; 037 * David Bastend; 038 * 039 * Changes 040 * ------- 041 * 01-Apr-2002 : Version 1, contributed by Hari (DG); 042 * 23-Apr-2002 : Moved dataset from JFreeChart to Plot (DG); 043 * 22-Aug-2002 : Added changes suggest by Bob Orchard, changed Color to Paint 044 * for consistency, plus added Javadoc comments (DG); 045 * 01-Oct-2002 : Fixed errors reported by Checkstyle (DG); 046 * 23-Jan-2003 : Removed one constructor (DG); 047 * 26-Mar-2003 : Implemented Serializable (DG); 048 * 20-Aug-2003 : Changed dataset from MeterDataset --> ValueDataset, added 049 * equals() method, 050 * 08-Sep-2003 : Added internationalization via use of properties 051 * resourceBundle (RFE 690236) (AL); 052 * implemented Cloneable, and various other changes (DG); 053 * 08-Sep-2003 : Added serialization methods (NB); 054 * 11-Sep-2003 : Added cloning support (NB); 055 * 16-Sep-2003 : Changed ChartRenderingInfo --> PlotRenderingInfo (DG); 056 * 25-Sep-2003 : Fix useless cloning. Correct dataset listener registration in 057 * constructor. (NB) 058 * 29-Oct-2003 : Added workaround for font alignment in PDF output (DG); 059 * 17-Jan-2004 : Changed to allow dialBackgroundPaint to be set to null - see 060 * bug 823628 (DG); 061 * 07-Apr-2004 : Changed string bounds calculation (DG); 062 * 12-May-2004 : Added tickLabelFormat attribute - see RFE 949566. Also 063 * updated the equals() method (DG); 064 * 02-Nov-2004 : Added sanity checks for range, and only draw the needle if the 065 * value is contained within the overall range - see bug report 066 * 1056047 (DG); 067 * 11-Jan-2005 : Removed deprecated code in preparation for the 1.0.0 068 * release (DG); 069 * 02-Feb-2005 : Added optional background paint for each region (DG); 070 * 22-Mar-2005 : Removed 'normal', 'warning' and 'critical' regions and put in 071 * facility to define an arbitrary number of MeterIntervals, 072 * based on a contribution by David Bastend (DG); 073 * 20-Apr-2005 : Small update for change to LegendItem constructors (DG); 074 * 05-May-2005 : Updated draw() method parameters (DG); 075 * 08-Jun-2005 : Fixed equals() method to handle GradientPaint (DG); 076 * 10-Nov-2005 : Added tickPaint, tickSize and valuePaint attributes, and 077 * put value label drawing code into a separate method (DG); 078 * ------------- JFREECHART 1.0.x --------------------------------------------- 079 * 05-Mar-2007 : Restore clip region correctly (see bug 1667750) (DG); 080 * 18-May-2007 : Set dataset for LegendItem (DG); 081 * 29-Nov-2007 : Fixed serialization bug with dialOutlinePaint (DG); 082 * 18-Dec-2008 : Use ResourceBundleWrapper - see patch 1607918 by 083 * Jess Thrysoee (DG); 084 * 085 */ 086 087 package org.jfree.chart.plot; 088 089 import java.awt.AlphaComposite; 090 import java.awt.BasicStroke; 091 import java.awt.Color; 092 import java.awt.Composite; 093 import java.awt.Font; 094 import java.awt.FontMetrics; 095 import java.awt.Graphics2D; 096 import java.awt.Paint; 097 import java.awt.Polygon; 098 import java.awt.Shape; 099 import java.awt.Stroke; 100 import java.awt.geom.Arc2D; 101 import java.awt.geom.Ellipse2D; 102 import java.awt.geom.Line2D; 103 import java.awt.geom.Point2D; 104 import java.awt.geom.Rectangle2D; 105 import java.io.IOException; 106 import java.io.ObjectInputStream; 107 import java.io.ObjectOutputStream; 108 import java.io.Serializable; 109 import java.text.NumberFormat; 110 import java.util.Collections; 111 import java.util.Iterator; 112 import java.util.List; 113 import java.util.ResourceBundle; 114 115 import org.jfree.chart.LegendItem; 116 import org.jfree.chart.LegendItemCollection; 117 import org.jfree.chart.event.PlotChangeEvent; 118 import org.jfree.chart.util.ResourceBundleWrapper; 119 import org.jfree.data.Range; 120 import org.jfree.data.general.DatasetChangeEvent; 121 import org.jfree.data.general.ValueDataset; 122 import org.jfree.io.SerialUtilities; 123 import org.jfree.text.TextUtilities; 124 import org.jfree.ui.RectangleInsets; 125 import org.jfree.ui.TextAnchor; 126 import org.jfree.util.ObjectUtilities; 127 import org.jfree.util.PaintUtilities; 128 129 /** 130 * A plot that displays a single value in the form of a needle on a dial. 131 * Defined ranges (for example, 'normal', 'warning' and 'critical') can be 132 * highlighted on the dial. 133 */ 134 public class MeterPlot extends Plot implements Serializable, Cloneable { 135 136 /** For serialization. */ 137 private static final long serialVersionUID = 2987472457734470962L; 138 139 /** The default background paint. */ 140 static final Paint DEFAULT_DIAL_BACKGROUND_PAINT = Color.black; 141 142 /** The default needle paint. */ 143 static final Paint DEFAULT_NEEDLE_PAINT = Color.green; 144 145 /** The default value font. */ 146 static final Font DEFAULT_VALUE_FONT = new Font("SansSerif", Font.BOLD, 12); 147 148 /** The default value paint. */ 149 static final Paint DEFAULT_VALUE_PAINT = Color.yellow; 150 151 /** The default meter angle. */ 152 public static final int DEFAULT_METER_ANGLE = 270; 153 154 /** The default border size. */ 155 public static final float DEFAULT_BORDER_SIZE = 3f; 156 157 /** The default circle size. */ 158 public static final float DEFAULT_CIRCLE_SIZE = 10f; 159 160 /** The default label font. */ 161 public static final Font DEFAULT_LABEL_FONT = new Font("SansSerif", 162 Font.BOLD, 10); 163 164 /** The dataset (contains a single value). */ 165 private ValueDataset dataset; 166 167 /** The dial shape (background shape). */ 168 private DialShape shape; 169 170 /** The dial extent (measured in degrees). */ 171 private int meterAngle; 172 173 /** The overall range of data values on the dial. */ 174 private Range range; 175 176 /** The tick size. */ 177 private double tickSize; 178 179 /** The paint used to draw the ticks. */ 180 private transient Paint tickPaint; 181 182 /** The units displayed on the dial. */ 183 private String units; 184 185 /** The font for the value displayed in the center of the dial. */ 186 private Font valueFont; 187 188 /** The paint for the value displayed in the center of the dial. */ 189 private transient Paint valuePaint; 190 191 /** A flag that controls whether or not the border is drawn. */ 192 private boolean drawBorder; 193 194 /** The outline paint. */ 195 private transient Paint dialOutlinePaint; 196 197 /** The paint for the dial background. */ 198 private transient Paint dialBackgroundPaint; 199 200 /** The paint for the needle. */ 201 private transient Paint needlePaint; 202 203 /** A flag that controls whether or not the tick labels are visible. */ 204 private boolean tickLabelsVisible; 205 206 /** The tick label font. */ 207 private Font tickLabelFont; 208 209 /** The tick label paint. */ 210 private transient Paint tickLabelPaint; 211 212 /** The tick label format. */ 213 private NumberFormat tickLabelFormat; 214 215 /** The resourceBundle for the localization. */ 216 protected static ResourceBundle localizationResources 217 = ResourceBundleWrapper.getBundle( 218 "org.jfree.chart.plot.LocalizationBundle"); 219 220 /** 221 * A (possibly empty) list of the {@link MeterInterval}s to be highlighted 222 * on the dial. 223 */ 224 private List intervals; 225 226 /** 227 * Creates a new plot with a default range of <code>0</code> to 228 * <code>100</code> and no value to display. 229 */ 230 public MeterPlot() { 231 this(null); 232 } 233 234 /** 235 * Creates a new plot that displays the value from the supplied dataset. 236 * 237 * @param dataset the dataset (<code>null</code> permitted). 238 */ 239 public MeterPlot(ValueDataset dataset) { 240 super(); 241 this.shape = DialShape.CIRCLE; 242 this.meterAngle = DEFAULT_METER_ANGLE; 243 this.range = new Range(0.0, 100.0); 244 this.tickSize = 10.0; 245 this.tickPaint = Color.white; 246 this.units = "Units"; 247 this.needlePaint = MeterPlot.DEFAULT_NEEDLE_PAINT; 248 this.tickLabelsVisible = true; 249 this.tickLabelFont = MeterPlot.DEFAULT_LABEL_FONT; 250 this.tickLabelPaint = Color.black; 251 this.tickLabelFormat = NumberFormat.getInstance(); 252 this.valueFont = MeterPlot.DEFAULT_VALUE_FONT; 253 this.valuePaint = MeterPlot.DEFAULT_VALUE_PAINT; 254 this.dialBackgroundPaint = MeterPlot.DEFAULT_DIAL_BACKGROUND_PAINT; 255 this.intervals = new java.util.ArrayList(); 256 setDataset(dataset); 257 } 258 259 /** 260 * Returns the dial shape. The default is {@link DialShape#CIRCLE}). 261 * 262 * @return The dial shape (never <code>null</code>). 263 * 264 * @see #setDialShape(DialShape) 265 */ 266 public DialShape getDialShape() { 267 return this.shape; 268 } 269 270 /** 271 * Sets the dial shape and sends a {@link PlotChangeEvent} to all 272 * registered listeners. 273 * 274 * @param shape the shape (<code>null</code> not permitted). 275 * 276 * @see #getDialShape() 277 */ 278 public void setDialShape(DialShape shape) { 279 if (shape == null) { 280 throw new IllegalArgumentException("Null 'shape' argument."); 281 } 282 this.shape = shape; 283 fireChangeEvent(); 284 } 285 286 /** 287 * Returns the meter angle in degrees. This defines, in part, the shape 288 * of the dial. The default is 270 degrees. 289 * 290 * @return The meter angle (in degrees). 291 * 292 * @see #setMeterAngle(int) 293 */ 294 public int getMeterAngle() { 295 return this.meterAngle; 296 } 297 298 /** 299 * Sets the angle (in degrees) for the whole range of the dial and sends 300 * a {@link PlotChangeEvent} to all registered listeners. 301 * 302 * @param angle the angle (in degrees, in the range 1-360). 303 * 304 * @see #getMeterAngle() 305 */ 306 public void setMeterAngle(int angle) { 307 if (angle < 1 || angle > 360) { 308 throw new IllegalArgumentException("Invalid 'angle' (" + angle 309 + ")"); 310 } 311 this.meterAngle = angle; 312 fireChangeEvent(); 313 } 314 315 /** 316 * Returns the overall range for the dial. 317 * 318 * @return The overall range (never <code>null</code>). 319 * 320 * @see #setRange(Range) 321 */ 322 public Range getRange() { 323 return this.range; 324 } 325 326 /** 327 * Sets the range for the dial and sends a {@link PlotChangeEvent} to all 328 * registered listeners. 329 * 330 * @param range the range (<code>null</code> not permitted and zero-length 331 * ranges not permitted). 332 * 333 * @see #getRange() 334 */ 335 public void setRange(Range range) { 336 if (range == null) { 337 throw new IllegalArgumentException("Null 'range' argument."); 338 } 339 if (!(range.getLength() > 0.0)) { 340 throw new IllegalArgumentException( 341 "Range length must be positive."); 342 } 343 this.range = range; 344 fireChangeEvent(); 345 } 346 347 /** 348 * Returns the tick size (the interval between ticks on the dial). 349 * 350 * @return The tick size. 351 * 352 * @see #setTickSize(double) 353 */ 354 public double getTickSize() { 355 return this.tickSize; 356 } 357 358 /** 359 * Sets the tick size and sends a {@link PlotChangeEvent} to all 360 * registered listeners. 361 * 362 * @param size the tick size (must be > 0). 363 * 364 * @see #getTickSize() 365 */ 366 public void setTickSize(double size) { 367 if (size <= 0) { 368 throw new IllegalArgumentException("Requires 'size' > 0."); 369 } 370 this.tickSize = size; 371 fireChangeEvent(); 372 } 373 374 /** 375 * Returns the paint used to draw the ticks around the dial. 376 * 377 * @return The paint used to draw the ticks around the dial (never 378 * <code>null</code>). 379 * 380 * @see #setTickPaint(Paint) 381 */ 382 public Paint getTickPaint() { 383 return this.tickPaint; 384 } 385 386 /** 387 * Sets the paint used to draw the tick labels around the dial and sends 388 * a {@link PlotChangeEvent} to all registered listeners. 389 * 390 * @param paint the paint (<code>null</code> not permitted). 391 * 392 * @see #getTickPaint() 393 */ 394 public void setTickPaint(Paint paint) { 395 if (paint == null) { 396 throw new IllegalArgumentException("Null 'paint' argument."); 397 } 398 this.tickPaint = paint; 399 fireChangeEvent(); 400 } 401 402 /** 403 * Returns a string describing the units for the dial. 404 * 405 * @return The units (possibly <code>null</code>). 406 * 407 * @see #setUnits(String) 408 */ 409 public String getUnits() { 410 return this.units; 411 } 412 413 /** 414 * Sets the units for the dial and sends a {@link PlotChangeEvent} to all 415 * registered listeners. 416 * 417 * @param units the units (<code>null</code> permitted). 418 * 419 * @see #getUnits() 420 */ 421 public void setUnits(String units) { 422 this.units = units; 423 fireChangeEvent(); 424 } 425 426 /** 427 * Returns the paint for the needle. 428 * 429 * @return The paint (never <code>null</code>). 430 * 431 * @see #setNeedlePaint(Paint) 432 */ 433 public Paint getNeedlePaint() { 434 return this.needlePaint; 435 } 436 437 /** 438 * Sets the paint used to display the needle and sends a 439 * {@link PlotChangeEvent} to all registered listeners. 440 * 441 * @param paint the paint (<code>null</code> not permitted). 442 * 443 * @see #getNeedlePaint() 444 */ 445 public void setNeedlePaint(Paint paint) { 446 if (paint == null) { 447 throw new IllegalArgumentException("Null 'paint' argument."); 448 } 449 this.needlePaint = paint; 450 fireChangeEvent(); 451 } 452 453 /** 454 * Returns the flag that determines whether or not tick labels are visible. 455 * 456 * @return The flag. 457 * 458 * @see #setTickLabelsVisible(boolean) 459 */ 460 public boolean getTickLabelsVisible() { 461 return this.tickLabelsVisible; 462 } 463 464 /** 465 * Sets the flag that controls whether or not the tick labels are visible 466 * and sends a {@link PlotChangeEvent} to all registered listeners. 467 * 468 * @param visible the flag. 469 * 470 * @see #getTickLabelsVisible() 471 */ 472 public void setTickLabelsVisible(boolean visible) { 473 if (this.tickLabelsVisible != visible) { 474 this.tickLabelsVisible = visible; 475 fireChangeEvent(); 476 } 477 } 478 479 /** 480 * Returns the tick label font. 481 * 482 * @return The font (never <code>null</code>). 483 * 484 * @see #setTickLabelFont(Font) 485 */ 486 public Font getTickLabelFont() { 487 return this.tickLabelFont; 488 } 489 490 /** 491 * Sets the tick label font and sends a {@link PlotChangeEvent} to all 492 * registered listeners. 493 * 494 * @param font the font (<code>null</code> not permitted). 495 * 496 * @see #getTickLabelFont() 497 */ 498 public void setTickLabelFont(Font font) { 499 if (font == null) { 500 throw new IllegalArgumentException("Null 'font' argument."); 501 } 502 if (!this.tickLabelFont.equals(font)) { 503 this.tickLabelFont = font; 504 fireChangeEvent(); 505 } 506 } 507 508 /** 509 * Returns the tick label paint. 510 * 511 * @return The paint (never <code>null</code>). 512 * 513 * @see #setTickLabelPaint(Paint) 514 */ 515 public Paint getTickLabelPaint() { 516 return this.tickLabelPaint; 517 } 518 519 /** 520 * Sets the tick label paint and sends a {@link PlotChangeEvent} to all 521 * registered listeners. 522 * 523 * @param paint the paint (<code>null</code> not permitted). 524 * 525 * @see #getTickLabelPaint() 526 */ 527 public void setTickLabelPaint(Paint paint) { 528 if (paint == null) { 529 throw new IllegalArgumentException("Null 'paint' argument."); 530 } 531 if (!this.tickLabelPaint.equals(paint)) { 532 this.tickLabelPaint = paint; 533 fireChangeEvent(); 534 } 535 } 536 537 /** 538 * Returns the tick label format. 539 * 540 * @return The tick label format (never <code>null</code>). 541 * 542 * @see #setTickLabelFormat(NumberFormat) 543 */ 544 public NumberFormat getTickLabelFormat() { 545 return this.tickLabelFormat; 546 } 547 548 /** 549 * Sets the format for the tick labels and sends a {@link PlotChangeEvent} 550 * to all registered listeners. 551 * 552 * @param format the format (<code>null</code> not permitted). 553 * 554 * @see #getTickLabelFormat() 555 */ 556 public void setTickLabelFormat(NumberFormat format) { 557 if (format == null) { 558 throw new IllegalArgumentException("Null 'format' argument."); 559 } 560 this.tickLabelFormat = format; 561 fireChangeEvent(); 562 } 563 564 /** 565 * Returns the font for the value label. 566 * 567 * @return The font (never <code>null</code>). 568 * 569 * @see #setValueFont(Font) 570 */ 571 public Font getValueFont() { 572 return this.valueFont; 573 } 574 575 /** 576 * Sets the font used to display the value label and sends a 577 * {@link PlotChangeEvent} to all registered listeners. 578 * 579 * @param font the font (<code>null</code> not permitted). 580 * 581 * @see #getValueFont() 582 */ 583 public void setValueFont(Font font) { 584 if (font == null) { 585 throw new IllegalArgumentException("Null 'font' argument."); 586 } 587 this.valueFont = font; 588 fireChangeEvent(); 589 } 590 591 /** 592 * Returns the paint for the value label. 593 * 594 * @return The paint (never <code>null</code>). 595 * 596 * @see #setValuePaint(Paint) 597 */ 598 public Paint getValuePaint() { 599 return this.valuePaint; 600 } 601 602 /** 603 * Sets the paint used to display the value label and sends a 604 * {@link PlotChangeEvent} to all registered listeners. 605 * 606 * @param paint the paint (<code>null</code> not permitted). 607 * 608 * @see #getValuePaint() 609 */ 610 public void setValuePaint(Paint paint) { 611 if (paint == null) { 612 throw new IllegalArgumentException("Null 'paint' argument."); 613 } 614 this.valuePaint = paint; 615 fireChangeEvent(); 616 } 617 618 /** 619 * Returns the paint for the dial background. 620 * 621 * @return The paint (possibly <code>null</code>). 622 * 623 * @see #setDialBackgroundPaint(Paint) 624 */ 625 public Paint getDialBackgroundPaint() { 626 return this.dialBackgroundPaint; 627 } 628 629 /** 630 * Sets the paint used to fill the dial background. Set this to 631 * <code>null</code> for no background. 632 * 633 * @param paint the paint (<code>null</code> permitted). 634 * 635 * @see #getDialBackgroundPaint() 636 */ 637 public void setDialBackgroundPaint(Paint paint) { 638 this.dialBackgroundPaint = paint; 639 fireChangeEvent(); 640 } 641 642 /** 643 * Returns a flag that controls whether or not a rectangular border is 644 * drawn around the plot area. 645 * 646 * @return A flag. 647 * 648 * @see #setDrawBorder(boolean) 649 */ 650 public boolean getDrawBorder() { 651 return this.drawBorder; 652 } 653 654 /** 655 * Sets the flag that controls whether or not a rectangular border is drawn 656 * around the plot area and sends a {@link PlotChangeEvent} to all 657 * registered listeners. 658 * 659 * @param draw the flag. 660 * 661 * @see #getDrawBorder() 662 */ 663 public void setDrawBorder(boolean draw) { 664 // TODO: fix output when this flag is set to true 665 this.drawBorder = draw; 666 fireChangeEvent(); 667 } 668 669 /** 670 * Returns the dial outline paint. 671 * 672 * @return The paint. 673 * 674 * @see #setDialOutlinePaint(Paint) 675 */ 676 public Paint getDialOutlinePaint() { 677 return this.dialOutlinePaint; 678 } 679 680 /** 681 * Sets the dial outline paint and sends a {@link PlotChangeEvent} to all 682 * registered listeners. 683 * 684 * @param paint the paint. 685 * 686 * @see #getDialOutlinePaint() 687 */ 688 public void setDialOutlinePaint(Paint paint) { 689 this.dialOutlinePaint = paint; 690 fireChangeEvent(); 691 } 692 693 /** 694 * Returns the dataset for the plot. 695 * 696 * @return The dataset (possibly <code>null</code>). 697 * 698 * @see #setDataset(ValueDataset) 699 */ 700 public ValueDataset getDataset() { 701 return this.dataset; 702 } 703 704 /** 705 * Sets the dataset for the plot, replacing the existing dataset if there 706 * is one, and triggers a {@link PlotChangeEvent}. 707 * 708 * @param dataset the dataset (<code>null</code> permitted). 709 * 710 * @see #getDataset() 711 */ 712 public void setDataset(ValueDataset dataset) { 713 714 // if there is an existing dataset, remove the plot from the list of 715 // change listeners... 716 ValueDataset existing = this.dataset; 717 if (existing != null) { 718 existing.removeChangeListener(this); 719 } 720 721 // set the new dataset, and register the chart as a change listener... 722 this.dataset = dataset; 723 if (dataset != null) { 724 setDatasetGroup(dataset.getGroup()); 725 dataset.addChangeListener(this); 726 } 727 728 // send a dataset change event to self... 729 DatasetChangeEvent event = new DatasetChangeEvent(this, dataset); 730 datasetChanged(event); 731 732 } 733 734 /** 735 * Returns an unmodifiable list of the intervals for the plot. 736 * 737 * @return A list. 738 * 739 * @see #addInterval(MeterInterval) 740 */ 741 public List getIntervals() { 742 return Collections.unmodifiableList(this.intervals); 743 } 744 745 /** 746 * Adds an interval and sends a {@link PlotChangeEvent} to all registered 747 * listeners. 748 * 749 * @param interval the interval (<code>null</code> not permitted). 750 * 751 * @see #getIntervals() 752 * @see #clearIntervals() 753 */ 754 public void addInterval(MeterInterval interval) { 755 if (interval == null) { 756 throw new IllegalArgumentException("Null 'interval' argument."); 757 } 758 this.intervals.add(interval); 759 fireChangeEvent(); 760 } 761 762 /** 763 * Clears the intervals for the plot and sends a {@link PlotChangeEvent} to 764 * all registered listeners. 765 * 766 * @see #addInterval(MeterInterval) 767 */ 768 public void clearIntervals() { 769 this.intervals.clear(); 770 fireChangeEvent(); 771 } 772 773 /** 774 * Returns an item for each interval. 775 * 776 * @return A collection of legend items. 777 */ 778 public LegendItemCollection getLegendItems() { 779 LegendItemCollection result = new LegendItemCollection(); 780 Iterator iterator = this.intervals.iterator(); 781 while (iterator.hasNext()) { 782 MeterInterval mi = (MeterInterval) iterator.next(); 783 Paint color = mi.getBackgroundPaint(); 784 if (color == null) { 785 color = mi.getOutlinePaint(); 786 } 787 LegendItem item = new LegendItem(mi.getLabel(), mi.getLabel(), 788 null, null, new Rectangle2D.Double(-4.0, -4.0, 8.0, 8.0), 789 color); 790 item.setDataset(getDataset()); 791 result.add(item); 792 } 793 return result; 794 } 795 796 /** 797 * Draws the plot on a Java 2D graphics device (such as the screen or a 798 * printer). 799 * 800 * @param g2 the graphics device. 801 * @param area the area within which the plot should be drawn. 802 * @param anchor the anchor point (<code>null</code> permitted). 803 * @param parentState the state from the parent plot, if there is one. 804 * @param info collects info about the drawing. 805 */ 806 public void draw(Graphics2D g2, Rectangle2D area, Point2D anchor, 807 PlotState parentState, 808 PlotRenderingInfo info) { 809 810 if (info != null) { 811 info.setPlotArea(area); 812 } 813 814 // adjust for insets... 815 RectangleInsets insets = getInsets(); 816 insets.trim(area); 817 818 area.setRect(area.getX() + 4, area.getY() + 4, area.getWidth() - 8, 819 area.getHeight() - 8); 820 821 // draw the background 822 if (this.drawBorder) { 823 drawBackground(g2, area); 824 } 825 826 // adjust the plot area by the interior spacing value 827 double gapHorizontal = (2 * DEFAULT_BORDER_SIZE); 828 double gapVertical = (2 * DEFAULT_BORDER_SIZE); 829 double meterX = area.getX() + gapHorizontal / 2; 830 double meterY = area.getY() + gapVertical / 2; 831 double meterW = area.getWidth() - gapHorizontal; 832 double meterH = area.getHeight() - gapVertical 833 + ((this.meterAngle <= 180) && (this.shape != DialShape.CIRCLE) 834 ? area.getHeight() / 1.25 : 0); 835 836 double min = Math.min(meterW, meterH) / 2; 837 meterX = (meterX + meterX + meterW) / 2 - min; 838 meterY = (meterY + meterY + meterH) / 2 - min; 839 meterW = 2 * min; 840 meterH = 2 * min; 841 842 Rectangle2D meterArea = new Rectangle2D.Double(meterX, meterY, meterW, 843 meterH); 844 845 Rectangle2D.Double originalArea = new Rectangle2D.Double( 846 meterArea.getX() - 4, meterArea.getY() - 4, 847 meterArea.getWidth() + 8, meterArea.getHeight() + 8); 848 849 double meterMiddleX = meterArea.getCenterX(); 850 double meterMiddleY = meterArea.getCenterY(); 851 852 // plot the data (unless the dataset is null)... 853 ValueDataset data = getDataset(); 854 if (data != null) { 855 double dataMin = this.range.getLowerBound(); 856 double dataMax = this.range.getUpperBound(); 857 858 Shape savedClip = g2.getClip(); 859 g2.clip(originalArea); 860 Composite originalComposite = g2.getComposite(); 861 g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 862 getForegroundAlpha())); 863 864 if (this.dialBackgroundPaint != null) { 865 fillArc(g2, originalArea, dataMin, dataMax, 866 this.dialBackgroundPaint, true); 867 } 868 drawTicks(g2, meterArea, dataMin, dataMax); 869 drawArcForInterval(g2, meterArea, new MeterInterval("", this.range, 870 this.dialOutlinePaint, new BasicStroke(1.0f), null)); 871 872 Iterator iterator = this.intervals.iterator(); 873 while (iterator.hasNext()) { 874 MeterInterval interval = (MeterInterval) iterator.next(); 875 drawArcForInterval(g2, meterArea, interval); 876 } 877 878 Number n = data.getValue(); 879 if (n != null) { 880 double value = n.doubleValue(); 881 drawValueLabel(g2, meterArea); 882 883 if (this.range.contains(value)) { 884 g2.setPaint(this.needlePaint); 885 g2.setStroke(new BasicStroke(2.0f)); 886 887 double radius = (meterArea.getWidth() / 2) 888 + DEFAULT_BORDER_SIZE + 15; 889 double valueAngle = valueToAngle(value); 890 double valueP1 = meterMiddleX 891 + (radius * Math.cos(Math.PI * (valueAngle / 180))); 892 double valueP2 = meterMiddleY 893 - (radius * Math.sin(Math.PI * (valueAngle / 180))); 894 895 Polygon arrow = new Polygon(); 896 if ((valueAngle > 135 && valueAngle < 225) 897 || (valueAngle < 45 && valueAngle > -45)) { 898 899 double valueP3 = (meterMiddleY 900 - DEFAULT_CIRCLE_SIZE / 4); 901 double valueP4 = (meterMiddleY 902 + DEFAULT_CIRCLE_SIZE / 4); 903 arrow.addPoint((int) meterMiddleX, (int) valueP3); 904 arrow.addPoint((int) meterMiddleX, (int) valueP4); 905 906 } 907 else { 908 arrow.addPoint((int) (meterMiddleX 909 - DEFAULT_CIRCLE_SIZE / 4), (int) meterMiddleY); 910 arrow.addPoint((int) (meterMiddleX 911 + DEFAULT_CIRCLE_SIZE / 4), (int) meterMiddleY); 912 } 913 arrow.addPoint((int) valueP1, (int) valueP2); 914 g2.fill(arrow); 915 916 Ellipse2D circle = new Ellipse2D.Double(meterMiddleX 917 - DEFAULT_CIRCLE_SIZE / 2, meterMiddleY 918 - DEFAULT_CIRCLE_SIZE / 2, DEFAULT_CIRCLE_SIZE, 919 DEFAULT_CIRCLE_SIZE); 920 g2.fill(circle); 921 } 922 } 923 924 g2.setClip(savedClip); 925 g2.setComposite(originalComposite); 926 927 } 928 if (this.drawBorder) { 929 drawOutline(g2, area); 930 } 931 932 } 933 934 /** 935 * Draws the arc to represent an interval. 936 * 937 * @param g2 the graphics device. 938 * @param meterArea the drawing area. 939 * @param interval the interval. 940 */ 941 protected void drawArcForInterval(Graphics2D g2, Rectangle2D meterArea, 942 MeterInterval interval) { 943 944 double minValue = interval.getRange().getLowerBound(); 945 double maxValue = interval.getRange().getUpperBound(); 946 Paint outlinePaint = interval.getOutlinePaint(); 947 Stroke outlineStroke = interval.getOutlineStroke(); 948 Paint backgroundPaint = interval.getBackgroundPaint(); 949 950 if (backgroundPaint != null) { 951 fillArc(g2, meterArea, minValue, maxValue, backgroundPaint, false); 952 } 953 if (outlinePaint != null) { 954 if (outlineStroke != null) { 955 drawArc(g2, meterArea, minValue, maxValue, outlinePaint, 956 outlineStroke); 957 } 958 drawTick(g2, meterArea, minValue, true); 959 drawTick(g2, meterArea, maxValue, true); 960 } 961 } 962 963 /** 964 * Draws an arc. 965 * 966 * @param g2 the graphics device. 967 * @param area the plot area. 968 * @param minValue the minimum value. 969 * @param maxValue the maximum value. 970 * @param paint the paint. 971 * @param stroke the stroke. 972 */ 973 protected void drawArc(Graphics2D g2, Rectangle2D area, double minValue, 974 double maxValue, Paint paint, Stroke stroke) { 975 976 double startAngle = valueToAngle(maxValue); 977 double endAngle = valueToAngle(minValue); 978 double extent = endAngle - startAngle; 979 980 double x = area.getX(); 981 double y = area.getY(); 982 double w = area.getWidth(); 983 double h = area.getHeight(); 984 g2.setPaint(paint); 985 g2.setStroke(stroke); 986 987 if (paint != null && stroke != null) { 988 Arc2D.Double arc = new Arc2D.Double(x, y, w, h, startAngle, 989 extent, Arc2D.OPEN); 990 g2.setPaint(paint); 991 g2.setStroke(stroke); 992 g2.draw(arc); 993 } 994 995 } 996 997 /** 998 * Fills an arc on the dial between the given values. 999 * 1000 * @param g2 the graphics device. 1001 * @param area the plot area. 1002 * @param minValue the minimum data value. 1003 * @param maxValue the maximum data value. 1004 * @param paint the background paint (<code>null</code> not permitted). 1005 * @param dial a flag that indicates whether the arc represents the whole 1006 * dial. 1007 */ 1008 protected void fillArc(Graphics2D g2, Rectangle2D area, 1009 double minValue, double maxValue, Paint paint, 1010 boolean dial) { 1011 if (paint == null) { 1012 throw new IllegalArgumentException("Null 'paint' argument"); 1013 } 1014 double startAngle = valueToAngle(maxValue); 1015 double endAngle = valueToAngle(minValue); 1016 double extent = endAngle - startAngle; 1017 1018 double x = area.getX(); 1019 double y = area.getY(); 1020 double w = area.getWidth(); 1021 double h = area.getHeight(); 1022 int joinType = Arc2D.OPEN; 1023 if (this.shape == DialShape.PIE) { 1024 joinType = Arc2D.PIE; 1025 } 1026 else if (this.shape == DialShape.CHORD) { 1027 if (dial && this.meterAngle > 180) { 1028 joinType = Arc2D.CHORD; 1029 } 1030 else { 1031 joinType = Arc2D.PIE; 1032 } 1033 } 1034 else if (this.shape == DialShape.CIRCLE) { 1035 joinType = Arc2D.PIE; 1036 if (dial) { 1037 extent = 360; 1038 } 1039 } 1040 else { 1041 throw new IllegalStateException("DialShape not recognised."); 1042 } 1043 1044 g2.setPaint(paint); 1045 Arc2D.Double arc = new Arc2D.Double(x, y, w, h, startAngle, extent, 1046 joinType); 1047 g2.fill(arc); 1048 } 1049 1050 /** 1051 * Translates a data value to an angle on the dial. 1052 * 1053 * @param value the value. 1054 * 1055 * @return The angle on the dial. 1056 */ 1057 public double valueToAngle(double value) { 1058 value = value - this.range.getLowerBound(); 1059 double baseAngle = 180 + ((this.meterAngle - 180) / 2); 1060 return baseAngle - ((value / this.range.getLength()) * this.meterAngle); 1061 } 1062 1063 /** 1064 * Draws the ticks that subdivide the overall range. 1065 * 1066 * @param g2 the graphics device. 1067 * @param meterArea the meter area. 1068 * @param minValue the minimum value. 1069 * @param maxValue the maximum value. 1070 */ 1071 protected void drawTicks(Graphics2D g2, Rectangle2D meterArea, 1072 double minValue, double maxValue) { 1073 for (double v = minValue; v <= maxValue; v += this.tickSize) { 1074 drawTick(g2, meterArea, v); 1075 } 1076 } 1077 1078 /** 1079 * Draws a tick. 1080 * 1081 * @param g2 the graphics device. 1082 * @param meterArea the meter area. 1083 * @param value the value. 1084 */ 1085 protected void drawTick(Graphics2D g2, Rectangle2D meterArea, 1086 double value) { 1087 drawTick(g2, meterArea, value, false); 1088 } 1089 1090 /** 1091 * Draws a tick on the dial. 1092 * 1093 * @param g2 the graphics device. 1094 * @param meterArea the meter area. 1095 * @param value the tick value. 1096 * @param label a flag that controls whether or not a value label is drawn. 1097 */ 1098 protected void drawTick(Graphics2D g2, Rectangle2D meterArea, 1099 double value, boolean label) { 1100 1101 double valueAngle = valueToAngle(value); 1102 1103 double meterMiddleX = meterArea.getCenterX(); 1104 double meterMiddleY = meterArea.getCenterY(); 1105 1106 g2.setPaint(this.tickPaint); 1107 g2.setStroke(new BasicStroke(2.0f)); 1108 1109 double valueP2X = 0; 1110 double valueP2Y = 0; 1111 1112 double radius = (meterArea.getWidth() / 2) + DEFAULT_BORDER_SIZE; 1113 double radius1 = radius - 15; 1114 1115 double valueP1X = meterMiddleX 1116 + (radius * Math.cos(Math.PI * (valueAngle / 180))); 1117 double valueP1Y = meterMiddleY 1118 - (radius * Math.sin(Math.PI * (valueAngle / 180))); 1119 1120 valueP2X = meterMiddleX 1121 + (radius1 * Math.cos(Math.PI * (valueAngle / 180))); 1122 valueP2Y = meterMiddleY 1123 - (radius1 * Math.sin(Math.PI * (valueAngle / 180))); 1124 1125 Line2D.Double line = new Line2D.Double(valueP1X, valueP1Y, valueP2X, 1126 valueP2Y); 1127 g2.draw(line); 1128 1129 if (this.tickLabelsVisible && label) { 1130 1131 String tickLabel = this.tickLabelFormat.format(value); 1132 g2.setFont(this.tickLabelFont); 1133 g2.setPaint(this.tickLabelPaint); 1134 1135 FontMetrics fm = g2.getFontMetrics(); 1136 Rectangle2D tickLabelBounds 1137 = TextUtilities.getTextBounds(tickLabel, g2, fm); 1138 1139 double x = valueP2X; 1140 double y = valueP2Y; 1141 if (valueAngle == 90 || valueAngle == 270) { 1142 x = x - tickLabelBounds.getWidth() / 2; 1143 } 1144 else if (valueAngle < 90 || valueAngle > 270) { 1145 x = x - tickLabelBounds.getWidth(); 1146 } 1147 if ((valueAngle > 135 && valueAngle < 225) 1148 || valueAngle > 315 || valueAngle < 45) { 1149 y = y - tickLabelBounds.getHeight() / 2; 1150 } 1151 else { 1152 y = y + tickLabelBounds.getHeight() / 2; 1153 } 1154 g2.drawString(tickLabel, (float) x, (float) y); 1155 } 1156 } 1157 1158 /** 1159 * Draws the value label just below the center of the dial. 1160 * 1161 * @param g2 the graphics device. 1162 * @param area the plot area. 1163 */ 1164 protected void drawValueLabel(Graphics2D g2, Rectangle2D area) { 1165 g2.setFont(this.valueFont); 1166 g2.setPaint(this.valuePaint); 1167 String valueStr = "No value"; 1168 if (this.dataset != null) { 1169 Number n = this.dataset.getValue(); 1170 if (n != null) { 1171 valueStr = this.tickLabelFormat.format(n.doubleValue()) + " " 1172 + this.units; 1173 } 1174 } 1175 float x = (float) area.getCenterX(); 1176 float y = (float) area.getCenterY() + DEFAULT_CIRCLE_SIZE; 1177 TextUtilities.drawAlignedString(valueStr, g2, x, y, 1178 TextAnchor.TOP_CENTER); 1179 } 1180 1181 /** 1182 * Returns a short string describing the type of plot. 1183 * 1184 * @return A string describing the type of plot. 1185 */ 1186 public String getPlotType() { 1187 return localizationResources.getString("Meter_Plot"); 1188 } 1189 1190 /** 1191 * A zoom method that does nothing. Plots are required to support the 1192 * zoom operation. In the case of a meter plot, it doesn't make sense to 1193 * zoom in or out, so the method is empty. 1194 * 1195 * @param percent The zoom percentage. 1196 */ 1197 public void zoom(double percent) { 1198 // intentionally blank 1199 } 1200 1201 /** 1202 * Tests the plot for equality with an arbitrary object. Note that the 1203 * dataset is ignored for the purposes of testing equality. 1204 * 1205 * @param obj the object (<code>null</code> permitted). 1206 * 1207 * @return A boolean. 1208 */ 1209 public boolean equals(Object obj) { 1210 if (obj == this) { 1211 return true; 1212 } 1213 if (!(obj instanceof MeterPlot)) { 1214 return false; 1215 } 1216 if (!super.equals(obj)) { 1217 return false; 1218 } 1219 MeterPlot that = (MeterPlot) obj; 1220 if (!ObjectUtilities.equal(this.units, that.units)) { 1221 return false; 1222 } 1223 if (!ObjectUtilities.equal(this.range, that.range)) { 1224 return false; 1225 } 1226 if (!ObjectUtilities.equal(this.intervals, that.intervals)) { 1227 return false; 1228 } 1229 if (!PaintUtilities.equal(this.dialOutlinePaint, 1230 that.dialOutlinePaint)) { 1231 return false; 1232 } 1233 if (this.shape != that.shape) { 1234 return false; 1235 } 1236 if (!PaintUtilities.equal(this.dialBackgroundPaint, 1237 that.dialBackgroundPaint)) { 1238 return false; 1239 } 1240 if (!PaintUtilities.equal(this.needlePaint, that.needlePaint)) { 1241 return false; 1242 } 1243 if (!ObjectUtilities.equal(this.valueFont, that.valueFont)) { 1244 return false; 1245 } 1246 if (!PaintUtilities.equal(this.valuePaint, that.valuePaint)) { 1247 return false; 1248 } 1249 if (!PaintUtilities.equal(this.tickPaint, that.tickPaint)) { 1250 return false; 1251 } 1252 if (this.tickSize != that.tickSize) { 1253 return false; 1254 } 1255 if (this.tickLabelsVisible != that.tickLabelsVisible) { 1256 return false; 1257 } 1258 if (!ObjectUtilities.equal(this.tickLabelFont, that.tickLabelFont)) { 1259 return false; 1260 } 1261 if (!PaintUtilities.equal(this.tickLabelPaint, that.tickLabelPaint)) { 1262 return false; 1263 } 1264 if (!ObjectUtilities.equal(this.tickLabelFormat, 1265 that.tickLabelFormat)) { 1266 return false; 1267 } 1268 if (this.drawBorder != that.drawBorder) { 1269 return false; 1270 } 1271 if (this.meterAngle != that.meterAngle) { 1272 return false; 1273 } 1274 return true; 1275 } 1276 1277 /** 1278 * Provides serialization support. 1279 * 1280 * @param stream the output stream. 1281 * 1282 * @throws IOException if there is an I/O error. 1283 */ 1284 private void writeObject(ObjectOutputStream stream) throws IOException { 1285 stream.defaultWriteObject(); 1286 SerialUtilities.writePaint(this.dialBackgroundPaint, stream); 1287 SerialUtilities.writePaint(this.dialOutlinePaint, stream); 1288 SerialUtilities.writePaint(this.needlePaint, stream); 1289 SerialUtilities.writePaint(this.valuePaint, stream); 1290 SerialUtilities.writePaint(this.tickPaint, stream); 1291 SerialUtilities.writePaint(this.tickLabelPaint, stream); 1292 } 1293 1294 /** 1295 * Provides serialization support. 1296 * 1297 * @param stream the input stream. 1298 * 1299 * @throws IOException if there is an I/O error. 1300 * @throws ClassNotFoundException if there is a classpath problem. 1301 */ 1302 private void readObject(ObjectInputStream stream) 1303 throws IOException, ClassNotFoundException { 1304 stream.defaultReadObject(); 1305 this.dialBackgroundPaint = SerialUtilities.readPaint(stream); 1306 this.dialOutlinePaint = SerialUtilities.readPaint(stream); 1307 this.needlePaint = SerialUtilities.readPaint(stream); 1308 this.valuePaint = SerialUtilities.readPaint(stream); 1309 this.tickPaint = SerialUtilities.readPaint(stream); 1310 this.tickLabelPaint = SerialUtilities.readPaint(stream); 1311 if (this.dataset != null) { 1312 this.dataset.addChangeListener(this); 1313 } 1314 } 1315 1316 /** 1317 * Returns an independent copy (clone) of the plot. The dataset is NOT 1318 * cloned - both the original and the clone will have a reference to the 1319 * same dataset. 1320 * 1321 * @return A clone. 1322 * 1323 * @throws CloneNotSupportedException if some component of the plot cannot 1324 * be cloned. 1325 */ 1326 public Object clone() throws CloneNotSupportedException { 1327 MeterPlot clone = (MeterPlot) super.clone(); 1328 clone.tickLabelFormat = (NumberFormat) this.tickLabelFormat.clone(); 1329 // the following relies on the fact that the intervals are immutable 1330 clone.intervals = new java.util.ArrayList(this.intervals); 1331 if (clone.dataset != null) { 1332 clone.dataset.addChangeListener(clone); 1333 } 1334 return clone; 1335 } 1336 1337 }