Source for org.jfree.chart.plot.MeterPlot

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