Source for org.jfree.chart.plot.CompassPlot

   1: /* ===========================================================
   2:  * JFreeChart : a free chart library for the Java(tm) platform
   3:  * ===========================================================
   4:  *
   5:  * (C) Copyright 2000-2007, 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:  * CompassPlot.java
  29:  * ----------------
  30:  * (C) Copyright 2002-2007, by the Australian Antarctic Division and 
  31:  * Contributors.
  32:  *
  33:  * Original Author:  Bryan Scott (for the Australian Antarctic Division);
  34:  * Contributor(s):   David Gilbert (for Object Refinery Limited);
  35:  *                   Arnaud Lelievre;
  36:  *
  37:  * $Id: CompassPlot.java,v 1.11.2.6 2007/03/20 21:52:16 mungady Exp $
  38:  *
  39:  * Changes:
  40:  * --------
  41:  * 25-Sep-2002 : Version 1, contributed by Bryan Scott (DG);
  42:  * 23-Jan-2003 : Removed one constructor (DG);
  43:  * 26-Mar-2003 : Implemented Serializable (DG);
  44:  * 27-Mar-2003 : Changed MeterDataset to ValueDataset (DG);
  45:  * 21-Aug-2003 : Implemented Cloneable (DG);
  46:  * 08-Sep-2003 : Added internationalization via use of properties 
  47:  *               resourceBundle (RFE 690236) (AL);
  48:  * 09-Sep-2003 : Changed Color --> Paint (DG);
  49:  * 15-Sep-2003 : Added null data value check (bug report 805009) (DG);
  50:  * 16-Sep-2003 : Changed ChartRenderingInfo --> PlotRenderingInfo (DG);
  51:  * 16-Mar-2004 : Added support for revolutionDistance to enable support for
  52:  *               other units than degrees.
  53:  * 16-Mar-2004 : Enabled LongNeedle to rotate about center.
  54:  * 11-Jan-2005 : Removed deprecated code in preparation for 1.0.0 release (DG);
  55:  * 17-Apr-2005 : Fixed bug in clone() method (DG);
  56:  * 05-May-2005 : Updated draw() method parameters (DG);
  57:  * 08-Jun-2005 : Fixed equals() method to handle GradientPaint (DG);
  58:  * 16-Jun-2005 : Renamed getData() --> getDatasets() and 
  59:  *               addData() --> addDataset() (DG);
  60:  * ------------- JFREECHART 1.0.x ---------------------------------------------
  61:  * 20-Mar-2007 : Fixed serialization (DG);
  62:  *
  63:  */
  64: 
  65: package org.jfree.chart.plot;
  66: 
  67: import java.awt.BasicStroke;
  68: import java.awt.Color;
  69: import java.awt.Font;
  70: import java.awt.Graphics2D;
  71: import java.awt.Paint;
  72: import java.awt.Polygon;
  73: import java.awt.Stroke;
  74: import java.awt.geom.Area;
  75: import java.awt.geom.Ellipse2D;
  76: import java.awt.geom.Point2D;
  77: import java.awt.geom.Rectangle2D;
  78: import java.io.IOException;
  79: import java.io.ObjectInputStream;
  80: import java.io.ObjectOutputStream;
  81: import java.io.Serializable;
  82: import java.util.Arrays;
  83: import java.util.ResourceBundle;
  84: 
  85: import org.jfree.chart.LegendItemCollection;
  86: import org.jfree.chart.event.PlotChangeEvent;
  87: import org.jfree.chart.needle.ArrowNeedle;
  88: import org.jfree.chart.needle.LineNeedle;
  89: import org.jfree.chart.needle.LongNeedle;
  90: import org.jfree.chart.needle.MeterNeedle;
  91: import org.jfree.chart.needle.MiddlePinNeedle;
  92: import org.jfree.chart.needle.PinNeedle;
  93: import org.jfree.chart.needle.PlumNeedle;
  94: import org.jfree.chart.needle.PointerNeedle;
  95: import org.jfree.chart.needle.ShipNeedle;
  96: import org.jfree.chart.needle.WindNeedle;
  97: import org.jfree.data.general.DefaultValueDataset;
  98: import org.jfree.data.general.ValueDataset;
  99: import org.jfree.io.SerialUtilities;
 100: import org.jfree.ui.RectangleInsets;
 101: import org.jfree.util.ObjectUtilities;
 102: import org.jfree.util.PaintUtilities;
 103: 
 104: /**
 105:  * A specialised plot that draws a compass to indicate a direction based on the
 106:  * value from a {@link ValueDataset}.
 107:  */
 108: public class CompassPlot extends Plot implements Cloneable, Serializable {
 109: 
 110:     /** For serialization. */
 111:     private static final long serialVersionUID = 6924382802125527395L;
 112:     
 113:     /** The default label font. */
 114:     public static final Font DEFAULT_LABEL_FONT = new Font("SansSerif", 
 115:             Font.BOLD, 10);
 116: 
 117:     /** A constant for the label type. */
 118:     public static final int NO_LABELS = 0;
 119: 
 120:     /** A constant for the label type. */
 121:     public static final int VALUE_LABELS = 1;
 122: 
 123:     /** The label type (NO_LABELS, VALUE_LABELS). */
 124:     private int labelType;
 125: 
 126:     /** The label font. */
 127:     private Font labelFont;
 128: 
 129:     /** A flag that controls whether or not a border is drawn. */
 130:     private boolean drawBorder = false;
 131: 
 132:     /** The rose highlight paint. */
 133:     private transient Paint roseHighlightPaint = Color.black;
 134: 
 135:     /** The rose paint. */
 136:     private transient Paint rosePaint = Color.yellow;
 137: 
 138:     /** The rose center paint. */
 139:     private transient Paint roseCenterPaint = Color.white;
 140: 
 141:     /** The compass font. */
 142:     private Font compassFont = new Font("Arial", Font.PLAIN, 10);
 143: 
 144:     /** A working shape. */
 145:     private transient Ellipse2D circle1;
 146: 
 147:     /** A working shape. */
 148:     private transient Ellipse2D circle2;
 149: 
 150:     /** A working area. */
 151:     private transient Area a1;
 152: 
 153:     /** A working area. */
 154:     private transient Area a2;
 155: 
 156:     /** A working shape. */
 157:     private transient Rectangle2D rect1;
 158: 
 159:     /** An array of value datasets. */
 160:     private ValueDataset[] datasets = new ValueDataset[1];
 161: 
 162:     /** An array of needles. */
 163:     private MeterNeedle[] seriesNeedle = new MeterNeedle[1];
 164: 
 165:     /** The resourceBundle for the localization. */
 166:     protected static ResourceBundle localizationResources 
 167:             = ResourceBundle.getBundle(
 168:                     "org.jfree.chart.plot.LocalizationBundle");
 169: 
 170:     /** 
 171:      * The count to complete one revolution.  Can be arbitrarily set
 172:      * For degrees (the default) it is 360, for radians this is 2*Pi, etc
 173:      */
 174:     protected double revolutionDistance = 360;
 175: 
 176:     /**
 177:      * Default constructor.
 178:      */
 179:     public CompassPlot() {
 180:         this(new DefaultValueDataset());
 181:     }
 182: 
 183:     /**
 184:      * Constructs a new compass plot.
 185:      *
 186:      * @param dataset  the dataset for the plot (<code>null</code> permitted).
 187:      */
 188:     public CompassPlot(ValueDataset dataset) {
 189:         super();
 190:         if (dataset != null) {
 191:             this.datasets[0] = dataset;
 192:             dataset.addChangeListener(this);
 193:         }
 194:         this.circle1 = new Ellipse2D.Double();
 195:         this.circle2 = new Ellipse2D.Double();
 196:         this.rect1   = new Rectangle2D.Double();
 197:         setSeriesNeedle(0);
 198:     }
 199: 
 200:     /**
 201:      * Returns the label type.  Defined by the constants: {@link #NO_LABELS}
 202:      * and {@link #VALUE_LABELS}.
 203:      *
 204:      * @return The label type.
 205:      * 
 206:      * @see #setLabelType(int)
 207:      */
 208:     public int getLabelType() {
 209:         // FIXME: this attribute is never used - deprecate?
 210:         return this.labelType;
 211:     }
 212: 
 213:     /**
 214:      * Sets the label type (either {@link #NO_LABELS} or {@link #VALUE_LABELS}.
 215:      *
 216:      * @param type  the type.
 217:      * 
 218:      * @see #getLabelType()
 219:      */
 220:     public void setLabelType(int type) {
 221:         // FIXME: this attribute is never used - deprecate?
 222:         if ((type != NO_LABELS) && (type != VALUE_LABELS)) {
 223:             throw new IllegalArgumentException(
 224:                     "MeterPlot.setLabelType(int): unrecognised type.");
 225:         }
 226:         if (this.labelType != type) {
 227:             this.labelType = type;
 228:             notifyListeners(new PlotChangeEvent(this));
 229:         }
 230:     }
 231: 
 232:     /**
 233:      * Returns the label font.
 234:      *
 235:      * @return The label font.
 236:      * 
 237:      * @see #setLabelFont(Font)
 238:      */
 239:     public Font getLabelFont() {
 240:         // FIXME: this attribute is not used - deprecate?
 241:         return this.labelFont;
 242:     }
 243: 
 244:     /**
 245:      * Sets the label font and sends a {@link PlotChangeEvent} to all 
 246:      * registered listeners.
 247:      *
 248:      * @param font  the new label font.
 249:      * 
 250:      * @see #getLabelFont()
 251:      */
 252:     public void setLabelFont(Font font) {
 253:         // FIXME: this attribute is not used - deprecate?
 254:         if (font == null) {
 255:             throw new IllegalArgumentException("Null 'font' not allowed.");
 256:         }
 257:         this.labelFont = font;
 258:         notifyListeners(new PlotChangeEvent(this));
 259:     }
 260: 
 261:     /**
 262:      * Returns the paint used to fill the outer circle of the compass.
 263:      * 
 264:      * @return The paint (never <code>null</code>).
 265:      * 
 266:      * @see #setRosePaint(Paint)
 267:      */
 268:     public Paint getRosePaint() {
 269:         return this.rosePaint;   
 270:     }
 271:     
 272:     /**
 273:      * Sets the paint used to fill the outer circle of the compass, 
 274:      * and sends a {@link PlotChangeEvent} to all registered listeners.
 275:      * 
 276:      * @param paint  the paint (<code>null</code> not permitted).
 277:      * 
 278:      * @see #getRosePaint()
 279:      */
 280:     public void setRosePaint(Paint paint) {
 281:         if (paint == null) {   
 282:             throw new IllegalArgumentException("Null 'paint' argument.");
 283:         }
 284:         this.rosePaint = paint;
 285:         notifyListeners(new PlotChangeEvent(this));        
 286:     }
 287: 
 288:     /**
 289:      * Returns the paint used to fill the inner background area of the 
 290:      * compass.
 291:      * 
 292:      * @return The paint (never <code>null</code>).
 293:      * 
 294:      * @see #setRoseCenterPaint(Paint)
 295:      */
 296:     public Paint getRoseCenterPaint() {
 297:         return this.roseCenterPaint;   
 298:     }
 299:     
 300:     /**
 301:      * Sets the paint used to fill the inner background area of the compass, 
 302:      * and sends a {@link PlotChangeEvent} to all registered listeners.
 303:      * 
 304:      * @param paint  the paint (<code>null</code> not permitted).
 305:      * 
 306:      * @see #getRoseCenterPaint()
 307:      */
 308:     public void setRoseCenterPaint(Paint paint) {
 309:         if (paint == null) {   
 310:             throw new IllegalArgumentException("Null 'paint' argument.");
 311:         }
 312:         this.roseCenterPaint = paint;
 313:         notifyListeners(new PlotChangeEvent(this));        
 314:     }
 315:     
 316:     /**
 317:      * Returns the paint used to draw the circles, symbols and labels on the
 318:      * compass.
 319:      * 
 320:      * @return The paint (never <code>null</code>).
 321:      * 
 322:      * @see #setRoseHighlightPaint(Paint)
 323:      */
 324:     public Paint getRoseHighlightPaint() {
 325:         return this.roseHighlightPaint;   
 326:     }
 327:     
 328:     /**
 329:      * Sets the paint used to draw the circles, symbols and labels of the 
 330:      * compass, and sends a {@link PlotChangeEvent} to all registered listeners.
 331:      * 
 332:      * @param paint  the paint (<code>null</code> not permitted).
 333:      * 
 334:      * @see #getRoseHighlightPaint()
 335:      */
 336:     public void setRoseHighlightPaint(Paint paint) {
 337:         if (paint == null) {   
 338:             throw new IllegalArgumentException("Null 'paint' argument.");
 339:         }
 340:         this.roseHighlightPaint = paint;
 341:         notifyListeners(new PlotChangeEvent(this));        
 342:     }
 343:     
 344:     /**
 345:      * Returns a flag that controls whether or not a border is drawn.
 346:      *
 347:      * @return The flag.
 348:      * 
 349:      * @see #setDrawBorder(boolean)
 350:      */
 351:     public boolean getDrawBorder() {
 352:         return this.drawBorder;
 353:     }
 354: 
 355:     /**
 356:      * Sets a flag that controls whether or not a border is drawn.
 357:      *
 358:      * @param status  the flag status.
 359:      * 
 360:      * @see #getDrawBorder()
 361:      */
 362:     public void setDrawBorder(boolean status) {
 363:         this.drawBorder = status;
 364:         notifyListeners(new PlotChangeEvent(this));
 365:     }
 366: 
 367:     /**
 368:      * Sets the series paint.
 369:      *
 370:      * @param series  the series index.
 371:      * @param paint  the paint.
 372:      * 
 373:      * @see #setSeriesOutlinePaint(int, Paint)
 374:      */
 375:     public void setSeriesPaint(int series, Paint paint) {
 376:        // super.setSeriesPaint(series, paint);
 377:         if ((series >= 0) && (series < this.seriesNeedle.length)) {
 378:             this.seriesNeedle[series].setFillPaint(paint);
 379:         }
 380:     }
 381: 
 382:     /**
 383:      * Sets the series outline paint.
 384:      *
 385:      * @param series  the series index.
 386:      * @param p  the paint.
 387:      * 
 388:      * @see #setSeriesPaint(int, Paint)
 389:      */
 390:     public void setSeriesOutlinePaint(int series, Paint p) {
 391: 
 392:         if ((series >= 0) && (series < this.seriesNeedle.length)) {
 393:             this.seriesNeedle[series].setOutlinePaint(p);
 394:         }
 395: 
 396:     }
 397: 
 398:     /**
 399:      * Sets the series outline stroke.
 400:      *
 401:      * @param series  the series index.
 402:      * @param stroke  the stroke.
 403:      * 
 404:      * @see #setSeriesOutlinePaint(int, Paint)
 405:      */
 406:     public void setSeriesOutlineStroke(int series, Stroke stroke) {
 407: 
 408:         if ((series >= 0) && (series < this.seriesNeedle.length)) {
 409:             this.seriesNeedle[series].setOutlineStroke(stroke);
 410:         }
 411: 
 412:     }
 413: 
 414:     /**
 415:      * Sets the needle type.
 416:      *
 417:      * @param type  the type.
 418:      * 
 419:      * @see #setSeriesNeedle(int, int)
 420:      */
 421:     public void setSeriesNeedle(int type) {
 422:         setSeriesNeedle(0, type);
 423:     }
 424: 
 425:     /**
 426:      * Sets the needle for a series.  The needle type is one of the following:
 427:      * <ul>
 428:      * <li>0 = {@link ArrowNeedle};</li>
 429:      * <li>1 = {@link LineNeedle};</li>
 430:      * <li>2 = {@link LongNeedle};</li>
 431:      * <li>3 = {@link PinNeedle};</li>
 432:      * <li>4 = {@link PlumNeedle};</li>
 433:      * <li>5 = {@link PointerNeedle};</li>
 434:      * <li>6 = {@link ShipNeedle};</li>
 435:      * <li>7 = {@link WindNeedle};</li>
 436:      * <li>8 = {@link ArrowNeedle};</li>
 437:      * <li>9 = {@link MiddlePinNeedle};</li>
 438:      * </ul>
 439:      * @param index  the series index.
 440:      * @param type  the needle type.
 441:      * 
 442:      * @see #setSeriesNeedle(int)
 443:      */
 444:     public void setSeriesNeedle(int index, int type) {
 445:         switch (type) {
 446:             case 0:
 447:                 setSeriesNeedle(index, new ArrowNeedle(true));
 448:                 setSeriesPaint(index, Color.red);
 449:                 this.seriesNeedle[index].setHighlightPaint(Color.white);
 450:                 break;
 451:             case 1:
 452:                 setSeriesNeedle(index, new LineNeedle());
 453:                 break;
 454:             case 2:
 455:                 MeterNeedle longNeedle = new LongNeedle();
 456:                 longNeedle.setRotateY(0.5);
 457:                 setSeriesNeedle(index, longNeedle);
 458:                 break;
 459:             case 3:
 460:                 setSeriesNeedle(index, new PinNeedle());
 461:                 break;
 462:             case 4:
 463:                 setSeriesNeedle(index, new PlumNeedle());
 464:                 break;
 465:             case 5:
 466:                 setSeriesNeedle(index, new PointerNeedle());
 467:                 break;
 468:             case 6:
 469:                 setSeriesPaint(index, null);
 470:                 setSeriesOutlineStroke(index, new BasicStroke(3));
 471:                 setSeriesNeedle(index, new ShipNeedle());
 472:                 break;
 473:             case 7:
 474:                 setSeriesPaint(index, Color.blue);
 475:                 setSeriesNeedle(index, new WindNeedle());
 476:                 break;
 477:             case 8:
 478:                 setSeriesNeedle(index, new ArrowNeedle(true));
 479:                 break;
 480:             case 9:
 481:                 setSeriesNeedle(index, new MiddlePinNeedle());
 482:                 break;
 483: 
 484:             default:
 485:                 throw new IllegalArgumentException("Unrecognised type.");
 486:         }
 487: 
 488:     }
 489: 
 490:     /**
 491:      * Sets the needle for a series and sends a {@link PlotChangeEvent} to all
 492:      * registered listeners.
 493:      *
 494:      * @param index  the series index.
 495:      * @param needle  the needle.
 496:      */
 497:     public void setSeriesNeedle(int index, MeterNeedle needle) {
 498: 
 499:         if ((needle != null) && (index < this.seriesNeedle.length)) {
 500:             this.seriesNeedle[index] = needle;
 501:         }
 502:         notifyListeners(new PlotChangeEvent(this));
 503: 
 504:     }
 505: 
 506:     /**
 507:      * Returns an array of dataset references for the plot.
 508:      *
 509:      * @return The dataset for the plot, cast as a ValueDataset.
 510:      * 
 511:      * @see #addDataset(ValueDataset)
 512:      */
 513:     public ValueDataset[] getDatasets() {
 514:         return this.datasets;
 515:     }
 516: 
 517:     /**
 518:      * Adds a dataset to the compass.
 519:      *
 520:      * @param dataset  the new dataset (<code>null</code> ignored).
 521:      * 
 522:      * @see #addDataset(ValueDataset, MeterNeedle)
 523:      */
 524:     public void addDataset(ValueDataset dataset) {
 525:         addDataset(dataset, null);
 526:     }
 527: 
 528:     /**
 529:      * Adds a dataset to the compass.
 530:      *
 531:      * @param dataset  the new dataset (<code>null</code> ignored).
 532:      * @param needle  the needle (<code>null</code> permitted).
 533:      */
 534:     public void addDataset(ValueDataset dataset, MeterNeedle needle) {
 535: 
 536:         if (dataset != null) {
 537:             int i = this.datasets.length + 1;
 538:             ValueDataset[] t = new ValueDataset[i];
 539:             MeterNeedle[] p = new MeterNeedle[i];
 540:             i = i - 2;
 541:             for (; i >= 0; --i) {
 542:                 t[i] = this.datasets[i];
 543:                 p[i] = this.seriesNeedle[i];
 544:             }
 545:             i = this.datasets.length;
 546:             t[i] = dataset;
 547:             p[i] = ((needle != null) ? needle : p[i - 1]);
 548: 
 549:             ValueDataset[] a = this.datasets;
 550:             MeterNeedle[] b = this.seriesNeedle;
 551:             this.datasets = t;
 552:             this.seriesNeedle = p;
 553: 
 554:             for (--i; i >= 0; --i) {
 555:                 a[i] = null;
 556:                 b[i] = null;
 557:             }
 558:             dataset.addChangeListener(this);
 559:         }
 560:     }
 561: 
 562:     /**
 563:      * Draws the plot on a Java 2D graphics device (such as the screen or a 
 564:      * printer).
 565:      *
 566:      * @param g2  the graphics device.
 567:      * @param area  the area within which the plot should be drawn.
 568:      * @param anchor  the anchor point (<code>null</code> permitted).
 569:      * @param parentState  the state from the parent plot, if there is one.
 570:      * @param info  collects info about the drawing.
 571:      */
 572:     public void draw(Graphics2D g2, Rectangle2D area, Point2D anchor,
 573:                      PlotState parentState,
 574:                      PlotRenderingInfo info) {
 575: 
 576:         int outerRadius = 0;
 577:         int innerRadius = 0;
 578:         int x1, y1, x2, y2;
 579:         double a;
 580: 
 581:         if (info != null) {
 582:             info.setPlotArea(area);
 583:         }
 584: 
 585:         // adjust for insets...
 586:         RectangleInsets insets = getInsets();
 587:         insets.trim(area);
 588: 
 589:         // draw the background
 590:         if (this.drawBorder) {
 591:             drawBackground(g2, area);
 592:         }
 593: 
 594:         int midX = (int) (area.getWidth() / 2);
 595:         int midY = (int) (area.getHeight() / 2);
 596:         int radius = midX;
 597:         if (midY < midX) {
 598:             radius = midY;
 599:         }
 600:         --radius;
 601:         int diameter = 2 * radius;
 602: 
 603:         midX += (int) area.getMinX();
 604:         midY += (int) area.getMinY();
 605: 
 606:         this.circle1.setFrame(midX - radius, midY - radius, diameter, diameter);
 607:         this.circle2.setFrame(
 608:             midX - radius + 15, midY - radius + 15, 
 609:             diameter - 30, diameter - 30
 610:         );
 611:         g2.setPaint(this.rosePaint);
 612:         this.a1 = new Area(this.circle1);
 613:         this.a2 = new Area(this.circle2);
 614:         this.a1.subtract(this.a2);
 615:         g2.fill(this.a1);
 616: 
 617:         g2.setPaint(this.roseCenterPaint);
 618:         x1 = diameter - 30;
 619:         g2.fillOval(midX - radius + 15, midY - radius + 15, x1, x1);
 620:         g2.setPaint(this.roseHighlightPaint);
 621:         g2.drawOval(midX - radius, midY - radius, diameter, diameter);
 622:         x1 = diameter - 20;
 623:         g2.drawOval(midX - radius + 10, midY - radius + 10, x1, x1);
 624:         x1 = diameter - 30;
 625:         g2.drawOval(midX - radius + 15, midY - radius + 15, x1, x1);
 626:         x1 = diameter - 80;
 627:         g2.drawOval(midX - radius + 40, midY - radius + 40, x1, x1);
 628: 
 629:         outerRadius = radius - 20;
 630:         innerRadius = radius - 32;
 631:         for (int w = 0; w < 360; w += 15) {
 632:             a = Math.toRadians(w);
 633:             x1 = midX - ((int) (Math.sin(a) * innerRadius));
 634:             x2 = midX - ((int) (Math.sin(a) * outerRadius));
 635:             y1 = midY - ((int) (Math.cos(a) * innerRadius));
 636:             y2 = midY - ((int) (Math.cos(a) * outerRadius));
 637:             g2.drawLine(x1, y1, x2, y2);
 638:         }
 639: 
 640:         g2.setPaint(this.roseHighlightPaint);
 641:         innerRadius = radius - 26;
 642:         outerRadius = 7;
 643:         for (int w = 45; w < 360; w += 90) {
 644:             a = Math.toRadians(w);
 645:             x1 = midX - ((int) (Math.sin(a) * innerRadius));
 646:             y1 = midY - ((int) (Math.cos(a) * innerRadius));
 647:             g2.fillOval(x1 - outerRadius, y1 - outerRadius, 2 * outerRadius, 
 648:                     2 * outerRadius);
 649:         }
 650: 
 651:         /// Squares
 652:         for (int w = 0; w < 360; w += 90) {
 653:             a = Math.toRadians(w);
 654:             x1 = midX - ((int) (Math.sin(a) * innerRadius));
 655:             y1 = midY - ((int) (Math.cos(a) * innerRadius));
 656: 
 657:             Polygon p = new Polygon();
 658:             p.addPoint(x1 - outerRadius, y1);
 659:             p.addPoint(x1, y1 + outerRadius);
 660:             p.addPoint(x1 + outerRadius, y1);
 661:             p.addPoint(x1, y1 - outerRadius);
 662:             g2.fillPolygon(p);
 663:         }
 664: 
 665:         /// Draw N, S, E, W
 666:         innerRadius = radius - 42;
 667:         Font f = getCompassFont(radius);
 668:         g2.setFont(f);
 669:         g2.drawString("N", midX - 5, midY - innerRadius + f.getSize());
 670:         g2.drawString("S", midX - 5, midY + innerRadius - 5);
 671:         g2.drawString("W", midX - innerRadius + 5, midY + 5);
 672:         g2.drawString("E", midX + innerRadius - f.getSize(), midY + 5);
 673: 
 674:         // plot the data (unless the dataset is null)...
 675:         y1 = radius / 2;
 676:         x1 = radius / 6;
 677:         Rectangle2D needleArea = new Rectangle2D.Double(
 678:             (midX - x1), (midY - y1), (2 * x1), (2 * y1)
 679:         );
 680:         int x = this.seriesNeedle.length;
 681:         int current = 0;
 682:         double value = 0;
 683:         int i = (this.datasets.length - 1);
 684:         for (; i >= 0; --i) {
 685:             ValueDataset data = this.datasets[i];
 686: 
 687:             if (data != null && data.getValue() != null) {
 688:                 value = (data.getValue().doubleValue()) 
 689:                     % this.revolutionDistance;
 690:                 value = value / this.revolutionDistance * 360;
 691:                 current = i % x;
 692:                 this.seriesNeedle[current].draw(g2, needleArea, value);
 693:             }
 694:         }
 695: 
 696:         if (this.drawBorder) {
 697:             drawOutline(g2, area);
 698:         }
 699: 
 700:     }
 701: 
 702:     /**
 703:      * Returns a short string describing the type of plot.
 704:      *
 705:      * @return A string describing the plot.
 706:      */
 707:     public String getPlotType() {
 708:         return localizationResources.getString("Compass_Plot");
 709:     }
 710: 
 711:     /**
 712:      * Returns the legend items for the plot.  For now, no legend is available 
 713:      * - this method returns null.
 714:      *
 715:      * @return The legend items.
 716:      */
 717:     public LegendItemCollection getLegendItems() {
 718:         return null;
 719:     }
 720: 
 721:     /**
 722:      * No zooming is implemented for compass plot, so this method is empty.
 723:      *
 724:      * @param percent  the zoom amount.
 725:      */
 726:     public void zoom(double percent) {
 727:         // no zooming possible
 728:     }
 729: 
 730:     /**
 731:      * Returns the font for the compass, adjusted for the size of the plot.
 732:      *
 733:      * @param radius the radius.
 734:      *
 735:      * @return The font.
 736:      */
 737:     protected Font getCompassFont(int radius) {
 738:         float fontSize = radius / 10.0f;
 739:         if (fontSize < 8) {
 740:             fontSize = 8;
 741:         }
 742:         Font newFont = this.compassFont.deriveFont(fontSize);
 743:         return newFont;
 744:     }
 745: 
 746:     /**
 747:      * Tests an object for equality with this plot.
 748:      *
 749:      * @param obj  the object (<code>null</code> permitted).
 750:      *
 751:      * @return A boolean.
 752:      */
 753:     public boolean equals(Object obj) {
 754:         if (obj == this) {
 755:             return true;
 756:         }
 757:         if (!(obj instanceof CompassPlot)) {
 758:             return false;
 759:         }
 760:         if (!super.equals(obj)) {
 761:             return false;
 762:         }
 763:         CompassPlot that = (CompassPlot) obj;
 764:         if (this.labelType != that.labelType) {
 765:             return false;
 766:         }
 767:         if (!ObjectUtilities.equal(this.labelFont, that.labelFont)) {
 768:             return false;
 769:         }
 770:         if (this.drawBorder != that.drawBorder) {
 771:             return false;
 772:         }
 773:         if (!PaintUtilities.equal(this.roseHighlightPaint, 
 774:                 that.roseHighlightPaint)) {
 775:             return false;
 776:         }
 777:         if (!PaintUtilities.equal(this.rosePaint, that.rosePaint)) {
 778:             return false;
 779:         }
 780:         if (!PaintUtilities.equal(this.roseCenterPaint, 
 781:                 that.roseCenterPaint)) {
 782:             return false;
 783:         }
 784:         if (!ObjectUtilities.equal(this.compassFont, that.compassFont)) {
 785:             return false;
 786:         }
 787:         if (!Arrays.equals(this.seriesNeedle, that.seriesNeedle)) {
 788:             return false;
 789:         }
 790:         if (getRevolutionDistance() != that.getRevolutionDistance()) {
 791:             return false;
 792:         }
 793:         return true;
 794: 
 795:     }
 796: 
 797:     /**
 798:      * Returns a clone of the plot.
 799:      *
 800:      * @return A clone.
 801:      *
 802:      * @throws CloneNotSupportedException  this class will not throw this 
 803:      *         exception, but subclasses (if any) might.
 804:      */
 805:     public Object clone() throws CloneNotSupportedException {
 806: 
 807:         CompassPlot clone = (CompassPlot) super.clone();
 808:         if (this.circle1 != null) {
 809:             clone.circle1 = (Ellipse2D) this.circle1.clone();
 810:         }
 811:         if (this.circle2 != null) {
 812:             clone.circle2 = (Ellipse2D) this.circle2.clone();
 813:         }
 814:         if (this.a1 != null) {
 815:             clone.a1 = (Area) this.a1.clone();
 816:         }
 817:         if (this.a2 != null) {
 818:             clone.a2 = (Area) this.a2.clone();
 819:         }
 820:         if (this.rect1 != null) {
 821:             clone.rect1 = (Rectangle2D) this.rect1.clone();            
 822:         }
 823:         clone.datasets = (ValueDataset[]) this.datasets.clone();
 824:         clone.seriesNeedle = (MeterNeedle[]) this.seriesNeedle.clone();
 825: 
 826:         // clone share data sets => add the clone as listener to the dataset
 827:         for (int i = 0; i < this.datasets.length; ++i) {
 828:             if (clone.datasets[i] != null) {
 829:                 clone.datasets[i].addChangeListener(clone);
 830:             }
 831:         }
 832:         return clone;
 833: 
 834:     }
 835: 
 836:     /**
 837:      * Sets the count to complete one revolution.  Can be arbitrarily set
 838:      * For degrees (the default) it is 360, for radians this is 2*Pi, etc
 839:      *
 840:      * @param size the count to complete one revolution.
 841:      * 
 842:      * @see #getRevolutionDistance()
 843:      */
 844:     public void setRevolutionDistance(double size) {
 845:         if (size > 0) {
 846:             this.revolutionDistance = size;
 847:         }
 848:     }
 849: 
 850:     /**
 851:      * Gets the count to complete one revolution.
 852:      *
 853:      * @return The count to complete one revolution.
 854:      * 
 855:      * @see #setRevolutionDistance(double)
 856:      */
 857:     public double getRevolutionDistance() {
 858:         return this.revolutionDistance;
 859:     }
 860:     
 861:     /**
 862:      * Provides serialization support.
 863:      *
 864:      * @param stream  the output stream.
 865:      *
 866:      * @throws IOException  if there is an I/O error.
 867:      */
 868:     private void writeObject(ObjectOutputStream stream) throws IOException {
 869:         stream.defaultWriteObject();
 870:         SerialUtilities.writePaint(this.rosePaint, stream);
 871:         SerialUtilities.writePaint(this.roseCenterPaint, stream);
 872:         SerialUtilities.writePaint(this.roseHighlightPaint, stream);
 873:     }
 874: 
 875:     /**
 876:      * Provides serialization support.
 877:      *
 878:      * @param stream  the input stream.
 879:      *
 880:      * @throws IOException  if there is an I/O error.
 881:      * @throws ClassNotFoundException  if there is a classpath problem.
 882:      */
 883:     private void readObject(ObjectInputStream stream) 
 884:         throws IOException, ClassNotFoundException {
 885:         stream.defaultReadObject();
 886:         this.rosePaint = SerialUtilities.readPaint(stream);
 887:         this.roseCenterPaint = SerialUtilities.readPaint(stream);
 888:         this.roseHighlightPaint = SerialUtilities.readPaint(stream);
 889:     }
 890: 
 891: }