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