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