0001: /* ===========================================================
0002: * JFreeChart : a free chart library for the Java(tm) platform
0003: * ===========================================================
0004: *
0005: * (C) Copyright 2000-2007, by Object Refinery Limited and Contributors.
0006: *
0007: * Project Info: http://www.jfree.org/jfreechart/index.html
0008: *
0009: * This library is free software; you can redistribute it and/or modify it
0010: * under the terms of the GNU Lesser General Public License as published by
0011: * the Free Software Foundation; either version 2.1 of the License, or
0012: * (at your option) any later version.
0013: *
0014: * This library is distributed in the hope that it will be useful, but
0015: * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
0016: * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
0017: * License for more details.
0018: *
0019: * You should have received a copy of the GNU Lesser General Public
0020: * License along with this library; if not, write to the Free Software
0021: * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
0022: * USA.
0023: *
0024: * [Java is a trademark or registered trademark of Sun Microsystems, Inc.
0025: * in the United States and other countries.]
0026: *
0027: * --------------
0028: * PolarPlot.java
0029: * --------------
0030: * (C) Copyright 2004-2007, by Solution Engineering, Inc. and Contributors.
0031: *
0032: * Original Author: Daniel Bridenbecker, Solution Engineering, Inc.;
0033: * Contributor(s): David Gilbert (for Object Refinery Limited);
0034: *
0035: * $Id: PolarPlot.java,v 1.13.2.8 2007/03/21 10:37:20 mungady Exp $
0036: *
0037: * Changes
0038: * -------
0039: * 19-Jan-2004 : Version 1, contributed by DB with minor changes by DG (DG);
0040: * 07-Apr-2004 : Changed text bounds calculation (DG);
0041: * 05-May-2005 : Updated draw() method parameters (DG);
0042: * 09-Jun-2005 : Fixed getDataRange() and equals() methods (DG);
0043: * 25-Oct-2005 : Implemented Zoomable (DG);
0044: * ------------- JFREECHART 1.0.x ---------------------------------------------
0045: * 07-Feb-2007 : Fixed bug 1599761, data value less than axis minimum (DG);
0046: * 21-Mar-2007 : Fixed serialization bug (DG);
0047: *
0048: */
0049:
0050: package org.jfree.chart.plot;
0051:
0052: import java.awt.AlphaComposite;
0053: import java.awt.BasicStroke;
0054: import java.awt.Color;
0055: import java.awt.Composite;
0056: import java.awt.Font;
0057: import java.awt.FontMetrics;
0058: import java.awt.Graphics2D;
0059: import java.awt.Paint;
0060: import java.awt.Point;
0061: import java.awt.Shape;
0062: import java.awt.Stroke;
0063: import java.awt.geom.Point2D;
0064: import java.awt.geom.Rectangle2D;
0065: import java.io.IOException;
0066: import java.io.ObjectInputStream;
0067: import java.io.ObjectOutputStream;
0068: import java.io.Serializable;
0069: import java.util.ArrayList;
0070: import java.util.Iterator;
0071: import java.util.List;
0072: import java.util.ResourceBundle;
0073:
0074: import org.jfree.chart.LegendItem;
0075: import org.jfree.chart.LegendItemCollection;
0076: import org.jfree.chart.axis.AxisState;
0077: import org.jfree.chart.axis.NumberTick;
0078: import org.jfree.chart.axis.ValueAxis;
0079: import org.jfree.chart.event.PlotChangeEvent;
0080: import org.jfree.chart.event.RendererChangeEvent;
0081: import org.jfree.chart.event.RendererChangeListener;
0082: import org.jfree.chart.renderer.PolarItemRenderer;
0083: import org.jfree.data.Range;
0084: import org.jfree.data.general.DatasetChangeEvent;
0085: import org.jfree.data.general.DatasetUtilities;
0086: import org.jfree.data.xy.XYDataset;
0087: import org.jfree.io.SerialUtilities;
0088: import org.jfree.text.TextUtilities;
0089: import org.jfree.ui.RectangleEdge;
0090: import org.jfree.ui.RectangleInsets;
0091: import org.jfree.ui.TextAnchor;
0092: import org.jfree.util.ObjectUtilities;
0093: import org.jfree.util.PaintUtilities;
0094:
0095: /**
0096: * Plots data that is in (theta, radius) pairs where
0097: * theta equal to zero is due north and increases clockwise.
0098: */
0099: public class PolarPlot extends Plot implements ValueAxisPlot, Zoomable,
0100: RendererChangeListener, Cloneable, Serializable {
0101:
0102: /** For serialization. */
0103: private static final long serialVersionUID = 3794383185924179525L;
0104:
0105: /** The default margin. */
0106: private static final int MARGIN = 20;
0107:
0108: /** The annotation margin. */
0109: private static final double ANNOTATION_MARGIN = 7.0;
0110:
0111: /** The default grid line stroke. */
0112: public static final Stroke DEFAULT_GRIDLINE_STROKE = new BasicStroke(
0113: 0.5f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 0.0f,
0114: new float[] { 2.0f, 2.0f }, 0.0f);
0115:
0116: /** The default grid line paint. */
0117: public static final Paint DEFAULT_GRIDLINE_PAINT = Color.gray;
0118:
0119: /** The resourceBundle for the localization. */
0120: protected static ResourceBundle localizationResources = ResourceBundle
0121: .getBundle("org.jfree.chart.plot.LocalizationBundle");
0122:
0123: /** The angles that are marked with gridlines. */
0124: private List angleTicks;
0125:
0126: /** The axis (used for the y-values). */
0127: private ValueAxis axis;
0128:
0129: /** The dataset. */
0130: private XYDataset dataset;
0131:
0132: /**
0133: * Object responsible for drawing the visual representation of each point
0134: * on the plot.
0135: */
0136: private PolarItemRenderer renderer;
0137:
0138: /** A flag that controls whether or not the angle labels are visible. */
0139: private boolean angleLabelsVisible = true;
0140:
0141: /** The font used to display the angle labels - never null. */
0142: private Font angleLabelFont = new Font("SansSerif", Font.PLAIN, 12);
0143:
0144: /** The paint used to display the angle labels. */
0145: private transient Paint angleLabelPaint = Color.black;
0146:
0147: /** A flag that controls whether the angular grid-lines are visible. */
0148: private boolean angleGridlinesVisible;
0149:
0150: /** The stroke used to draw the angular grid-lines. */
0151: private transient Stroke angleGridlineStroke;
0152:
0153: /** The paint used to draw the angular grid-lines. */
0154: private transient Paint angleGridlinePaint;
0155:
0156: /** A flag that controls whether the radius grid-lines are visible. */
0157: private boolean radiusGridlinesVisible;
0158:
0159: /** The stroke used to draw the radius grid-lines. */
0160: private transient Stroke radiusGridlineStroke;
0161:
0162: /** The paint used to draw the radius grid-lines. */
0163: private transient Paint radiusGridlinePaint;
0164:
0165: /** The annotations for the plot. */
0166: private List cornerTextItems = new ArrayList();
0167:
0168: /**
0169: * Default constructor.
0170: */
0171: public PolarPlot() {
0172: this (null, null, null);
0173: }
0174:
0175: /**
0176: * Creates a new plot.
0177: *
0178: * @param dataset the dataset (<code>null</code> permitted).
0179: * @param radiusAxis the radius axis (<code>null</code> permitted).
0180: * @param renderer the renderer (<code>null</code> permitted).
0181: */
0182: public PolarPlot(XYDataset dataset, ValueAxis radiusAxis,
0183: PolarItemRenderer renderer) {
0184:
0185: super ();
0186:
0187: this .dataset = dataset;
0188: if (this .dataset != null) {
0189: this .dataset.addChangeListener(this );
0190: }
0191:
0192: this .angleTicks = new java.util.ArrayList();
0193: this .angleTicks.add(new NumberTick(new Double(0.0), "0",
0194: TextAnchor.CENTER, TextAnchor.CENTER, 0.0));
0195: this .angleTicks.add(new NumberTick(new Double(45.0), "45",
0196: TextAnchor.CENTER, TextAnchor.CENTER, 0.0));
0197: this .angleTicks.add(new NumberTick(new Double(90.0), "90",
0198: TextAnchor.CENTER, TextAnchor.CENTER, 0.0));
0199: this .angleTicks.add(new NumberTick(new Double(135.0), "135",
0200: TextAnchor.CENTER, TextAnchor.CENTER, 0.0));
0201: this .angleTicks.add(new NumberTick(new Double(180.0), "180",
0202: TextAnchor.CENTER, TextAnchor.CENTER, 0.0));
0203: this .angleTicks.add(new NumberTick(new Double(225.0), "225",
0204: TextAnchor.CENTER, TextAnchor.CENTER, 0.0));
0205: this .angleTicks.add(new NumberTick(new Double(270.0), "270",
0206: TextAnchor.CENTER, TextAnchor.CENTER, 0.0));
0207: this .angleTicks.add(new NumberTick(new Double(315.0), "315",
0208: TextAnchor.CENTER, TextAnchor.CENTER, 0.0));
0209:
0210: this .axis = radiusAxis;
0211: if (this .axis != null) {
0212: this .axis.setPlot(this );
0213: this .axis.addChangeListener(this );
0214: }
0215:
0216: this .renderer = renderer;
0217: if (this .renderer != null) {
0218: this .renderer.setPlot(this );
0219: this .renderer.addChangeListener(this );
0220: }
0221:
0222: this .angleGridlinesVisible = true;
0223: this .angleGridlineStroke = DEFAULT_GRIDLINE_STROKE;
0224: this .angleGridlinePaint = DEFAULT_GRIDLINE_PAINT;
0225:
0226: this .radiusGridlinesVisible = true;
0227: this .radiusGridlineStroke = DEFAULT_GRIDLINE_STROKE;
0228: this .radiusGridlinePaint = DEFAULT_GRIDLINE_PAINT;
0229: }
0230:
0231: /**
0232: * Add text to be displayed in the lower right hand corner and sends a
0233: * {@link PlotChangeEvent} to all registered listeners.
0234: *
0235: * @param text the text to display (<code>null</code> not permitted).
0236: *
0237: * @see #removeCornerTextItem(String)
0238: */
0239: public void addCornerTextItem(String text) {
0240: if (text == null) {
0241: throw new IllegalArgumentException("Null 'text' argument.");
0242: }
0243: this .cornerTextItems.add(text);
0244: this .notifyListeners(new PlotChangeEvent(this ));
0245: }
0246:
0247: /**
0248: * Remove the given text from the list of corner text items and
0249: * sends a {@link PlotChangeEvent} to all registered listeners.
0250: *
0251: * @param text the text to remove (<code>null</code> ignored).
0252: *
0253: * @see #addCornerTextItem(String)
0254: */
0255: public void removeCornerTextItem(String text) {
0256: boolean removed = this .cornerTextItems.remove(text);
0257: if (removed) {
0258: this .notifyListeners(new PlotChangeEvent(this ));
0259: }
0260: }
0261:
0262: /**
0263: * Clear the list of corner text items and sends a {@link PlotChangeEvent}
0264: * to all registered listeners.
0265: *
0266: * @see #addCornerTextItem(String)
0267: * @see #removeCornerTextItem(String)
0268: */
0269: public void clearCornerTextItems() {
0270: if (this .cornerTextItems.size() > 0) {
0271: this .cornerTextItems.clear();
0272: this .notifyListeners(new PlotChangeEvent(this ));
0273: }
0274: }
0275:
0276: /**
0277: * Returns the plot type as a string.
0278: *
0279: * @return A short string describing the type of plot.
0280: */
0281: public String getPlotType() {
0282: return PolarPlot.localizationResources.getString("Polar_Plot");
0283: }
0284:
0285: /**
0286: * Returns the axis for the plot.
0287: *
0288: * @return The radius axis (possibly <code>null</code>).
0289: *
0290: * @see #setAxis(ValueAxis)
0291: */
0292: public ValueAxis getAxis() {
0293: return this .axis;
0294: }
0295:
0296: /**
0297: * Sets the axis for the plot and sends a {@link PlotChangeEvent} to all
0298: * registered listeners.
0299: *
0300: * @param axis the new axis (<code>null</code> permitted).
0301: */
0302: public void setAxis(ValueAxis axis) {
0303: if (axis != null) {
0304: axis.setPlot(this );
0305: }
0306:
0307: // plot is likely registered as a listener with the existing axis...
0308: if (this .axis != null) {
0309: this .axis.removeChangeListener(this );
0310: }
0311:
0312: this .axis = axis;
0313: if (this .axis != null) {
0314: this .axis.configure();
0315: this .axis.addChangeListener(this );
0316: }
0317: notifyListeners(new PlotChangeEvent(this ));
0318: }
0319:
0320: /**
0321: * Returns the primary dataset for the plot.
0322: *
0323: * @return The primary dataset (possibly <code>null</code>).
0324: *
0325: * @see #setDataset(XYDataset)
0326: */
0327: public XYDataset getDataset() {
0328: return this .dataset;
0329: }
0330:
0331: /**
0332: * Sets the dataset for the plot, replacing the existing dataset if there
0333: * is one.
0334: *
0335: * @param dataset the dataset (<code>null</code> permitted).
0336: *
0337: * @see #getDataset()
0338: */
0339: public void setDataset(XYDataset dataset) {
0340: // if there is an existing dataset, remove the plot from the list of
0341: // change listeners...
0342: XYDataset existing = this .dataset;
0343: if (existing != null) {
0344: existing.removeChangeListener(this );
0345: }
0346:
0347: // set the new m_Dataset, and register the chart as a change listener...
0348: this .dataset = dataset;
0349: if (this .dataset != null) {
0350: setDatasetGroup(this .dataset.getGroup());
0351: this .dataset.addChangeListener(this );
0352: }
0353:
0354: // send a m_Dataset change event to self...
0355: DatasetChangeEvent event = new DatasetChangeEvent(this ,
0356: this .dataset);
0357: datasetChanged(event);
0358: }
0359:
0360: /**
0361: * Returns the item renderer.
0362: *
0363: * @return The renderer (possibly <code>null</code>).
0364: *
0365: * @see #setRenderer(PolarItemRenderer)
0366: */
0367: public PolarItemRenderer getRenderer() {
0368: return this .renderer;
0369: }
0370:
0371: /**
0372: * Sets the item renderer, and notifies all listeners of a change to the
0373: * plot.
0374: * <P>
0375: * If the renderer is set to <code>null</code>, no chart will be drawn.
0376: *
0377: * @param renderer the new renderer (<code>null</code> permitted).
0378: *
0379: * @see #getRenderer()
0380: */
0381: public void setRenderer(PolarItemRenderer renderer) {
0382: if (this .renderer != null) {
0383: this .renderer.removeChangeListener(this );
0384: }
0385:
0386: this .renderer = renderer;
0387: if (this .renderer != null) {
0388: this .renderer.setPlot(this );
0389: }
0390:
0391: notifyListeners(new PlotChangeEvent(this ));
0392: }
0393:
0394: /**
0395: * Returns a flag that controls whether or not the angle labels are visible.
0396: *
0397: * @return A boolean.
0398: *
0399: * @see #setAngleLabelsVisible(boolean)
0400: */
0401: public boolean isAngleLabelsVisible() {
0402: return this .angleLabelsVisible;
0403: }
0404:
0405: /**
0406: * Sets the flag that controls whether or not the angle labels are visible,
0407: * and sends a {@link PlotChangeEvent} to all registered listeners.
0408: *
0409: * @param visible the flag.
0410: *
0411: * @see #isAngleLabelsVisible()
0412: */
0413: public void setAngleLabelsVisible(boolean visible) {
0414: if (this .angleLabelsVisible != visible) {
0415: this .angleLabelsVisible = visible;
0416: notifyListeners(new PlotChangeEvent(this ));
0417: }
0418: }
0419:
0420: /**
0421: * Returns the font used to display the angle labels.
0422: *
0423: * @return A font (never <code>null</code>).
0424: *
0425: * @see #setAngleLabelFont(Font)
0426: */
0427: public Font getAngleLabelFont() {
0428: return this .angleLabelFont;
0429: }
0430:
0431: /**
0432: * Sets the font used to display the angle labels and sends a
0433: * {@link PlotChangeEvent} to all registered listeners.
0434: *
0435: * @param font the font (<code>null</code> not permitted).
0436: *
0437: * @see #getAngleLabelFont()
0438: */
0439: public void setAngleLabelFont(Font font) {
0440: if (font == null) {
0441: throw new IllegalArgumentException("Null 'font' argument.");
0442: }
0443: this .angleLabelFont = font;
0444: notifyListeners(new PlotChangeEvent(this ));
0445: }
0446:
0447: /**
0448: * Returns the paint used to display the angle labels.
0449: *
0450: * @return A paint (never <code>null</code>).
0451: *
0452: * @see #setAngleLabelPaint(Paint)
0453: */
0454: public Paint getAngleLabelPaint() {
0455: return this .angleLabelPaint;
0456: }
0457:
0458: /**
0459: * Sets the paint used to display the angle labels and sends a
0460: * {@link PlotChangeEvent} to all registered listeners.
0461: *
0462: * @param paint the paint (<code>null</code> not permitted).
0463: */
0464: public void setAngleLabelPaint(Paint paint) {
0465: if (paint == null) {
0466: throw new IllegalArgumentException("Null 'paint' argument.");
0467: }
0468: this .angleLabelPaint = paint;
0469: notifyListeners(new PlotChangeEvent(this ));
0470: }
0471:
0472: /**
0473: * Returns <code>true</code> if the angular gridlines are visible, and
0474: * <code>false<code> otherwise.
0475: *
0476: * @return <code>true</code> or <code>false</code>.
0477: *
0478: * @see #setAngleGridlinesVisible(boolean)
0479: */
0480: public boolean isAngleGridlinesVisible() {
0481: return this .angleGridlinesVisible;
0482: }
0483:
0484: /**
0485: * Sets the flag that controls whether or not the angular grid-lines are
0486: * visible.
0487: * <p>
0488: * If the flag value is changed, a {@link PlotChangeEvent} is sent to all
0489: * registered listeners.
0490: *
0491: * @param visible the new value of the flag.
0492: *
0493: * @see #isAngleGridlinesVisible()
0494: */
0495: public void setAngleGridlinesVisible(boolean visible) {
0496: if (this .angleGridlinesVisible != visible) {
0497: this .angleGridlinesVisible = visible;
0498: notifyListeners(new PlotChangeEvent(this ));
0499: }
0500: }
0501:
0502: /**
0503: * Returns the stroke for the grid-lines (if any) plotted against the
0504: * angular axis.
0505: *
0506: * @return The stroke (possibly <code>null</code>).
0507: *
0508: * @see #setAngleGridlineStroke(Stroke)
0509: */
0510: public Stroke getAngleGridlineStroke() {
0511: return this .angleGridlineStroke;
0512: }
0513:
0514: /**
0515: * Sets the stroke for the grid lines plotted against the angular axis and
0516: * sends a {@link PlotChangeEvent} to all registered listeners.
0517: * <p>
0518: * If you set this to <code>null</code>, no grid lines will be drawn.
0519: *
0520: * @param stroke the stroke (<code>null</code> permitted).
0521: *
0522: * @see #getAngleGridlineStroke()
0523: */
0524: public void setAngleGridlineStroke(Stroke stroke) {
0525: this .angleGridlineStroke = stroke;
0526: notifyListeners(new PlotChangeEvent(this ));
0527: }
0528:
0529: /**
0530: * Returns the paint for the grid lines (if any) plotted against the
0531: * angular axis.
0532: *
0533: * @return The paint (possibly <code>null</code>).
0534: *
0535: * @see #setAngleGridlinePaint(Paint)
0536: */
0537: public Paint getAngleGridlinePaint() {
0538: return this .angleGridlinePaint;
0539: }
0540:
0541: /**
0542: * Sets the paint for the grid lines plotted against the angular axis.
0543: * <p>
0544: * If you set this to <code>null</code>, no grid lines will be drawn.
0545: *
0546: * @param paint the paint (<code>null</code> permitted).
0547: *
0548: * @see #getAngleGridlinePaint()
0549: */
0550: public void setAngleGridlinePaint(Paint paint) {
0551: this .angleGridlinePaint = paint;
0552: notifyListeners(new PlotChangeEvent(this ));
0553: }
0554:
0555: /**
0556: * Returns <code>true</code> if the radius axis grid is visible, and
0557: * <code>false<code> otherwise.
0558: *
0559: * @return <code>true</code> or <code>false</code>.
0560: *
0561: * @see #setRadiusGridlinesVisible(boolean)
0562: */
0563: public boolean isRadiusGridlinesVisible() {
0564: return this .radiusGridlinesVisible;
0565: }
0566:
0567: /**
0568: * Sets the flag that controls whether or not the radius axis grid lines
0569: * are visible.
0570: * <p>
0571: * If the flag value is changed, a {@link PlotChangeEvent} is sent to all
0572: * registered listeners.
0573: *
0574: * @param visible the new value of the flag.
0575: *
0576: * @see #isRadiusGridlinesVisible()
0577: */
0578: public void setRadiusGridlinesVisible(boolean visible) {
0579: if (this .radiusGridlinesVisible != visible) {
0580: this .radiusGridlinesVisible = visible;
0581: notifyListeners(new PlotChangeEvent(this ));
0582: }
0583: }
0584:
0585: /**
0586: * Returns the stroke for the grid lines (if any) plotted against the
0587: * radius axis.
0588: *
0589: * @return The stroke (possibly <code>null</code>).
0590: *
0591: * @see #setRadiusGridlineStroke(Stroke)
0592: */
0593: public Stroke getRadiusGridlineStroke() {
0594: return this .radiusGridlineStroke;
0595: }
0596:
0597: /**
0598: * Sets the stroke for the grid lines plotted against the radius axis and
0599: * sends a {@link PlotChangeEvent} to all registered listeners.
0600: * <p>
0601: * If you set this to <code>null</code>, no grid lines will be drawn.
0602: *
0603: * @param stroke the stroke (<code>null</code> permitted).
0604: *
0605: * @see #getRadiusGridlineStroke()
0606: */
0607: public void setRadiusGridlineStroke(Stroke stroke) {
0608: this .radiusGridlineStroke = stroke;
0609: notifyListeners(new PlotChangeEvent(this ));
0610: }
0611:
0612: /**
0613: * Returns the paint for the grid lines (if any) plotted against the radius
0614: * axis.
0615: *
0616: * @return The paint (possibly <code>null</code>).
0617: *
0618: * @see #setRadiusGridlinePaint(Paint)
0619: */
0620: public Paint getRadiusGridlinePaint() {
0621: return this .radiusGridlinePaint;
0622: }
0623:
0624: /**
0625: * Sets the paint for the grid lines plotted against the radius axis and
0626: * sends a {@link PlotChangeEvent} to all registered listeners.
0627: * <p>
0628: * If you set this to <code>null</code>, no grid lines will be drawn.
0629: *
0630: * @param paint the paint (<code>null</code> permitted).
0631: *
0632: * @see #getRadiusGridlinePaint()
0633: */
0634: public void setRadiusGridlinePaint(Paint paint) {
0635: this .radiusGridlinePaint = paint;
0636: notifyListeners(new PlotChangeEvent(this ));
0637: }
0638:
0639: /**
0640: * Draws the plot on a Java 2D graphics device (such as the screen or a
0641: * printer).
0642: * <P>
0643: * This plot relies on a {@link PolarItemRenderer} to draw each
0644: * item in the plot. This allows the visual representation of the data to
0645: * be changed easily.
0646: * <P>
0647: * The optional info argument collects information about the rendering of
0648: * the plot (dimensions, tooltip information etc). Just pass in
0649: * <code>null</code> if you do not need this information.
0650: *
0651: * @param g2 the graphics device.
0652: * @param area the area within which the plot (including axes and
0653: * labels) should be drawn.
0654: * @param anchor the anchor point (<code>null</code> permitted).
0655: * @param parentState ignored.
0656: * @param info collects chart drawing information (<code>null</code>
0657: * permitted).
0658: */
0659: public void draw(Graphics2D g2, Rectangle2D area, Point2D anchor,
0660: PlotState parentState, PlotRenderingInfo info) {
0661:
0662: // if the plot area is too small, just return...
0663: boolean b1 = (area.getWidth() <= MINIMUM_WIDTH_TO_DRAW);
0664: boolean b2 = (area.getHeight() <= MINIMUM_HEIGHT_TO_DRAW);
0665: if (b1 || b2) {
0666: return;
0667: }
0668:
0669: // record the plot area...
0670: if (info != null) {
0671: info.setPlotArea(area);
0672: }
0673:
0674: // adjust the drawing area for the plot insets (if any)...
0675: RectangleInsets insets = getInsets();
0676: insets.trim(area);
0677:
0678: Rectangle2D dataArea = area;
0679: if (info != null) {
0680: info.setDataArea(dataArea);
0681: }
0682:
0683: // draw the plot background and axes...
0684: drawBackground(g2, dataArea);
0685: double h = Math.min(dataArea.getWidth() / 2.0, dataArea
0686: .getHeight() / 2.0)
0687: - MARGIN;
0688: Rectangle2D quadrant = new Rectangle2D.Double(dataArea
0689: .getCenterX(), dataArea.getCenterY(), h, h);
0690: AxisState state = drawAxis(g2, area, quadrant);
0691: if (this .renderer != null) {
0692: Shape originalClip = g2.getClip();
0693: Composite originalComposite = g2.getComposite();
0694:
0695: g2.clip(dataArea);
0696: g2.setComposite(AlphaComposite.getInstance(
0697: AlphaComposite.SRC_OVER, getForegroundAlpha()));
0698:
0699: drawGridlines(g2, dataArea, this .angleTicks, state
0700: .getTicks());
0701:
0702: // draw...
0703: render(g2, dataArea, info);
0704:
0705: g2.setClip(originalClip);
0706: g2.setComposite(originalComposite);
0707: }
0708: drawOutline(g2, dataArea);
0709: drawCornerTextItems(g2, dataArea);
0710: }
0711:
0712: /**
0713: * Draws the corner text items.
0714: *
0715: * @param g2 the drawing surface.
0716: * @param area the area.
0717: */
0718: protected void drawCornerTextItems(Graphics2D g2, Rectangle2D area) {
0719: if (this .cornerTextItems.isEmpty()) {
0720: return;
0721: }
0722:
0723: g2.setColor(Color.black);
0724: double width = 0.0;
0725: double height = 0.0;
0726: for (Iterator it = this .cornerTextItems.iterator(); it
0727: .hasNext();) {
0728: String msg = (String) it.next();
0729: FontMetrics fm = g2.getFontMetrics();
0730: Rectangle2D bounds = TextUtilities.getTextBounds(msg, g2,
0731: fm);
0732: width = Math.max(width, bounds.getWidth());
0733: height += bounds.getHeight();
0734: }
0735:
0736: double xadj = ANNOTATION_MARGIN * 2.0;
0737: double yadj = ANNOTATION_MARGIN;
0738: width += xadj;
0739: height += yadj;
0740:
0741: double x = area.getMaxX() - width;
0742: double y = area.getMaxY() - height;
0743: g2.drawRect((int) x, (int) y, (int) width, (int) height);
0744: x += ANNOTATION_MARGIN;
0745: for (Iterator it = this .cornerTextItems.iterator(); it
0746: .hasNext();) {
0747: String msg = (String) it.next();
0748: Rectangle2D bounds = TextUtilities.getTextBounds(msg, g2,
0749: g2.getFontMetrics());
0750: y += bounds.getHeight();
0751: g2.drawString(msg, (int) x, (int) y);
0752: }
0753: }
0754:
0755: /**
0756: * A utility method for drawing the axes.
0757: *
0758: * @param g2 the graphics device.
0759: * @param plotArea the plot area.
0760: * @param dataArea the data area.
0761: *
0762: * @return A map containing the axis states.
0763: */
0764: protected AxisState drawAxis(Graphics2D g2, Rectangle2D plotArea,
0765: Rectangle2D dataArea) {
0766: return this .axis.draw(g2, dataArea.getMinY(), plotArea,
0767: dataArea, RectangleEdge.TOP, null);
0768: }
0769:
0770: /**
0771: * Draws a representation of the data within the dataArea region, using the
0772: * current m_Renderer.
0773: *
0774: * @param g2 the graphics device.
0775: * @param dataArea the region in which the data is to be drawn.
0776: * @param info an optional object for collection dimension
0777: * information (<code>null</code> permitted).
0778: */
0779: protected void render(Graphics2D g2, Rectangle2D dataArea,
0780: PlotRenderingInfo info) {
0781:
0782: // now get the data and plot it (the visual representation will depend
0783: // on the m_Renderer that has been set)...
0784: if (!DatasetUtilities.isEmptyOrNull(this .dataset)) {
0785: int seriesCount = this .dataset.getSeriesCount();
0786: for (int series = 0; series < seriesCount; series++) {
0787: this .renderer.drawSeries(g2, dataArea, info, this ,
0788: this .dataset, series);
0789: }
0790: } else {
0791: drawNoDataMessage(g2, dataArea);
0792: }
0793: }
0794:
0795: /**
0796: * Draws the gridlines for the plot, if they are visible.
0797: *
0798: * @param g2 the graphics device.
0799: * @param dataArea the data area.
0800: * @param angularTicks the ticks for the angular axis.
0801: * @param radialTicks the ticks for the radial axis.
0802: */
0803: protected void drawGridlines(Graphics2D g2, Rectangle2D dataArea,
0804: List angularTicks, List radialTicks) {
0805:
0806: // no renderer, no gridlines...
0807: if (this .renderer == null) {
0808: return;
0809: }
0810:
0811: // draw the domain grid lines, if any...
0812: if (isAngleGridlinesVisible()) {
0813: Stroke gridStroke = getAngleGridlineStroke();
0814: Paint gridPaint = getAngleGridlinePaint();
0815: if ((gridStroke != null) && (gridPaint != null)) {
0816: this .renderer.drawAngularGridLines(g2, this ,
0817: angularTicks, dataArea);
0818: }
0819: }
0820:
0821: // draw the radius grid lines, if any...
0822: if (isRadiusGridlinesVisible()) {
0823: Stroke gridStroke = getRadiusGridlineStroke();
0824: Paint gridPaint = getRadiusGridlinePaint();
0825: if ((gridStroke != null) && (gridPaint != null)) {
0826: this .renderer.drawRadialGridLines(g2, this , this .axis,
0827: radialTicks, dataArea);
0828: }
0829: }
0830: }
0831:
0832: /**
0833: * Zooms the axis ranges by the specified percentage about the anchor point.
0834: *
0835: * @param percent the amount of the zoom.
0836: */
0837: public void zoom(double percent) {
0838: if (percent > 0.0) {
0839: double radius = getMaxRadius();
0840: double scaledRadius = radius * percent;
0841: this .axis.setUpperBound(scaledRadius);
0842: getAxis().setAutoRange(false);
0843: } else {
0844: getAxis().setAutoRange(true);
0845: }
0846: }
0847:
0848: /**
0849: * Returns the range for the specified axis.
0850: *
0851: * @param axis the axis.
0852: *
0853: * @return The range.
0854: */
0855: public Range getDataRange(ValueAxis axis) {
0856: Range result = null;
0857: if (this .dataset != null) {
0858: result = Range.combine(result, DatasetUtilities
0859: .findRangeBounds(this .dataset));
0860: }
0861: return result;
0862: }
0863:
0864: /**
0865: * Receives notification of a change to the plot's m_Dataset.
0866: * <P>
0867: * The axis ranges are updated if necessary.
0868: *
0869: * @param event information about the event (not used here).
0870: */
0871: public void datasetChanged(DatasetChangeEvent event) {
0872:
0873: if (this .axis != null) {
0874: this .axis.configure();
0875: }
0876:
0877: if (getParent() != null) {
0878: getParent().datasetChanged(event);
0879: } else {
0880: super .datasetChanged(event);
0881: }
0882: }
0883:
0884: /**
0885: * Notifies all registered listeners of a property change.
0886: * <P>
0887: * One source of property change events is the plot's m_Renderer.
0888: *
0889: * @param event information about the property change.
0890: */
0891: public void rendererChanged(RendererChangeEvent event) {
0892: notifyListeners(new PlotChangeEvent(this ));
0893: }
0894:
0895: /**
0896: * Returns the number of series in the dataset for this plot. If the
0897: * dataset is <code>null</code>, the method returns 0.
0898: *
0899: * @return The series count.
0900: */
0901: public int getSeriesCount() {
0902: int result = 0;
0903:
0904: if (this .dataset != null) {
0905: result = this .dataset.getSeriesCount();
0906: }
0907: return result;
0908: }
0909:
0910: /**
0911: * Returns the legend items for the plot. Each legend item is generated by
0912: * the plot's m_Renderer, since the m_Renderer is responsible for the visual
0913: * representation of the data.
0914: *
0915: * @return The legend items.
0916: */
0917: public LegendItemCollection getLegendItems() {
0918: LegendItemCollection result = new LegendItemCollection();
0919:
0920: // get the legend items for the main m_Dataset...
0921: if (this .dataset != null) {
0922: if (this .renderer != null) {
0923: int seriesCount = this .dataset.getSeriesCount();
0924: for (int i = 0; i < seriesCount; i++) {
0925: LegendItem item = this .renderer.getLegendItem(i);
0926: result.add(item);
0927: }
0928: }
0929: }
0930: return result;
0931: }
0932:
0933: /**
0934: * Tests this plot for equality with another object.
0935: *
0936: * @param obj the object (<code>null</code> permitted).
0937: *
0938: * @return <code>true</code> or <code>false</code>.
0939: */
0940: public boolean equals(Object obj) {
0941: if (obj == this ) {
0942: return true;
0943: }
0944: if (!(obj instanceof PolarPlot)) {
0945: return false;
0946: }
0947: PolarPlot that = (PolarPlot) obj;
0948: if (!ObjectUtilities.equal(this .axis, that.axis)) {
0949: return false;
0950: }
0951: if (!ObjectUtilities.equal(this .renderer, that.renderer)) {
0952: return false;
0953: }
0954: if (this .angleGridlinesVisible != that.angleGridlinesVisible) {
0955: return false;
0956: }
0957: if (this .angleLabelsVisible != that.angleLabelsVisible) {
0958: return false;
0959: }
0960: if (!this .angleLabelFont.equals(that.angleLabelFont)) {
0961: return false;
0962: }
0963: if (!PaintUtilities.equal(this .angleLabelPaint,
0964: that.angleLabelPaint)) {
0965: return false;
0966: }
0967: if (!ObjectUtilities.equal(this .angleGridlineStroke,
0968: that.angleGridlineStroke)) {
0969: return false;
0970: }
0971: if (!PaintUtilities.equal(this .angleGridlinePaint,
0972: that.angleGridlinePaint)) {
0973: return false;
0974: }
0975: if (this .radiusGridlinesVisible != that.radiusGridlinesVisible) {
0976: return false;
0977: }
0978: if (!ObjectUtilities.equal(this .radiusGridlineStroke,
0979: that.radiusGridlineStroke)) {
0980: return false;
0981: }
0982: if (!PaintUtilities.equal(this .radiusGridlinePaint,
0983: that.radiusGridlinePaint)) {
0984: return false;
0985: }
0986: if (!this .cornerTextItems.equals(that.cornerTextItems)) {
0987: return false;
0988: }
0989: return super .equals(obj);
0990: }
0991:
0992: /**
0993: * Returns a clone of the plot.
0994: *
0995: * @return A clone.
0996: *
0997: * @throws CloneNotSupportedException this can occur if some component of
0998: * the plot cannot be cloned.
0999: */
1000: public Object clone() throws CloneNotSupportedException {
1001:
1002: PolarPlot clone = (PolarPlot) super .clone();
1003: if (this .axis != null) {
1004: clone.axis = (ValueAxis) ObjectUtilities.clone(this .axis);
1005: clone.axis.setPlot(clone);
1006: clone.axis.addChangeListener(clone);
1007: }
1008:
1009: if (clone.dataset != null) {
1010: clone.dataset.addChangeListener(clone);
1011: }
1012:
1013: if (this .renderer != null) {
1014: clone.renderer = (PolarItemRenderer) ObjectUtilities
1015: .clone(this .renderer);
1016: }
1017:
1018: clone.cornerTextItems = new ArrayList(this .cornerTextItems);
1019:
1020: return clone;
1021: }
1022:
1023: /**
1024: * Provides serialization support.
1025: *
1026: * @param stream the output stream.
1027: *
1028: * @throws IOException if there is an I/O error.
1029: */
1030: private void writeObject(ObjectOutputStream stream)
1031: throws IOException {
1032: stream.defaultWriteObject();
1033: SerialUtilities.writeStroke(this .angleGridlineStroke, stream);
1034: SerialUtilities.writePaint(this .angleGridlinePaint, stream);
1035: SerialUtilities.writeStroke(this .radiusGridlineStroke, stream);
1036: SerialUtilities.writePaint(this .radiusGridlinePaint, stream);
1037: SerialUtilities.writePaint(this .angleLabelPaint, stream);
1038: }
1039:
1040: /**
1041: * Provides serialization support.
1042: *
1043: * @param stream the input stream.
1044: *
1045: * @throws IOException if there is an I/O error.
1046: * @throws ClassNotFoundException if there is a classpath problem.
1047: */
1048: private void readObject(ObjectInputStream stream)
1049: throws IOException, ClassNotFoundException {
1050:
1051: stream.defaultReadObject();
1052: this .angleGridlineStroke = SerialUtilities.readStroke(stream);
1053: this .angleGridlinePaint = SerialUtilities.readPaint(stream);
1054: this .radiusGridlineStroke = SerialUtilities.readStroke(stream);
1055: this .radiusGridlinePaint = SerialUtilities.readPaint(stream);
1056: this .angleLabelPaint = SerialUtilities.readPaint(stream);
1057:
1058: if (this .axis != null) {
1059: this .axis.setPlot(this );
1060: this .axis.addChangeListener(this );
1061: }
1062:
1063: if (this .dataset != null) {
1064: this .dataset.addChangeListener(this );
1065: }
1066: }
1067:
1068: /**
1069: * This method is required by the {@link Zoomable} interface, but since
1070: * the plot does not have any domain axes, it does nothing.
1071: *
1072: * @param factor the zoom factor.
1073: * @param state the plot state.
1074: * @param source the source point (in Java2D coordinates).
1075: */
1076: public void zoomDomainAxes(double factor, PlotRenderingInfo state,
1077: Point2D source) {
1078: // do nothing
1079: }
1080:
1081: /**
1082: * This method is required by the {@link Zoomable} interface, but since
1083: * the plot does not have any domain axes, it does nothing.
1084: *
1085: * @param lowerPercent the new lower bound.
1086: * @param upperPercent the new upper bound.
1087: * @param state the plot state.
1088: * @param source the source point (in Java2D coordinates).
1089: */
1090: public void zoomDomainAxes(double lowerPercent,
1091: double upperPercent, PlotRenderingInfo state, Point2D source) {
1092: // do nothing
1093: }
1094:
1095: /**
1096: * Multiplies the range on the range axis/axes by the specified factor.
1097: *
1098: * @param factor the zoom factor.
1099: * @param state the plot state.
1100: * @param source the source point (in Java2D coordinates).
1101: */
1102: public void zoomRangeAxes(double factor, PlotRenderingInfo state,
1103: Point2D source) {
1104: zoom(factor);
1105: }
1106:
1107: /**
1108: * Zooms in on the range axes.
1109: *
1110: * @param lowerPercent the new lower bound.
1111: * @param upperPercent the new upper bound.
1112: * @param state the plot state.
1113: * @param source the source point (in Java2D coordinates).
1114: */
1115: public void zoomRangeAxes(double lowerPercent, double upperPercent,
1116: PlotRenderingInfo state, Point2D source) {
1117: zoom((upperPercent + lowerPercent) / 2.0);
1118: }
1119:
1120: /**
1121: * Returns <code>false</code> always.
1122: *
1123: * @return <code>false</code> always.
1124: */
1125: public boolean isDomainZoomable() {
1126: return false;
1127: }
1128:
1129: /**
1130: * Returns <code>true</code> to indicate that the range axis is zoomable.
1131: *
1132: * @return <code>true</code>.
1133: */
1134: public boolean isRangeZoomable() {
1135: return true;
1136: }
1137:
1138: /**
1139: * Returns the orientation of the plot.
1140: *
1141: * @return The orientation.
1142: */
1143: public PlotOrientation getOrientation() {
1144: return PlotOrientation.HORIZONTAL;
1145: }
1146:
1147: /**
1148: * Returns the upper bound of the radius axis.
1149: *
1150: * @return The upper bound.
1151: */
1152: public double getMaxRadius() {
1153: return this .axis.getUpperBound();
1154: }
1155:
1156: /**
1157: * Translates a (theta, radius) pair into Java2D coordinates. If
1158: * <code>radius</code> is less than the lower bound of the axis, then
1159: * this method returns the centre point.
1160: *
1161: * @param angleDegrees the angle in degrees.
1162: * @param radius the radius.
1163: * @param dataArea the data area.
1164: *
1165: * @return A point in Java2D space.
1166: */
1167: public Point translateValueThetaRadiusToJava2D(double angleDegrees,
1168: double radius, Rectangle2D dataArea) {
1169:
1170: double radians = Math.toRadians(angleDegrees - 90.0);
1171:
1172: double minx = dataArea.getMinX() + MARGIN;
1173: double maxx = dataArea.getMaxX() - MARGIN;
1174: double miny = dataArea.getMinY() + MARGIN;
1175: double maxy = dataArea.getMaxY() - MARGIN;
1176:
1177: double lengthX = maxx - minx;
1178: double lengthY = maxy - miny;
1179: double length = Math.min(lengthX, lengthY);
1180:
1181: double midX = minx + lengthX / 2.0;
1182: double midY = miny + lengthY / 2.0;
1183:
1184: double axisMin = this .axis.getLowerBound();
1185: double axisMax = getMaxRadius();
1186: double adjustedRadius = Math.max(radius, axisMin);
1187:
1188: double xv = length / 2.0 * Math.cos(radians);
1189: double yv = length / 2.0 * Math.sin(radians);
1190:
1191: float x = (float) (midX + (xv * (adjustedRadius - axisMin) / (axisMax - axisMin)));
1192: float y = (float) (midY + (yv * (adjustedRadius - axisMin) / (axisMax - axisMin)));
1193:
1194: int ix = Math.round(x);
1195: int iy = Math.round(y);
1196:
1197: Point p = new Point(ix, iy);
1198: return p;
1199:
1200: }
1201:
1202: }
|