Source for org.jfree.chart.plot.PolarPlot

   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:  * PolarPlot.java
  29:  * --------------
  30:  * (C) Copyright 2004-2007, by Solution Engineering, Inc. and Contributors.
  31:  *
  32:  * Original Author:  Daniel Bridenbecker, Solution Engineering, Inc.;
  33:  * Contributor(s):   David Gilbert (for Object Refinery Limited);
  34:  *
  35:  * Changes
  36:  * -------
  37:  * 19-Jan-2004 : Version 1, contributed by DB with minor changes by DG (DG);
  38:  * 07-Apr-2004 : Changed text bounds calculation (DG);
  39:  * 05-May-2005 : Updated draw() method parameters (DG);
  40:  * 09-Jun-2005 : Fixed getDataRange() and equals() methods (DG);
  41:  * 25-Oct-2005 : Implemented Zoomable (DG);
  42:  * ------------- JFREECHART 1.0.x ---------------------------------------------
  43:  * 07-Feb-2007 : Fixed bug 1599761, data value less than axis minimum (DG);
  44:  * 21-Mar-2007 : Fixed serialization bug (DG);
  45:  * 24-Sep-2007 : Implemented new zooming methods (DG);
  46:  *
  47:  */
  48: 
  49: package org.jfree.chart.plot;
  50: 
  51: 
  52: import java.awt.AlphaComposite;
  53: import java.awt.BasicStroke;
  54: import java.awt.Color;
  55: import java.awt.Composite;
  56: import java.awt.Font;
  57: import java.awt.FontMetrics;
  58: import java.awt.Graphics2D;
  59: import java.awt.Paint;
  60: import java.awt.Point;
  61: import java.awt.Shape;
  62: import java.awt.Stroke;
  63: import java.awt.geom.Point2D;
  64: import java.awt.geom.Rectangle2D;
  65: import java.io.IOException;
  66: import java.io.ObjectInputStream;
  67: import java.io.ObjectOutputStream;
  68: import java.io.Serializable;
  69: import java.util.ArrayList;
  70: import java.util.Iterator;
  71: import java.util.List;
  72: import java.util.ResourceBundle;
  73: 
  74: import org.jfree.chart.LegendItem;
  75: import org.jfree.chart.LegendItemCollection;
  76: import org.jfree.chart.axis.AxisState;
  77: import org.jfree.chart.axis.NumberTick;
  78: import org.jfree.chart.axis.ValueAxis;
  79: import org.jfree.chart.event.PlotChangeEvent;
  80: import org.jfree.chart.event.RendererChangeEvent;
  81: import org.jfree.chart.event.RendererChangeListener;
  82: import org.jfree.chart.renderer.PolarItemRenderer;
  83: import org.jfree.data.Range;
  84: import org.jfree.data.general.DatasetChangeEvent;
  85: import org.jfree.data.general.DatasetUtilities;
  86: import org.jfree.data.xy.XYDataset;
  87: import org.jfree.io.SerialUtilities;
  88: import org.jfree.text.TextUtilities;
  89: import org.jfree.ui.RectangleEdge;
  90: import org.jfree.ui.RectangleInsets;
  91: import org.jfree.ui.TextAnchor;
  92: import org.jfree.util.ObjectUtilities;
  93: import org.jfree.util.PaintUtilities;
  94: 
  95: 
  96: /**
  97:  * Plots data that is in (theta, radius) pairs where
  98:  * theta equal to zero is due north and increases clockwise.
  99:  */
 100: public class PolarPlot extends Plot implements ValueAxisPlot, Zoomable,
 101:         RendererChangeListener, Cloneable, Serializable {
 102:    
 103:     /** For serialization. */
 104:     private static final long serialVersionUID = 3794383185924179525L;
 105:     
 106:     /** The default margin. */
 107:     private static final int MARGIN = 20;
 108:    
 109:     /** The annotation margin. */
 110:     private static final double ANNOTATION_MARGIN = 7.0;
 111:    
 112:     /** The default grid line stroke. */
 113:     public static final Stroke DEFAULT_GRIDLINE_STROKE = new BasicStroke(
 114:             0.5f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 
 115:             0.0f, new float[]{2.0f, 2.0f}, 0.0f);
 116:    
 117:     /** The default grid line paint. */
 118:     public static final Paint DEFAULT_GRIDLINE_PAINT = Color.gray;
 119:    
 120:     /** The resourceBundle for the localization. */
 121:     protected static ResourceBundle localizationResources 
 122:         = ResourceBundle.getBundle("org.jfree.chart.plot.LocalizationBundle");
 123:    
 124:     /** The angles that are marked with gridlines. */
 125:     private List angleTicks;
 126:     
 127:     /** The axis (used for the y-values). */
 128:     private ValueAxis axis;
 129:     
 130:     /** The dataset. */
 131:     private XYDataset dataset;
 132:    
 133:     /** 
 134:      * Object responsible for drawing the visual representation of each point 
 135:      * on the plot. 
 136:      */
 137:     private PolarItemRenderer renderer;
 138:    
 139:     /** A flag that controls whether or not the angle labels are visible. */
 140:     private boolean angleLabelsVisible = true;
 141:     
 142:     /** The font used to display the angle labels - never null. */
 143:     private Font angleLabelFont = new Font("SansSerif", Font.PLAIN, 12);
 144:     
 145:     /** The paint used to display the angle labels. */
 146:     private transient Paint angleLabelPaint = Color.black;
 147:     
 148:     /** A flag that controls whether the angular grid-lines are visible. */
 149:     private boolean angleGridlinesVisible;
 150:    
 151:     /** The stroke used to draw the angular grid-lines. */
 152:     private transient Stroke angleGridlineStroke;
 153:    
 154:     /** The paint used to draw the angular grid-lines. */
 155:     private transient Paint angleGridlinePaint;
 156:    
 157:     /** A flag that controls whether the radius grid-lines are visible. */
 158:     private boolean radiusGridlinesVisible;
 159:    
 160:     /** The stroke used to draw the radius grid-lines. */
 161:     private transient Stroke radiusGridlineStroke;
 162:    
 163:     /** The paint used to draw the radius grid-lines. */
 164:     private transient Paint radiusGridlinePaint;
 165:    
 166:     /** The annotations for the plot. */
 167:     private List cornerTextItems = new ArrayList();
 168:    
 169:     /**
 170:      * Default constructor.
 171:      */
 172:     public PolarPlot() {
 173:         this(null, null, null);
 174:     }
 175:    
 176:    /**
 177:      * Creates a new plot.
 178:      *
 179:      * @param dataset  the dataset (<code>null</code> permitted).
 180:      * @param radiusAxis  the radius axis (<code>null</code> permitted).
 181:      * @param renderer  the renderer (<code>null</code> permitted).
 182:      */
 183:     public PolarPlot(XYDataset dataset, 
 184:                      ValueAxis radiusAxis,
 185:                      PolarItemRenderer renderer) {
 186:       
 187:         super();
 188:             
 189:         this.dataset = dataset;
 190:         if (this.dataset != null) {
 191:             this.dataset.addChangeListener(this);
 192:         }
 193:       
 194:         this.angleTicks = new java.util.ArrayList();
 195:         this.angleTicks.add(new NumberTick(new Double(0.0), "0", 
 196:                 TextAnchor.CENTER, TextAnchor.CENTER, 0.0));
 197:         this.angleTicks.add(new NumberTick(new Double(45.0), "45", 
 198:                 TextAnchor.CENTER, TextAnchor.CENTER, 0.0));
 199:         this.angleTicks.add(new NumberTick(new Double(90.0), "90", 
 200:                 TextAnchor.CENTER, TextAnchor.CENTER, 0.0));
 201:         this.angleTicks.add(new NumberTick(new Double(135.0), "135", 
 202:                 TextAnchor.CENTER, TextAnchor.CENTER, 0.0));
 203:         this.angleTicks.add(new NumberTick(new Double(180.0), "180", 
 204:                 TextAnchor.CENTER, TextAnchor.CENTER, 0.0));
 205:         this.angleTicks.add(new NumberTick(new Double(225.0), "225", 
 206:                 TextAnchor.CENTER, TextAnchor.CENTER, 0.0));
 207:         this.angleTicks.add(new NumberTick(new Double(270.0), "270", 
 208:                 TextAnchor.CENTER, TextAnchor.CENTER, 0.0));
 209:         this.angleTicks.add(new NumberTick(new Double(315.0), "315", 
 210:                 TextAnchor.CENTER, TextAnchor.CENTER, 0.0));
 211:         
 212:         this.axis = radiusAxis;
 213:         if (this.axis != null) {
 214:             this.axis.setPlot(this);
 215:             this.axis.addChangeListener(this);
 216:         }
 217:       
 218:         this.renderer = renderer;
 219:         if (this.renderer != null) {
 220:             this.renderer.setPlot(this);
 221:             this.renderer.addChangeListener(this);
 222:         }
 223:       
 224:         this.angleGridlinesVisible = true;
 225:         this.angleGridlineStroke = DEFAULT_GRIDLINE_STROKE;
 226:         this.angleGridlinePaint = DEFAULT_GRIDLINE_PAINT;
 227:       
 228:         this.radiusGridlinesVisible = true;
 229:         this.radiusGridlineStroke = DEFAULT_GRIDLINE_STROKE;
 230:         this.radiusGridlinePaint = DEFAULT_GRIDLINE_PAINT;      
 231:     }
 232:    
 233:     /**
 234:      * Add text to be displayed in the lower right hand corner and sends a 
 235:      * {@link PlotChangeEvent} to all registered listeners.
 236:      * 
 237:      * @param text  the text to display (<code>null</code> not permitted).
 238:      * 
 239:      * @see #removeCornerTextItem(String)
 240:      */
 241:     public void addCornerTextItem(String text) {
 242:         if (text == null) {
 243:             throw new IllegalArgumentException("Null 'text' argument.");
 244:         }
 245:         this.cornerTextItems.add(text);
 246:         this.notifyListeners(new PlotChangeEvent(this));
 247:     }
 248:    
 249:     /**
 250:      * Remove the given text from the list of corner text items and
 251:      * sends a {@link PlotChangeEvent} to all registered listeners.
 252:      * 
 253:      * @param text  the text to remove (<code>null</code> ignored).
 254:      * 
 255:      * @see #addCornerTextItem(String)
 256:      */
 257:     public void removeCornerTextItem(String text) {
 258:         boolean removed = this.cornerTextItems.remove(text);
 259:         if (removed) {
 260:             this.notifyListeners(new PlotChangeEvent(this));        
 261:         }
 262:     }
 263:    
 264:     /**
 265:      * Clear the list of corner text items and sends a {@link PlotChangeEvent}
 266:      * to all registered listeners.
 267:      * 
 268:      * @see #addCornerTextItem(String)
 269:      * @see #removeCornerTextItem(String)
 270:      */
 271:     public void clearCornerTextItems() {
 272:         if (this.cornerTextItems.size() > 0) {
 273:             this.cornerTextItems.clear();
 274:             this.notifyListeners(new PlotChangeEvent(this));        
 275:         }
 276:     }
 277:    
 278:     /**
 279:      * Returns the plot type as a string.
 280:      *
 281:      * @return A short string describing the type of plot.
 282:      */
 283:     public String getPlotType() {
 284:        return PolarPlot.localizationResources.getString("Polar_Plot");
 285:     }
 286:     
 287:     /**
 288:      * Returns the axis for the plot.
 289:      *
 290:      * @return The radius axis (possibly <code>null</code>).
 291:      * 
 292:      * @see #setAxis(ValueAxis)
 293:      */
 294:     public ValueAxis getAxis() {
 295:         return this.axis;
 296:     }
 297:    
 298:     /**
 299:      * Sets the axis for the plot and sends a {@link PlotChangeEvent} to all
 300:      * registered listeners.
 301:      *
 302:      * @param axis  the new axis (<code>null</code> permitted).
 303:      */
 304:     public void setAxis(ValueAxis axis) {
 305:         if (axis != null) {
 306:             axis.setPlot(this);
 307:         }
 308:        
 309:         // plot is likely registered as a listener with the existing axis...
 310:         if (this.axis != null) {
 311:             this.axis.removeChangeListener(this);
 312:         }
 313:        
 314:         this.axis = axis;
 315:         if (this.axis != null) {
 316:             this.axis.configure();
 317:             this.axis.addChangeListener(this);
 318:         }
 319:         notifyListeners(new PlotChangeEvent(this));
 320:     }
 321:    
 322:     /**
 323:      * Returns the primary dataset for the plot.
 324:      *
 325:      * @return The primary dataset (possibly <code>null</code>).
 326:      * 
 327:      * @see #setDataset(XYDataset)
 328:      */
 329:     public XYDataset getDataset() {
 330:         return this.dataset;
 331:     }
 332:     
 333:     /**
 334:      * Sets the dataset for the plot, replacing the existing dataset if there 
 335:      * is one.
 336:      *
 337:      * @param dataset  the dataset (<code>null</code> permitted).
 338:      * 
 339:      * @see #getDataset()
 340:      */
 341:     public void setDataset(XYDataset dataset) {
 342:         // if there is an existing dataset, remove the plot from the list of 
 343:         // change listeners...
 344:         XYDataset existing = this.dataset;
 345:         if (existing != null) {
 346:             existing.removeChangeListener(this);
 347:         }
 348:        
 349:         // set the new m_Dataset, and register the chart as a change listener...
 350:         this.dataset = dataset;
 351:         if (this.dataset != null) {
 352:             setDatasetGroup(this.dataset.getGroup());
 353:             this.dataset.addChangeListener(this);
 354:         }
 355:        
 356:         // send a m_Dataset change event to self...
 357:         DatasetChangeEvent event = new DatasetChangeEvent(this, this.dataset);
 358:         datasetChanged(event);
 359:     }
 360:    
 361:     /**
 362:      * Returns the item renderer.
 363:      *
 364:      * @return The renderer (possibly <code>null</code>).
 365:      * 
 366:      * @see #setRenderer(PolarItemRenderer)
 367:      */
 368:     public PolarItemRenderer getRenderer() {
 369:         return this.renderer;
 370:     }
 371:    
 372:     /**
 373:      * Sets the item renderer, and notifies all listeners of a change to the 
 374:      * plot.
 375:      * <P>
 376:      * If the renderer is set to <code>null</code>, no chart will be drawn.
 377:      *
 378:      * @param renderer  the new renderer (<code>null</code> permitted).
 379:      * 
 380:      * @see #getRenderer()
 381:      */
 382:     public void setRenderer(PolarItemRenderer renderer) {
 383:         if (this.renderer != null) {
 384:             this.renderer.removeChangeListener(this);
 385:         }
 386:        
 387:         this.renderer = renderer;
 388:         if (this.renderer != null) {
 389:             this.renderer.setPlot(this);
 390:         }
 391:        
 392:         notifyListeners(new PlotChangeEvent(this));
 393:     }
 394:    
 395:     /**
 396:      * Returns a flag that controls whether or not the angle labels are visible.
 397:      * 
 398:      * @return A boolean.
 399:      * 
 400:      * @see #setAngleLabelsVisible(boolean)
 401:      */
 402:     public boolean isAngleLabelsVisible() {
 403:         return this.angleLabelsVisible;
 404:     }
 405:     
 406:     /**
 407:      * Sets the flag that controls whether or not the angle labels are visible,
 408:      * and sends a {@link PlotChangeEvent} to all registered listeners.
 409:      * 
 410:      * @param visible  the flag.
 411:      * 
 412:      * @see #isAngleLabelsVisible()
 413:      */
 414:     public void setAngleLabelsVisible(boolean visible) {
 415:         if (this.angleLabelsVisible != visible) {
 416:             this.angleLabelsVisible = visible;
 417:             notifyListeners(new PlotChangeEvent(this));
 418:         }
 419:     }
 420:     
 421:     /**
 422:      * Returns the font used to display the angle labels.
 423:      * 
 424:      * @return A font (never <code>null</code>).
 425:      * 
 426:      * @see #setAngleLabelFont(Font)
 427:      */
 428:     public Font getAngleLabelFont() {
 429:         return this.angleLabelFont;
 430:     }
 431:     
 432:     /**
 433:      * Sets the font used to display the angle labels and sends a 
 434:      * {@link PlotChangeEvent} to all registered listeners.
 435:      * 
 436:      * @param font  the font (<code>null</code> not permitted).
 437:      * 
 438:      * @see #getAngleLabelFont()
 439:      */
 440:     public void setAngleLabelFont(Font font) {
 441:         if (font == null) {
 442:             throw new IllegalArgumentException("Null 'font' argument.");   
 443:         }
 444:         this.angleLabelFont = font;
 445:         notifyListeners(new PlotChangeEvent(this));
 446:     }
 447:     
 448:     /**
 449:      * Returns the paint used to display the angle labels.
 450:      * 
 451:      * @return A paint (never <code>null</code>).
 452:      * 
 453:      * @see #setAngleLabelPaint(Paint)
 454:      */
 455:     public Paint getAngleLabelPaint() {
 456:         return this.angleLabelPaint;
 457:     }
 458:     
 459:     /**
 460:      * Sets the paint used to display the angle labels and sends a 
 461:      * {@link PlotChangeEvent} to all registered listeners.
 462:      * 
 463:      * @param paint  the paint (<code>null</code> not permitted).
 464:      */
 465:     public void setAngleLabelPaint(Paint paint) {
 466:         if (paint == null) {
 467:             throw new IllegalArgumentException("Null 'paint' argument.");
 468:         }
 469:         this.angleLabelPaint = paint;
 470:         notifyListeners(new PlotChangeEvent(this));
 471:     }
 472:     
 473:     /**
 474:      * Returns <code>true</code> if the angular gridlines are visible, and 
 475:      * <code>false<code> otherwise.
 476:      *
 477:      * @return <code>true</code> or <code>false</code>.
 478:      * 
 479:      * @see #setAngleGridlinesVisible(boolean)
 480:      */
 481:     public boolean isAngleGridlinesVisible() {
 482:         return this.angleGridlinesVisible;
 483:     }
 484:     
 485:     /**
 486:      * Sets the flag that controls whether or not the angular grid-lines are 
 487:      * visible.
 488:      * <p>
 489:      * If the flag value is changed, a {@link PlotChangeEvent} is sent to all 
 490:      * registered listeners.
 491:      *
 492:      * @param visible  the new value of the flag.
 493:      * 
 494:      * @see #isAngleGridlinesVisible()
 495:      */
 496:     public void setAngleGridlinesVisible(boolean visible) {
 497:         if (this.angleGridlinesVisible != visible) {
 498:             this.angleGridlinesVisible = visible;
 499:             notifyListeners(new PlotChangeEvent(this));
 500:         }
 501:     }
 502:    
 503:     /**
 504:      * Returns the stroke for the grid-lines (if any) plotted against the 
 505:      * angular axis.
 506:      *
 507:      * @return The stroke (possibly <code>null</code>).
 508:      * 
 509:      * @see #setAngleGridlineStroke(Stroke)
 510:      */
 511:     public Stroke getAngleGridlineStroke() {
 512:         return this.angleGridlineStroke;
 513:     }
 514:     
 515:     /**
 516:      * Sets the stroke for the grid lines plotted against the angular axis and
 517:      * sends a {@link PlotChangeEvent} to all registered listeners.
 518:      * <p>
 519:      * If you set this to <code>null</code>, no grid lines will be drawn.
 520:      *
 521:      * @param stroke  the stroke (<code>null</code> permitted).
 522:      * 
 523:      * @see #getAngleGridlineStroke()
 524:      */
 525:     public void setAngleGridlineStroke(Stroke stroke) {
 526:         this.angleGridlineStroke = stroke;
 527:         notifyListeners(new PlotChangeEvent(this));
 528:     }
 529:     
 530:     /**
 531:      * Returns the paint for the grid lines (if any) plotted against the 
 532:      * angular axis.
 533:      *
 534:      * @return The paint (possibly <code>null</code>).
 535:      * 
 536:      * @see #setAngleGridlinePaint(Paint)
 537:      */
 538:     public Paint getAngleGridlinePaint() {
 539:         return this.angleGridlinePaint;
 540:     }
 541:    
 542:     /**
 543:      * Sets the paint for the grid lines plotted against the angular axis.
 544:      * <p>
 545:      * If you set this to <code>null</code>, no grid lines will be drawn.
 546:      *
 547:      * @param paint  the paint (<code>null</code> permitted).
 548:      * 
 549:      * @see #getAngleGridlinePaint()
 550:      */
 551:     public void setAngleGridlinePaint(Paint paint) {
 552:         this.angleGridlinePaint = paint;
 553:         notifyListeners(new PlotChangeEvent(this));
 554:     }
 555:     
 556:     /**
 557:      * Returns <code>true</code> if the radius axis grid is visible, and 
 558:      * <code>false<code> otherwise.
 559:      *
 560:      * @return <code>true</code> or <code>false</code>.
 561:      * 
 562:      * @see #setRadiusGridlinesVisible(boolean)
 563:      */
 564:     public boolean isRadiusGridlinesVisible() {
 565:         return this.radiusGridlinesVisible;
 566:     }
 567:     
 568:     /**
 569:      * Sets the flag that controls whether or not the radius axis grid lines 
 570:      * are visible.
 571:      * <p>
 572:      * If the flag value is changed, a {@link PlotChangeEvent} is sent to all 
 573:      * registered listeners.
 574:      *
 575:      * @param visible  the new value of the flag.
 576:      * 
 577:      * @see #isRadiusGridlinesVisible()
 578:      */
 579:     public void setRadiusGridlinesVisible(boolean visible) {
 580:         if (this.radiusGridlinesVisible != visible) {
 581:             this.radiusGridlinesVisible = visible;
 582:             notifyListeners(new PlotChangeEvent(this));
 583:         }
 584:     }
 585:    
 586:     /**
 587:      * Returns the stroke for the grid lines (if any) plotted against the 
 588:      * radius axis.
 589:      *
 590:      * @return The stroke (possibly <code>null</code>).
 591:      * 
 592:      * @see #setRadiusGridlineStroke(Stroke)
 593:      */
 594:     public Stroke getRadiusGridlineStroke() {
 595:         return this.radiusGridlineStroke;
 596:     }
 597:     
 598:     /**
 599:      * Sets the stroke for the grid lines plotted against the radius axis and
 600:      * sends a {@link PlotChangeEvent} to all registered listeners.
 601:      * <p>
 602:      * If you set this to <code>null</code>, no grid lines will be drawn.
 603:      *
 604:      * @param stroke  the stroke (<code>null</code> permitted).
 605:      * 
 606:      * @see #getRadiusGridlineStroke()
 607:      */
 608:     public void setRadiusGridlineStroke(Stroke stroke) {
 609:         this.radiusGridlineStroke = stroke;
 610:         notifyListeners(new PlotChangeEvent(this));
 611:     }
 612:     
 613:     /**
 614:      * Returns the paint for the grid lines (if any) plotted against the radius
 615:      * axis.
 616:      *
 617:      * @return The paint (possibly <code>null</code>).
 618:      * 
 619:      * @see #setRadiusGridlinePaint(Paint)
 620:      */
 621:     public Paint getRadiusGridlinePaint() {
 622:         return this.radiusGridlinePaint;
 623:     }
 624:     
 625:     /**
 626:      * Sets the paint for the grid lines plotted against the radius axis and
 627:      * sends a {@link PlotChangeEvent} to all registered listeners.
 628:      * <p>
 629:      * If you set this to <code>null</code>, no grid lines will be drawn.
 630:      *
 631:      * @param paint  the paint (<code>null</code> permitted).
 632:      * 
 633:      * @see #getRadiusGridlinePaint()
 634:      */
 635:     public void setRadiusGridlinePaint(Paint paint) {
 636:         this.radiusGridlinePaint = paint;
 637:         notifyListeners(new PlotChangeEvent(this));
 638:     }
 639:     
 640:     /**
 641:      * Draws the plot on a Java 2D graphics device (such as the screen or a 
 642:      * printer).
 643:      * <P>
 644:      * This plot relies on a {@link PolarItemRenderer} to draw each 
 645:      * item in the plot.  This allows the visual representation of the data to 
 646:      * be changed easily.
 647:      * <P>
 648:      * The optional info argument collects information about the rendering of
 649:      * the plot (dimensions, tooltip information etc).  Just pass in 
 650:      * <code>null</code> if you do not need this information.
 651:      *
 652:      * @param g2  the graphics device.
 653:      * @param area  the area within which the plot (including axes and 
 654:      *              labels) should be drawn.
 655:      * @param anchor  the anchor point (<code>null</code> permitted).
 656:      * @param parentState  ignored.
 657:      * @param info  collects chart drawing information (<code>null</code> 
 658:      *              permitted).
 659:      */
 660:     public void draw(Graphics2D g2, 
 661:                      Rectangle2D area, 
 662:                      Point2D anchor,
 663:                      PlotState parentState,
 664:                      PlotRenderingInfo info) {
 665:        
 666:         // if the plot area is too small, just return...
 667:         boolean b1 = (area.getWidth() <= MINIMUM_WIDTH_TO_DRAW);
 668:         boolean b2 = (area.getHeight() <= MINIMUM_HEIGHT_TO_DRAW);
 669:         if (b1 || b2) {
 670:             return;
 671:         }
 672:        
 673:         // record the plot area...
 674:         if (info != null) {
 675:             info.setPlotArea(area);
 676:         }
 677:        
 678:         // adjust the drawing area for the plot insets (if any)...
 679:         RectangleInsets insets = getInsets();
 680:         insets.trim(area);
 681:       
 682:         Rectangle2D dataArea = area;
 683:         if (info != null) {
 684:             info.setDataArea(dataArea);
 685:         }
 686:        
 687:         // draw the plot background and axes...
 688:         drawBackground(g2, dataArea);
 689:         double h = Math.min(dataArea.getWidth() / 2.0, 
 690:                 dataArea.getHeight() / 2.0) - MARGIN;
 691:         Rectangle2D quadrant = new Rectangle2D.Double(dataArea.getCenterX(), 
 692:                 dataArea.getCenterY(), h, h);
 693:         AxisState state = drawAxis(g2, area, quadrant);
 694:         if (this.renderer != null) {
 695:             Shape originalClip = g2.getClip();
 696:             Composite originalComposite = g2.getComposite();
 697:           
 698:             g2.clip(dataArea);
 699:             g2.setComposite(AlphaComposite.getInstance(
 700:                     AlphaComposite.SRC_OVER, getForegroundAlpha()));
 701:           
 702:             drawGridlines(g2, dataArea, this.angleTicks, state.getTicks());
 703:           
 704:             // draw...
 705:             render(g2, dataArea, info);
 706:           
 707:             g2.setClip(originalClip);
 708:             g2.setComposite(originalComposite);
 709:         }
 710:         drawOutline(g2, dataArea);
 711:         drawCornerTextItems(g2, dataArea);
 712:     }
 713:    
 714:     /**
 715:      * Draws the corner text items.
 716:      * 
 717:      * @param g2  the drawing surface.
 718:      * @param area  the area.
 719:      */
 720:     protected void drawCornerTextItems(Graphics2D g2, Rectangle2D area) {
 721:         if (this.cornerTextItems.isEmpty()) {
 722:             return;
 723:         }
 724:        
 725:         g2.setColor(Color.black);
 726:         double width = 0.0;
 727:         double height = 0.0;
 728:         for (Iterator it = this.cornerTextItems.iterator(); it.hasNext();) {
 729:             String msg = (String) it.next();
 730:             FontMetrics fm = g2.getFontMetrics();
 731:             Rectangle2D bounds = TextUtilities.getTextBounds(msg, g2, fm);
 732:             width = Math.max(width, bounds.getWidth());
 733:             height += bounds.getHeight();
 734:         }
 735:         
 736:         double xadj = ANNOTATION_MARGIN * 2.0;
 737:         double yadj = ANNOTATION_MARGIN;
 738:         width += xadj;
 739:         height += yadj;
 740:        
 741:         double x = area.getMaxX() - width;
 742:         double y = area.getMaxY() - height;
 743:         g2.drawRect((int) x, (int) y, (int) width, (int) height);
 744:         x += ANNOTATION_MARGIN;
 745:         for (Iterator it = this.cornerTextItems.iterator(); it.hasNext();) {
 746:             String msg = (String) it.next();
 747:             Rectangle2D bounds = TextUtilities.getTextBounds(msg, g2, 
 748:                     g2.getFontMetrics());
 749:             y += bounds.getHeight();
 750:             g2.drawString(msg, (int) x, (int) y);
 751:         }
 752:     }
 753:    
 754:     /**
 755:      * A utility method for drawing the axes.
 756:      *
 757:      * @param g2  the graphics device.
 758:      * @param plotArea  the plot area.
 759:      * @param dataArea  the data area.
 760:      * 
 761:      * @return A map containing the axis states.
 762:      */
 763:     protected AxisState drawAxis(Graphics2D g2, Rectangle2D plotArea, 
 764:                                  Rectangle2D dataArea) {
 765:         return this.axis.draw(g2, dataArea.getMinY(), plotArea, dataArea, 
 766:                 RectangleEdge.TOP, null);
 767:     }
 768:    
 769:     /**
 770:      * Draws a representation of the data within the dataArea region, using the
 771:      * current m_Renderer.
 772:      *
 773:      * @param g2  the graphics device.
 774:      * @param dataArea  the region in which the data is to be drawn.
 775:      * @param info  an optional object for collection dimension 
 776:      *              information (<code>null</code> permitted).
 777:      */
 778:     protected void render(Graphics2D g2,
 779:                        Rectangle2D dataArea,
 780:                        PlotRenderingInfo info) {
 781:       
 782:         // now get the data and plot it (the visual representation will depend
 783:         // on the m_Renderer that has been set)...
 784:         if (!DatasetUtilities.isEmptyOrNull(this.dataset)) {
 785:             int seriesCount = this.dataset.getSeriesCount();
 786:             for (int series = 0; series < seriesCount; series++) {
 787:                 this.renderer.drawSeries(g2, dataArea, info, this, 
 788:                         this.dataset, series);
 789:             }
 790:         }
 791:         else {
 792:             drawNoDataMessage(g2, dataArea);
 793:         }
 794:     }
 795:    
 796:     /**
 797:      * Draws the gridlines for the plot, if they are visible.
 798:      *
 799:      * @param g2  the graphics device.
 800:      * @param dataArea  the data area.
 801:      * @param angularTicks  the ticks for the angular axis.
 802:      * @param radialTicks  the ticks for the radial axis.
 803:      */
 804:     protected void drawGridlines(Graphics2D g2, Rectangle2D dataArea, 
 805:                                  List angularTicks, List radialTicks) {
 806: 
 807:         // no renderer, no gridlines...
 808:         if (this.renderer == null) {
 809:             return;
 810:         }
 811:        
 812:         // draw the domain grid lines, if any...
 813:         if (isAngleGridlinesVisible()) {
 814:             Stroke gridStroke = getAngleGridlineStroke();
 815:             Paint gridPaint = getAngleGridlinePaint();
 816:             if ((gridStroke != null) && (gridPaint != null)) {
 817:                 this.renderer.drawAngularGridLines(g2, this, angularTicks, 
 818:                         dataArea);
 819:             }
 820:         }
 821:        
 822:         // draw the radius grid lines, if any...
 823:         if (isRadiusGridlinesVisible()) {
 824:             Stroke gridStroke = getRadiusGridlineStroke();
 825:             Paint gridPaint = getRadiusGridlinePaint();
 826:             if ((gridStroke != null) && (gridPaint != null)) {
 827:                 this.renderer.drawRadialGridLines(g2, this, this.axis, 
 828:                         radialTicks, dataArea);
 829:             }
 830:         }      
 831:     }
 832:    
 833:     /**
 834:      * Zooms the axis ranges by the specified percentage about the anchor point.
 835:      *
 836:      * @param percent  the amount of the zoom.
 837:      */
 838:     public void zoom(double percent) {
 839:         if (percent > 0.0) {
 840:             double radius = getMaxRadius();
 841:             double scaledRadius = radius * percent;
 842:             this.axis.setUpperBound(scaledRadius);
 843:             getAxis().setAutoRange(false);
 844:         } 
 845:         else {
 846:             getAxis().setAutoRange(true);
 847:         }
 848:     }
 849:    
 850:     /**
 851:      * Returns the range for the specified axis.
 852:      *
 853:      * @param axis  the axis.
 854:      *
 855:      * @return The range.
 856:      */
 857:     public Range getDataRange(ValueAxis axis) {
 858:         Range result = null;
 859:         if (this.dataset != null) {
 860:             result = Range.combine(result, 
 861:                     DatasetUtilities.findRangeBounds(this.dataset));
 862:         }
 863:         return result;
 864:     }
 865:    
 866:     /**
 867:      * Receives notification of a change to the plot's m_Dataset.
 868:      * <P>
 869:      * The axis ranges are updated if necessary.
 870:      *
 871:      * @param event  information about the event (not used here).
 872:      */
 873:     public void datasetChanged(DatasetChangeEvent event) {
 874: 
 875:         if (this.axis != null) {
 876:             this.axis.configure();
 877:         }
 878:        
 879:         if (getParent() != null) {
 880:             getParent().datasetChanged(event);
 881:         }
 882:         else {
 883:             super.datasetChanged(event);
 884:         }
 885:     }
 886:    
 887:     /**
 888:      * Notifies all registered listeners of a property change.
 889:      * <P>
 890:      * One source of property change events is the plot's m_Renderer.
 891:      *
 892:      * @param event  information about the property change.
 893:      */
 894:     public void rendererChanged(RendererChangeEvent event) {
 895:         notifyListeners(new PlotChangeEvent(this));
 896:     }
 897:    
 898:     /**
 899:      * Returns the number of series in the dataset for this plot.  If the 
 900:      * dataset is <code>null</code>, the method returns 0.
 901:      *
 902:      * @return The series count.
 903:      */
 904:     public int getSeriesCount() {
 905:         int result = 0;
 906:        
 907:         if (this.dataset != null) {
 908:             result = this.dataset.getSeriesCount();
 909:         }
 910:         return result;
 911:     }
 912:    
 913:     /**
 914:      * Returns the legend items for the plot.  Each legend item is generated by
 915:      * the plot's m_Renderer, since the m_Renderer is responsible for the visual
 916:      * representation of the data.
 917:      *
 918:      * @return The legend items.
 919:      */
 920:     public LegendItemCollection getLegendItems() {
 921:         LegendItemCollection result = new LegendItemCollection();
 922:        
 923:         // get the legend items for the main m_Dataset...
 924:         if (this.dataset != null) {
 925:             if (this.renderer != null) {
 926:                 int seriesCount = this.dataset.getSeriesCount();
 927:                 for (int i = 0; i < seriesCount; i++) {
 928:                     LegendItem item = this.renderer.getLegendItem(i);
 929:                     result.add(item);
 930:                 }
 931:             }
 932:         }      
 933:         return result;
 934:     }
 935:    
 936:     /**
 937:      * Tests this plot for equality with another object.
 938:      *
 939:      * @param obj  the object (<code>null</code> permitted).
 940:      *
 941:      * @return <code>true</code> or <code>false</code>.
 942:      */
 943:     public boolean equals(Object obj) {
 944:         if (obj == this) {
 945:             return true;
 946:         }
 947:         if (!(obj instanceof PolarPlot)) {
 948:             return false;
 949:         }
 950:         PolarPlot that = (PolarPlot) obj;
 951:         if (!ObjectUtilities.equal(this.axis, that.axis)) {
 952:             return false;
 953:         }
 954:         if (!ObjectUtilities.equal(this.renderer, that.renderer)) {
 955:             return false;
 956:         }
 957:         if (this.angleGridlinesVisible != that.angleGridlinesVisible) {
 958:             return false;
 959:         }
 960:         if (this.angleLabelsVisible != that.angleLabelsVisible) {
 961:             return false;   
 962:         }
 963:         if (!this.angleLabelFont.equals(that.angleLabelFont)) {
 964:             return false;   
 965:         }
 966:         if (!PaintUtilities.equal(this.angleLabelPaint, that.angleLabelPaint)) {
 967:             return false;   
 968:         }
 969:         if (!ObjectUtilities.equal(this.angleGridlineStroke, 
 970:                 that.angleGridlineStroke)) {
 971:             return false;
 972:         }
 973:         if (!PaintUtilities.equal(
 974:             this.angleGridlinePaint, that.angleGridlinePaint
 975:         )) {
 976:             return false;
 977:         }
 978:         if (this.radiusGridlinesVisible != that.radiusGridlinesVisible) {
 979:             return false;
 980:         }
 981:         if (!ObjectUtilities.equal(this.radiusGridlineStroke, 
 982:                 that.radiusGridlineStroke)) {
 983:             return false;
 984:         }
 985:         if (!PaintUtilities.equal(this.radiusGridlinePaint, 
 986:                 that.radiusGridlinePaint)) {
 987:             return false;
 988:         }
 989:         if (!this.cornerTextItems.equals(that.cornerTextItems)) {
 990:             return false;
 991:         }
 992:         return super.equals(obj);
 993:     }
 994:    
 995:     /**
 996:      * Returns a clone of the plot.
 997:      *
 998:      * @return A clone.
 999:      *
1000:      * @throws CloneNotSupportedException  this can occur if some component of 
1001:      *         the plot cannot be cloned.
1002:      */
1003:     public Object clone() throws CloneNotSupportedException {
1004:       
1005:         PolarPlot clone = (PolarPlot) super.clone();
1006:         if (this.axis != null) {
1007:             clone.axis = (ValueAxis) ObjectUtilities.clone(this.axis);
1008:             clone.axis.setPlot(clone);
1009:             clone.axis.addChangeListener(clone);
1010:         }
1011:       
1012:         if (clone.dataset != null) {
1013:             clone.dataset.addChangeListener(clone);
1014:         }
1015:       
1016:         if (this.renderer != null) {
1017:             clone.renderer 
1018:                 = (PolarItemRenderer) ObjectUtilities.clone(this.renderer);
1019:         }
1020:         
1021:         clone.cornerTextItems = new ArrayList(this.cornerTextItems);
1022:        
1023:         return clone;
1024:     }
1025:    
1026:     /**
1027:      * Provides serialization support.
1028:      *
1029:      * @param stream  the output stream.
1030:      *
1031:      * @throws IOException  if there is an I/O error.
1032:      */
1033:     private void writeObject(ObjectOutputStream stream) throws IOException {
1034:         stream.defaultWriteObject();
1035:         SerialUtilities.writeStroke(this.angleGridlineStroke, stream);
1036:         SerialUtilities.writePaint(this.angleGridlinePaint, stream);
1037:         SerialUtilities.writeStroke(this.radiusGridlineStroke, stream);
1038:         SerialUtilities.writePaint(this.radiusGridlinePaint, stream);
1039:         SerialUtilities.writePaint(this.angleLabelPaint, stream);
1040:     }
1041:    
1042:     /**
1043:      * Provides serialization support.
1044:      *
1045:      * @param stream  the input stream.
1046:      *
1047:      * @throws IOException  if there is an I/O error.
1048:      * @throws ClassNotFoundException  if there is a classpath problem.
1049:      */
1050:     private void readObject(ObjectInputStream stream) 
1051:         throws IOException, ClassNotFoundException {
1052:       
1053:         stream.defaultReadObject();
1054:         this.angleGridlineStroke = SerialUtilities.readStroke(stream);
1055:         this.angleGridlinePaint = SerialUtilities.readPaint(stream);
1056:         this.radiusGridlineStroke = SerialUtilities.readStroke(stream);
1057:         this.radiusGridlinePaint = SerialUtilities.readPaint(stream);
1058:         this.angleLabelPaint = SerialUtilities.readPaint(stream);
1059:       
1060:         if (this.axis != null) {
1061:             this.axis.setPlot(this);
1062:             this.axis.addChangeListener(this);
1063:         }
1064:       
1065:         if (this.dataset != null) {
1066:             this.dataset.addChangeListener(this);
1067:         }
1068:     }
1069:    
1070:     /**
1071:      * This method is required by the {@link Zoomable} interface, but since
1072:      * the plot does not have any domain axes, it does nothing.
1073:      *
1074:      * @param factor  the zoom factor.
1075:      * @param state  the plot state.
1076:      * @param source  the source point (in Java2D coordinates).
1077:      */
1078:     public void zoomDomainAxes(double factor, PlotRenderingInfo state, 
1079:                                Point2D source) {
1080:         // do nothing
1081:     }
1082:    
1083:     /**
1084:      * This method is required by the {@link Zoomable} interface, but since
1085:      * the plot does not have any domain axes, it does nothing.
1086:      *
1087:      * @param factor  the zoom factor.
1088:      * @param state  the plot state.
1089:      * @param source  the source point (in Java2D coordinates).
1090:      * @param useAnchor  use source point as zoom anchor?
1091:      * 
1092:      * @since 1.0.7
1093:      */
1094:     public void zoomDomainAxes(double factor, PlotRenderingInfo state, 
1095:                                Point2D source, boolean useAnchor) {
1096:         // do nothing
1097:     }
1098:    
1099:     /**
1100:      * This method is required by the {@link Zoomable} interface, but since
1101:      * the plot does not have any domain axes, it does nothing.
1102:      * 
1103:      * @param lowerPercent  the new lower bound.
1104:      * @param upperPercent  the new upper bound.
1105:      * @param state  the plot state.
1106:      * @param source  the source point (in Java2D coordinates).
1107:      */
1108:     public void zoomDomainAxes(double lowerPercent, double upperPercent, 
1109:                                PlotRenderingInfo state, Point2D source) {
1110:         // do nothing
1111:     }
1112: 
1113:     /**
1114:      * Multiplies the range on the range axis/axes by the specified factor.
1115:      *
1116:      * @param factor  the zoom factor.
1117:      * @param state  the plot state.
1118:      * @param source  the source point (in Java2D coordinates).
1119:      */
1120:     public void zoomRangeAxes(double factor, PlotRenderingInfo state, 
1121:                               Point2D source) {
1122:         zoom(factor);
1123:     }
1124:    
1125:     /**
1126:      * Multiplies the range on the range axis by the specified factor.
1127:      *
1128:      * @param factor  the zoom factor.
1129:      * @param info  the plot rendering info.
1130:      * @param source  the source point (in Java2D space).
1131:      * @param useAnchor  use source point as zoom anchor?
1132:      * 
1133:      * @see #zoomDomainAxes(double, PlotRenderingInfo, Point2D, boolean)
1134:      * 
1135:      * @since 1.0.7
1136:      */
1137:     public void zoomRangeAxes(double factor, PlotRenderingInfo info,
1138:                               Point2D source, boolean useAnchor) {
1139:                 
1140:         if (useAnchor) {
1141:             // get the source coordinate - this plot has always a VERTICAL
1142:             // orientation
1143:             double sourceX = source.getX();
1144:             double anchorX = this.axis.java2DToValue(sourceX, 
1145:                     info.getDataArea(), RectangleEdge.BOTTOM);
1146:             this.axis.resizeRange(factor, anchorX);
1147:         }
1148:         else {
1149:             this.axis.resizeRange(factor);
1150:         }
1151:         
1152:     }
1153:     
1154:     /**
1155:      * Zooms in on the range axes.
1156:      * 
1157:      * @param lowerPercent  the new lower bound.
1158:      * @param upperPercent  the new upper bound.
1159:      * @param state  the plot state.
1160:      * @param source  the source point (in Java2D coordinates).
1161:      */
1162:     public void zoomRangeAxes(double lowerPercent, double upperPercent, 
1163:                               PlotRenderingInfo state, Point2D source) {
1164:         zoom((upperPercent + lowerPercent) / 2.0);
1165:     }   
1166: 
1167:     /**
1168:      * Returns <code>false</code> always.
1169:      * 
1170:      * @return <code>false</code> always.
1171:      */
1172:     public boolean isDomainZoomable() {
1173:         return false;
1174:     }
1175:     
1176:     /**
1177:      * Returns <code>true</code> to indicate that the range axis is zoomable.
1178:      * 
1179:      * @return <code>true</code>.
1180:      */
1181:     public boolean isRangeZoomable() {
1182:         return true;
1183:     }
1184:     
1185:     /**
1186:      * Returns the orientation of the plot.
1187:      * 
1188:      * @return The orientation.
1189:      */
1190:     public PlotOrientation getOrientation() {
1191:         return PlotOrientation.HORIZONTAL;
1192:     }
1193: 
1194:     /**
1195:      * Returns the upper bound of the radius axis.
1196:      * 
1197:      * @return The upper bound.
1198:      */
1199:     public double getMaxRadius() {
1200:         return this.axis.getUpperBound();
1201:     }
1202: 
1203:     /**
1204:      * Translates a (theta, radius) pair into Java2D coordinates.  If 
1205:      * <code>radius</code> is less than the lower bound of the axis, then
1206:      * this method returns the centre point.
1207:      * 
1208:      * @param angleDegrees  the angle in degrees.
1209:      * @param radius  the radius.
1210:      * @param dataArea  the data area.
1211:      * 
1212:      * @return A point in Java2D space.
1213:      */   
1214:     public Point translateValueThetaRadiusToJava2D(double angleDegrees, 
1215:                                                    double radius,
1216:                                                    Rectangle2D dataArea) {
1217:        
1218:         double radians = Math.toRadians(angleDegrees - 90.0);
1219:       
1220:         double minx = dataArea.getMinX() + MARGIN;
1221:         double maxx = dataArea.getMaxX() - MARGIN;
1222:         double miny = dataArea.getMinY() + MARGIN;
1223:         double maxy = dataArea.getMaxY() - MARGIN;
1224:       
1225:         double lengthX = maxx - minx;
1226:         double lengthY = maxy - miny;
1227:         double length = Math.min(lengthX, lengthY);
1228:       
1229:         double midX = minx + lengthX / 2.0;
1230:         double midY = miny + lengthY / 2.0;
1231:       
1232:         double axisMin = this.axis.getLowerBound();
1233:         double axisMax =  getMaxRadius();
1234:         double adjustedRadius = Math.max(radius, axisMin);
1235: 
1236:         double xv = length / 2.0 * Math.cos(radians);
1237:         double yv = length / 2.0 * Math.sin(radians);
1238: 
1239:         float x = (float) (midX + (xv * (adjustedRadius - axisMin) 
1240:                 / (axisMax - axisMin)));
1241:         float y = (float) (midY + (yv * (adjustedRadius - axisMin) 
1242:                 / (axisMax - axisMin)));
1243:       
1244:         int ix = Math.round(x);
1245:         int iy = Math.round(y);
1246:       
1247:         Point p = new Point(ix, iy);
1248:         return p;
1249:         
1250:     }
1251:     
1252: }