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