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