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