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