001/* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2022, 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-2021, 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 java.awt.AlphaComposite; 044import java.awt.BasicStroke; 045import java.awt.Color; 046import java.awt.Composite; 047import java.awt.Font; 048import java.awt.FontMetrics; 049import java.awt.Graphics2D; 050import java.awt.Paint; 051import java.awt.Polygon; 052import java.awt.Shape; 053import java.awt.Stroke; 054import java.awt.geom.Arc2D; 055import java.awt.geom.Ellipse2D; 056import java.awt.geom.Line2D; 057import java.awt.geom.Point2D; 058import java.awt.geom.Rectangle2D; 059import java.io.IOException; 060import java.io.ObjectInputStream; 061import java.io.ObjectOutputStream; 062import java.io.Serializable; 063import java.text.NumberFormat; 064import java.util.ArrayList; 065import java.util.Collections; 066import java.util.List; 067import java.util.Objects; 068import java.util.ResourceBundle; 069 070import org.jfree.chart.legend.LegendItem; 071import org.jfree.chart.legend.LegendItemCollection; 072import org.jfree.chart.event.PlotChangeEvent; 073import org.jfree.chart.text.TextUtils; 074import org.jfree.chart.api.RectangleInsets; 075import org.jfree.chart.text.TextAnchor; 076import org.jfree.chart.internal.PaintUtils; 077import org.jfree.chart.internal.Args; 078import org.jfree.chart.internal.SerialUtils; 079import org.jfree.data.Range; 080import org.jfree.data.general.DatasetChangeEvent; 081import org.jfree.data.general.ValueDataset; 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 dataset.addChangeListener(this); 690 } 691 692 // send a dataset change event to self... 693 DatasetChangeEvent event = new DatasetChangeEvent(this, dataset); 694 datasetChanged(event); 695 696 } 697 698 /** 699 * Returns an unmodifiable list of the intervals for the plot. 700 * 701 * @return A list. 702 * 703 * @see #addInterval(MeterInterval) 704 */ 705 public List<MeterInterval> getIntervals() { 706 return Collections.unmodifiableList(intervals); 707 } 708 709 /** 710 * Adds an interval and sends a {@link PlotChangeEvent} to all registered 711 * listeners. 712 * 713 * @param interval the interval ({@code null} not permitted). 714 * 715 * @see #getIntervals() 716 * @see #clearIntervals() 717 */ 718 public void addInterval(MeterInterval interval) { 719 Args.nullNotPermitted(interval, "interval"); 720 intervals.add(interval); 721 fireChangeEvent(); 722 } 723 724 /** 725 * Clears the intervals for the plot and sends a {@link PlotChangeEvent} to 726 * all registered listeners. 727 * 728 * @see #addInterval(MeterInterval) 729 */ 730 public void clearIntervals() { 731 intervals.clear(); 732 fireChangeEvent(); 733 } 734 735 /** 736 * Returns an item for each interval. 737 * 738 * @return A collection of legend items. 739 */ 740 @Override 741 public LegendItemCollection getLegendItems() { 742 LegendItemCollection result = new LegendItemCollection(); 743 for (MeterInterval mi : intervals) { 744 Paint color = mi.getBackgroundPaint(); 745 if (color == null) { 746 color = mi.getOutlinePaint(); 747 } 748 LegendItem item = new LegendItem(mi.getLabel(), mi.getLabel(), 749 null, null, new Rectangle2D.Double(-4.0, -4.0, 8.0, 8.0), 750 color); 751 item.setDataset(getDataset()); 752 result.add(item); 753 } 754 return result; 755 } 756 757 /** 758 * Draws the plot on a Java 2D graphics device (such as the screen or a 759 * printer). 760 * 761 * @param g2 the graphics device. 762 * @param area the area within which the plot should be drawn. 763 * @param anchor the anchor point ({@code null} permitted). 764 * @param parentState the state from the parent plot, if there is one. 765 * @param info collects info about the drawing. 766 */ 767 @Override 768 public void draw(Graphics2D g2, Rectangle2D area, Point2D anchor, 769 PlotState parentState, PlotRenderingInfo info) { 770 771 if (info != null) { 772 info.setPlotArea(area); 773 } 774 775 // adjust for insets... 776 RectangleInsets insets = getInsets(); 777 insets.trim(area); 778 779 area.setRect(area.getX() + 4, area.getY() + 4, area.getWidth() - 8, 780 area.getHeight() - 8); 781 782 // draw the background 783 if (this.drawBorder) { 784 drawBackground(g2, area); 785 } 786 787 // adjust the plot area by the interior spacing value 788 double gapHorizontal = (2 * DEFAULT_BORDER_SIZE); 789 double gapVertical = (2 * DEFAULT_BORDER_SIZE); 790 double meterX = area.getX() + gapHorizontal / 2; 791 double meterY = area.getY() + gapVertical / 2; 792 double meterW = area.getWidth() - gapHorizontal; 793 double meterH = area.getHeight() - gapVertical 794 + ((this.meterAngle <= 180) && (this.shape != DialShape.CIRCLE) 795 ? area.getHeight() / 1.25 : 0); 796 797 double min = Math.min(meterW, meterH) / 2; 798 meterX = (meterX + meterX + meterW) / 2 - min; 799 meterY = (meterY + meterY + meterH) / 2 - min; 800 meterW = 2 * min; 801 meterH = 2 * min; 802 803 Rectangle2D meterArea = new Rectangle2D.Double(meterX, meterY, meterW, 804 meterH); 805 806 Rectangle2D.Double originalArea = new Rectangle2D.Double( 807 meterArea.getX() - 4, meterArea.getY() - 4, 808 meterArea.getWidth() + 8, meterArea.getHeight() + 8); 809 810 double meterMiddleX = meterArea.getCenterX(); 811 double meterMiddleY = meterArea.getCenterY(); 812 813 // plot the data (unless the dataset is null)... 814 ValueDataset data = getDataset(); 815 if (data != null) { 816 double dataMin = this.range.getLowerBound(); 817 double dataMax = this.range.getUpperBound(); 818 819 Shape savedClip = g2.getClip(); 820 g2.clip(originalArea); 821 Composite originalComposite = g2.getComposite(); 822 g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 823 getForegroundAlpha())); 824 825 if (this.dialBackgroundPaint != null) { 826 fillArc(g2, originalArea, dataMin, dataMax, 827 this.dialBackgroundPaint, true); 828 } 829 drawTicks(g2, meterArea, dataMin, dataMax); 830 drawArcForInterval(g2, meterArea, new MeterInterval("", this.range, 831 this.dialOutlinePaint, new BasicStroke(1.0f), null)); 832 833 for (MeterInterval interval : this.intervals) { 834 drawArcForInterval(g2, meterArea, interval); 835 } 836 837 Number n = data.getValue(); 838 if (n != null) { 839 double value = n.doubleValue(); 840 drawValueLabel(g2, meterArea); 841 842 if (this.range.contains(value)) { 843 g2.setPaint(this.needlePaint); 844 g2.setStroke(new BasicStroke(2.0f)); 845 846 double radius = (meterArea.getWidth() / 2) 847 + DEFAULT_BORDER_SIZE + 15; 848 double valueAngle = valueToAngle(value); 849 double valueP1 = meterMiddleX 850 + (radius * Math.cos(Math.PI * (valueAngle / 180))); 851 double valueP2 = meterMiddleY 852 - (radius * Math.sin(Math.PI * (valueAngle / 180))); 853 854 Polygon arrow = new Polygon(); 855 if ((valueAngle > 135 && valueAngle < 225) 856 || (valueAngle < 45 && valueAngle > -45)) { 857 858 double valueP3 = (meterMiddleY 859 - DEFAULT_CIRCLE_SIZE / 4); 860 double valueP4 = (meterMiddleY 861 + DEFAULT_CIRCLE_SIZE / 4); 862 arrow.addPoint((int) meterMiddleX, (int) valueP3); 863 arrow.addPoint((int) meterMiddleX, (int) valueP4); 864 865 } 866 else { 867 arrow.addPoint((int) (meterMiddleX 868 - DEFAULT_CIRCLE_SIZE / 4), (int) meterMiddleY); 869 arrow.addPoint((int) (meterMiddleX 870 + DEFAULT_CIRCLE_SIZE / 4), (int) meterMiddleY); 871 } 872 arrow.addPoint((int) valueP1, (int) valueP2); 873 g2.fill(arrow); 874 875 Ellipse2D circle = new Ellipse2D.Double(meterMiddleX 876 - DEFAULT_CIRCLE_SIZE / 2, meterMiddleY 877 - DEFAULT_CIRCLE_SIZE / 2, DEFAULT_CIRCLE_SIZE, 878 DEFAULT_CIRCLE_SIZE); 879 g2.fill(circle); 880 } 881 } 882 883 g2.setClip(savedClip); 884 g2.setComposite(originalComposite); 885 886 } 887 if (this.drawBorder) { 888 drawOutline(g2, area); 889 } 890 891 } 892 893 /** 894 * Draws the arc to represent an interval. 895 * 896 * @param g2 the graphics device. 897 * @param meterArea the drawing area. 898 * @param interval the interval. 899 */ 900 protected void drawArcForInterval(Graphics2D g2, Rectangle2D meterArea, 901 MeterInterval interval) { 902 903 double minValue = interval.getRange().getLowerBound(); 904 double maxValue = interval.getRange().getUpperBound(); 905 Paint outlinePaint = interval.getOutlinePaint(); 906 Stroke outlineStroke = interval.getOutlineStroke(); 907 Paint backgroundPaint = interval.getBackgroundPaint(); 908 909 if (backgroundPaint != null) { 910 fillArc(g2, meterArea, minValue, maxValue, backgroundPaint, false); 911 } 912 if (outlinePaint != null) { 913 if (outlineStroke != null) { 914 drawArc(g2, meterArea, minValue, maxValue, outlinePaint, 915 outlineStroke); 916 } 917 drawTick(g2, meterArea, minValue, true); 918 drawTick(g2, meterArea, maxValue, true); 919 } 920 } 921 922 /** 923 * Draws an arc. 924 * 925 * @param g2 the graphics device. 926 * @param area the plot area. 927 * @param minValue the minimum value. 928 * @param maxValue the maximum value. 929 * @param paint the paint. 930 * @param stroke the stroke. 931 */ 932 protected void drawArc(Graphics2D g2, Rectangle2D area, double minValue, 933 double maxValue, Paint paint, Stroke stroke) { 934 935 double startAngle = valueToAngle(maxValue); 936 double endAngle = valueToAngle(minValue); 937 double extent = endAngle - startAngle; 938 939 double x = area.getX(); 940 double y = area.getY(); 941 double w = area.getWidth(); 942 double h = area.getHeight(); 943 g2.setPaint(paint); 944 g2.setStroke(stroke); 945 946 if (paint != null && stroke != null) { 947 Arc2D.Double arc = new Arc2D.Double(x, y, w, h, startAngle, 948 extent, Arc2D.OPEN); 949 g2.setPaint(paint); 950 g2.setStroke(stroke); 951 g2.draw(arc); 952 } 953 954 } 955 956 /** 957 * Fills an arc on the dial between the given values. 958 * 959 * @param g2 the graphics device. 960 * @param area the plot area. 961 * @param minValue the minimum data value. 962 * @param maxValue the maximum data value. 963 * @param paint the background paint ({@code null} not permitted). 964 * @param dial a flag that indicates whether the arc represents the whole 965 * dial. 966 */ 967 protected void fillArc(Graphics2D g2, Rectangle2D area, 968 double minValue, double maxValue, Paint paint, boolean dial) { 969 970 Args.nullNotPermitted(paint, "paint"); 971 double startAngle = valueToAngle(maxValue); 972 double endAngle = valueToAngle(minValue); 973 double extent = endAngle - startAngle; 974 975 double x = area.getX(); 976 double y = area.getY(); 977 double w = area.getWidth(); 978 double h = area.getHeight(); 979 int joinType = Arc2D.OPEN; 980 if (this.shape == DialShape.PIE) { 981 joinType = Arc2D.PIE; 982 } 983 else if (this.shape == DialShape.CHORD) { 984 if (dial && this.meterAngle > 180) { 985 joinType = Arc2D.CHORD; 986 } 987 else { 988 joinType = Arc2D.PIE; 989 } 990 } 991 else if (this.shape == DialShape.CIRCLE) { 992 joinType = Arc2D.PIE; 993 if (dial) { 994 extent = 360; 995 } 996 } 997 else { 998 throw new IllegalStateException("DialShape not recognised."); 999 } 1000 1001 g2.setPaint(paint); 1002 Arc2D.Double arc = new Arc2D.Double(x, y, w, h, startAngle, extent, 1003 joinType); 1004 g2.fill(arc); 1005 } 1006 1007 /** 1008 * Translates a data value to an angle on the dial. 1009 * 1010 * @param value the value. 1011 * 1012 * @return The angle on the dial. 1013 */ 1014 public double valueToAngle(double value) { 1015 value = value - this.range.getLowerBound(); 1016 double baseAngle = 180 + ((this.meterAngle - 180) / 2.0); 1017 return baseAngle - ((value / this.range.getLength()) * this.meterAngle); 1018 } 1019 1020 /** 1021 * Draws the ticks that subdivide the overall range. 1022 * 1023 * @param g2 the graphics device. 1024 * @param meterArea the meter area. 1025 * @param minValue the minimum value. 1026 * @param maxValue the maximum value. 1027 */ 1028 protected void drawTicks(Graphics2D g2, Rectangle2D meterArea, 1029 double minValue, double maxValue) { 1030 for (double v = minValue; v <= maxValue; v += this.tickSize) { 1031 drawTick(g2, meterArea, v); 1032 } 1033 } 1034 1035 /** 1036 * Draws a tick. 1037 * 1038 * @param g2 the graphics device. 1039 * @param meterArea the meter area. 1040 * @param value the value. 1041 */ 1042 protected void drawTick(Graphics2D g2, Rectangle2D meterArea, 1043 double value) { 1044 drawTick(g2, meterArea, value, false); 1045 } 1046 1047 /** 1048 * Draws a tick on the dial. 1049 * 1050 * @param g2 the graphics device. 1051 * @param meterArea the meter area. 1052 * @param value the tick value. 1053 * @param label a flag that controls whether or not a value label is drawn. 1054 */ 1055 protected void drawTick(Graphics2D g2, Rectangle2D meterArea, 1056 double value, boolean label) { 1057 1058 double valueAngle = valueToAngle(value); 1059 1060 double meterMiddleX = meterArea.getCenterX(); 1061 double meterMiddleY = meterArea.getCenterY(); 1062 1063 g2.setPaint(this.tickPaint); 1064 g2.setStroke(new BasicStroke(2.0f)); 1065 1066 double valueP2X; 1067 double valueP2Y; 1068 1069 double radius = (meterArea.getWidth() / 2) + DEFAULT_BORDER_SIZE; 1070 double radius1 = radius - 15; 1071 1072 double valueP1X = meterMiddleX 1073 + (radius * Math.cos(Math.PI * (valueAngle / 180))); 1074 double valueP1Y = meterMiddleY 1075 - (radius * Math.sin(Math.PI * (valueAngle / 180))); 1076 1077 valueP2X = meterMiddleX 1078 + (radius1 * Math.cos(Math.PI * (valueAngle / 180))); 1079 valueP2Y = meterMiddleY 1080 - (radius1 * Math.sin(Math.PI * (valueAngle / 180))); 1081 1082 Line2D.Double line = new Line2D.Double(valueP1X, valueP1Y, valueP2X, 1083 valueP2Y); 1084 g2.draw(line); 1085 1086 if (this.tickLabelsVisible && label) { 1087 1088 String tickLabel = this.tickLabelFormat.format(value); 1089 g2.setFont(this.tickLabelFont); 1090 g2.setPaint(this.tickLabelPaint); 1091 1092 FontMetrics fm = g2.getFontMetrics(); 1093 Rectangle2D tickLabelBounds 1094 = TextUtils.getTextBounds(tickLabel, g2, fm); 1095 1096 double x = valueP2X; 1097 double y = valueP2Y; 1098 if (valueAngle == 90 || valueAngle == 270) { 1099 x = x - tickLabelBounds.getWidth() / 2; 1100 } 1101 else if (valueAngle < 90 || valueAngle > 270) { 1102 x = x - tickLabelBounds.getWidth(); 1103 } 1104 if ((valueAngle > 135 && valueAngle < 225) 1105 || valueAngle > 315 || valueAngle < 45) { 1106 y = y - tickLabelBounds.getHeight() / 2; 1107 } 1108 else { 1109 y = y + tickLabelBounds.getHeight() / 2; 1110 } 1111 g2.drawString(tickLabel, (float) x, (float) y); 1112 } 1113 } 1114 1115 /** 1116 * Draws the value label just below the center of the dial. 1117 * 1118 * @param g2 the graphics device. 1119 * @param area the plot area. 1120 */ 1121 protected void drawValueLabel(Graphics2D g2, Rectangle2D area) { 1122 if (valueVisible) { 1123 g2.setFont(this.valueFont); 1124 g2.setPaint(this.valuePaint); 1125 String valueStr = "No value"; 1126 if (this.dataset != null) { 1127 Number n = this.dataset.getValue(); 1128 if (n != null) { 1129 valueStr = this.tickLabelFormat.format(n.doubleValue()) + " " 1130 + this.units; 1131 } 1132 } 1133 float x = (float) area.getCenterX(); 1134 float y = (float) area.getCenterY() + DEFAULT_CIRCLE_SIZE; 1135 TextUtils.drawAlignedString(valueStr, g2, x, y, 1136 TextAnchor.TOP_CENTER); 1137 } 1138 } 1139 1140 /** 1141 * Returns a short string describing the type of plot. 1142 * 1143 * @return A string describing the type of plot. 1144 */ 1145 @Override 1146 public String getPlotType() { 1147 return localizationResources.getString("Meter_Plot"); 1148 } 1149 1150 /** 1151 * A zoom method that does nothing. Plots are required to support the 1152 * zoom operation. In the case of a meter plot, it doesn't make sense to 1153 * zoom in or out, so the method is empty. 1154 * 1155 * @param percent The zoom percentage. 1156 */ 1157 @Override 1158 public void zoom(double percent) { 1159 // intentionally blank 1160 } 1161 1162 /** 1163 * Tests the plot for equality with an arbitrary object. Note that the 1164 * dataset is ignored for the purposes of testing equality. 1165 * 1166 * @param obj the object ({@code null} permitted). 1167 * 1168 * @return A boolean. 1169 */ 1170 @Override 1171 public boolean equals(Object obj) { 1172 if (obj == this) { 1173 return true; 1174 } 1175 if (!(obj instanceof MeterPlot)) { 1176 return false; 1177 } 1178 if (!super.equals(obj)) { 1179 return false; 1180 } 1181 MeterPlot that = (MeterPlot) obj; 1182 if (!Objects.equals(this.units, that.units)) { 1183 return false; 1184 } 1185 if (!Objects.equals(this.range, that.range)) { 1186 return false; 1187 } 1188 if (!Objects.equals(this.intervals, that.intervals)) { 1189 return false; 1190 } 1191 if (!PaintUtils.equal(this.dialOutlinePaint, 1192 that.dialOutlinePaint)) { 1193 return false; 1194 } 1195 if (this.shape != that.shape) { 1196 return false; 1197 } 1198 if (!PaintUtils.equal(this.dialBackgroundPaint, 1199 that.dialBackgroundPaint)) { 1200 return false; 1201 } 1202 if (!PaintUtils.equal(this.needlePaint, that.needlePaint)) { 1203 return false; 1204 } 1205 if (this.valueVisible != that.valueVisible) { 1206 return false; 1207 } 1208 if (!Objects.equals(this.valueFont, that.valueFont)) { 1209 return false; 1210 } 1211 if (!PaintUtils.equal(this.valuePaint, that.valuePaint)) { 1212 return false; 1213 } 1214 if (!PaintUtils.equal(this.tickPaint, that.tickPaint)) { 1215 return false; 1216 } 1217 if (this.tickSize != that.tickSize) { 1218 return false; 1219 } 1220 if (this.tickLabelsVisible != that.tickLabelsVisible) { 1221 return false; 1222 } 1223 if (!Objects.equals(this.tickLabelFont, that.tickLabelFont)) { 1224 return false; 1225 } 1226 if (!PaintUtils.equal(this.tickLabelPaint, that.tickLabelPaint)) { 1227 return false; 1228 } 1229 if (!Objects.equals(this.tickLabelFormat, that.tickLabelFormat)) { 1230 return false; 1231 } 1232 if (this.drawBorder != that.drawBorder) { 1233 return false; 1234 } 1235 if (this.meterAngle != that.meterAngle) { 1236 return false; 1237 } 1238 return true; 1239 } 1240 1241 /** 1242 * Provides serialization support. 1243 * 1244 * @param stream the output stream. 1245 * 1246 * @throws IOException if there is an I/O error. 1247 */ 1248 private void writeObject(ObjectOutputStream stream) throws IOException { 1249 stream.defaultWriteObject(); 1250 SerialUtils.writePaint(this.dialBackgroundPaint, stream); 1251 SerialUtils.writePaint(this.dialOutlinePaint, stream); 1252 SerialUtils.writePaint(this.needlePaint, stream); 1253 SerialUtils.writePaint(this.valuePaint, stream); 1254 SerialUtils.writePaint(this.tickPaint, stream); 1255 SerialUtils.writePaint(this.tickLabelPaint, stream); 1256 } 1257 1258 /** 1259 * Provides serialization support. 1260 * 1261 * @param stream the input stream. 1262 * 1263 * @throws IOException if there is an I/O error. 1264 * @throws ClassNotFoundException if there is a classpath problem. 1265 */ 1266 private void readObject(ObjectInputStream stream) 1267 throws IOException, ClassNotFoundException { 1268 stream.defaultReadObject(); 1269 this.dialBackgroundPaint = SerialUtils.readPaint(stream); 1270 this.dialOutlinePaint = SerialUtils.readPaint(stream); 1271 this.needlePaint = SerialUtils.readPaint(stream); 1272 this.valuePaint = SerialUtils.readPaint(stream); 1273 this.tickPaint = SerialUtils.readPaint(stream); 1274 this.tickLabelPaint = SerialUtils.readPaint(stream); 1275 if (this.dataset != null) { 1276 this.dataset.addChangeListener(this); 1277 } 1278 } 1279 1280 /** 1281 * Returns an independent copy (clone) of the plot. The dataset is NOT 1282 * cloned - both the original and the clone will have a reference to the 1283 * same dataset. 1284 * 1285 * @return A clone. 1286 * 1287 * @throws CloneNotSupportedException if some component of the plot cannot 1288 * be cloned. 1289 */ 1290 @Override 1291 public Object clone() throws CloneNotSupportedException { 1292 MeterPlot clone = (MeterPlot) super.clone(); 1293 clone.tickLabelFormat = (NumberFormat) this.tickLabelFormat.clone(); 1294 // the following relies on the fact that the intervals are immutable 1295 clone.intervals = new ArrayList<>(this.intervals); 1296 if (clone.dataset != null) { 1297 clone.dataset.addChangeListener(clone); 1298 } 1299 return clone; 1300 } 1301 1302}