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