1:
51:
52: package ;
53:
54: import ;
55: import ;
56: import ;
57: import ;
58: import ;
59: import ;
60: import ;
61: import ;
62: import ;
63: import ;
64: import ;
65: import ;
66: import ;
67: import ;
68: import ;
69: import ;
70: import ;
71: import ;
72: import ;
73: import ;
74: import ;
75:
76: import ;
77: import ;
78: import ;
79: import ;
80: import ;
81: import ;
82: import ;
83: import ;
84: import ;
85: import ;
86: import ;
87: import ;
88: import ;
89: import ;
90: import ;
91: import ;
92: import ;
93: import ;
94: import ;
95: import ;
96: import ;
97: import ;
98:
99:
103: public class PolarPlot extends Plot implements ValueAxisPlot, Zoomable,
104: RendererChangeListener, Cloneable, Serializable {
105:
106:
107: private static final long serialVersionUID = 3794383185924179525L;
108:
109:
110: private static final int MARGIN = 20;
111:
112:
113: private static final double ANNOTATION_MARGIN = 7.0;
114:
115:
120: public static final double DEFAULT_ANGLE_TICK_UNIT_SIZE = 45.0;
121:
122:
123: public static final Stroke DEFAULT_GRIDLINE_STROKE = new BasicStroke(
124: 0.5f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL,
125: 0.0f, new float[]{2.0f, 2.0f}, 0.0f);
126:
127:
128: public static final Paint DEFAULT_GRIDLINE_PAINT = Color.gray;
129:
130:
131: protected static ResourceBundle localizationResources
132: = ResourceBundle.getBundle("org.jfree.chart.plot.LocalizationBundle");
133:
134:
135: private List angleTicks;
136:
137:
138: private ValueAxis axis;
139:
140:
141: private XYDataset dataset;
142:
143:
147: private PolarItemRenderer renderer;
148:
149:
154: private TickUnit angleTickUnit;
155:
156:
157: private boolean angleLabelsVisible = true;
158:
159:
160: private Font angleLabelFont = new Font("SansSerif", Font.PLAIN, 12);
161:
162:
163: private transient Paint angleLabelPaint = Color.black;
164:
165:
166: private boolean angleGridlinesVisible;
167:
168:
169: private transient Stroke angleGridlineStroke;
170:
171:
172: private transient Paint angleGridlinePaint;
173:
174:
175: private boolean radiusGridlinesVisible;
176:
177:
178: private transient Stroke radiusGridlineStroke;
179:
180:
181: private transient Paint radiusGridlinePaint;
182:
183:
184: private List cornerTextItems = new ArrayList();
185:
186:
189: public PolarPlot() {
190: this(null, null, null);
191: }
192:
193:
200: public PolarPlot(XYDataset dataset,
201: ValueAxis radiusAxis,
202: PolarItemRenderer renderer) {
203:
204: super();
205:
206: this.dataset = dataset;
207: if (this.dataset != null) {
208: this.dataset.addChangeListener(this);
209: }
210: this.angleTickUnit = new NumberTickUnit(DEFAULT_ANGLE_TICK_UNIT_SIZE);
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:
241: public void addCornerTextItem(String text) {
242: if (text == null) {
243: throw new IllegalArgumentException("Null 'text' argument.");
244: }
245: this.cornerTextItems.add(text);
246: fireChangeEvent();
247: }
248:
249:
257: public void removeCornerTextItem(String text) {
258: boolean removed = this.cornerTextItems.remove(text);
259: if (removed) {
260: fireChangeEvent();
261: }
262: }
263:
264:
271: public void clearCornerTextItems() {
272: if (this.cornerTextItems.size() > 0) {
273: this.cornerTextItems.clear();
274: fireChangeEvent();
275: }
276: }
277:
278:
283: public String getPlotType() {
284: return PolarPlot.localizationResources.getString("Polar_Plot");
285: }
286:
287:
294: public ValueAxis getAxis() {
295: return this.axis;
296: }
297:
298:
304: public void setAxis(ValueAxis axis) {
305: if (axis != null) {
306: axis.setPlot(this);
307: }
308:
309:
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: fireChangeEvent();
320: }
321:
322:
329: public XYDataset getDataset() {
330: return this.dataset;
331: }
332:
333:
341: public void setDataset(XYDataset dataset) {
342:
343:
344: XYDataset existing = this.dataset;
345: if (existing != null) {
346: existing.removeChangeListener(this);
347: }
348:
349:
350: this.dataset = dataset;
351: if (this.dataset != null) {
352: setDatasetGroup(this.dataset.getGroup());
353: this.dataset.addChangeListener(this);
354: }
355:
356:
357: DatasetChangeEvent event = new DatasetChangeEvent(this, this.dataset);
358: datasetChanged(event);
359: }
360:
361:
368: public PolarItemRenderer getRenderer() {
369: return this.renderer;
370: }
371:
372:
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: fireChangeEvent();
392: }
393:
394:
402: public TickUnit getAngleTickUnit() {
403: return this.angleTickUnit;
404: }
405:
406:
414: public void setAngleTickUnit(TickUnit unit) {
415: if (unit == null) {
416: throw new IllegalArgumentException("Null 'unit' argument.");
417: }
418: this.angleTickUnit = unit;
419: fireChangeEvent();
420: }
421:
422:
429: public boolean isAngleLabelsVisible() {
430: return this.angleLabelsVisible;
431: }
432:
433:
441: public void setAngleLabelsVisible(boolean visible) {
442: if (this.angleLabelsVisible != visible) {
443: this.angleLabelsVisible = visible;
444: fireChangeEvent();
445: }
446: }
447:
448:
455: public Font getAngleLabelFont() {
456: return this.angleLabelFont;
457: }
458:
459:
467: public void setAngleLabelFont(Font font) {
468: if (font == null) {
469: throw new IllegalArgumentException("Null 'font' argument.");
470: }
471: this.angleLabelFont = font;
472: fireChangeEvent();
473: }
474:
475:
482: public Paint getAngleLabelPaint() {
483: return this.angleLabelPaint;
484: }
485:
486:
492: public void setAngleLabelPaint(Paint paint) {
493: if (paint == null) {
494: throw new IllegalArgumentException("Null 'paint' argument.");
495: }
496: this.angleLabelPaint = paint;
497: fireChangeEvent();
498: }
499:
500:
508: public boolean isAngleGridlinesVisible() {
509: return this.angleGridlinesVisible;
510: }
511:
512:
523: public void setAngleGridlinesVisible(boolean visible) {
524: if (this.angleGridlinesVisible != visible) {
525: this.angleGridlinesVisible = visible;
526: fireChangeEvent();
527: }
528: }
529:
530:
538: public Stroke getAngleGridlineStroke() {
539: return this.angleGridlineStroke;
540: }
541:
542:
552: public void setAngleGridlineStroke(Stroke stroke) {
553: this.angleGridlineStroke = stroke;
554: fireChangeEvent();
555: }
556:
557:
565: public Paint getAngleGridlinePaint() {
566: return this.angleGridlinePaint;
567: }
568:
569:
578: public void setAngleGridlinePaint(Paint paint) {
579: this.angleGridlinePaint = paint;
580: fireChangeEvent();
581: }
582:
583:
591: public boolean isRadiusGridlinesVisible() {
592: return this.radiusGridlinesVisible;
593: }
594:
595:
606: public void setRadiusGridlinesVisible(boolean visible) {
607: if (this.radiusGridlinesVisible != visible) {
608: this.radiusGridlinesVisible = visible;
609: fireChangeEvent();
610: }
611: }
612:
613:
621: public Stroke getRadiusGridlineStroke() {
622: return this.radiusGridlineStroke;
623: }
624:
625:
635: public void setRadiusGridlineStroke(Stroke stroke) {
636: this.radiusGridlineStroke = stroke;
637: fireChangeEvent();
638: }
639:
640:
648: public Paint getRadiusGridlinePaint() {
649: return this.radiusGridlinePaint;
650: }
651:
652:
662: public void setRadiusGridlinePaint(Paint paint) {
663: this.radiusGridlinePaint = paint;
664: fireChangeEvent();
665: }
666:
667:
674: protected List refreshAngleTicks() {
675: List ticks = new ArrayList();
676: for (double currentTickVal = 0.0; currentTickVal < 360.0;
677: currentTickVal += this.angleTickUnit.getSize()) {
678: NumberTick tick = new NumberTick(new Double(currentTickVal),
679: this.angleTickUnit.valueToString(currentTickVal),
680: TextAnchor.CENTER, TextAnchor.CENTER, 0.0);
681: ticks.add(tick);
682: }
683: return ticks;
684: }
685:
686:
706: public void draw(Graphics2D g2,
707: Rectangle2D area,
708: Point2D anchor,
709: PlotState parentState,
710: PlotRenderingInfo info) {
711:
712:
713: boolean b1 = (area.getWidth() <= MINIMUM_WIDTH_TO_DRAW);
714: boolean b2 = (area.getHeight() <= MINIMUM_HEIGHT_TO_DRAW);
715: if (b1 || b2) {
716: return;
717: }
718:
719:
720: if (info != null) {
721: info.setPlotArea(area);
722: }
723:
724:
725: RectangleInsets insets = getInsets();
726: insets.trim(area);
727:
728: Rectangle2D dataArea = area;
729: if (info != null) {
730: info.setDataArea(dataArea);
731: }
732:
733:
734: drawBackground(g2, dataArea);
735: double h = Math.min(dataArea.getWidth() / 2.0,
736: dataArea.getHeight() / 2.0) - MARGIN;
737: Rectangle2D quadrant = new Rectangle2D.Double(dataArea.getCenterX(),
738: dataArea.getCenterY(), h, h);
739: AxisState state = drawAxis(g2, area, quadrant);
740: if (this.renderer != null) {
741: Shape originalClip = g2.getClip();
742: Composite originalComposite = g2.getComposite();
743:
744: g2.clip(dataArea);
745: g2.setComposite(AlphaComposite.getInstance(
746: AlphaComposite.SRC_OVER, getForegroundAlpha()));
747:
748: this.angleTicks = refreshAngleTicks();
749: drawGridlines(g2, dataArea, this.angleTicks, state.getTicks());
750:
751:
752: render(g2, dataArea, info);
753:
754: g2.setClip(originalClip);
755: g2.setComposite(originalComposite);
756: }
757: drawOutline(g2, dataArea);
758: drawCornerTextItems(g2, dataArea);
759: }
760:
761:
767: protected void drawCornerTextItems(Graphics2D g2, Rectangle2D area) {
768: if (this.cornerTextItems.isEmpty()) {
769: return;
770: }
771:
772: g2.setColor(Color.black);
773: double width = 0.0;
774: double height = 0.0;
775: for (Iterator it = this.cornerTextItems.iterator(); it.hasNext();) {
776: String msg = (String) it.next();
777: FontMetrics fm = g2.getFontMetrics();
778: Rectangle2D bounds = TextUtilities.getTextBounds(msg, g2, fm);
779: width = Math.max(width, bounds.getWidth());
780: height += bounds.getHeight();
781: }
782:
783: double xadj = ANNOTATION_MARGIN * 2.0;
784: double yadj = ANNOTATION_MARGIN;
785: width += xadj;
786: height += yadj;
787:
788: double x = area.getMaxX() - width;
789: double y = area.getMaxY() - height;
790: g2.drawRect((int) x, (int) y, (int) width, (int) height);
791: x += ANNOTATION_MARGIN;
792: for (Iterator it = this.cornerTextItems.iterator(); it.hasNext();) {
793: String msg = (String) it.next();
794: Rectangle2D bounds = TextUtilities.getTextBounds(msg, g2,
795: g2.getFontMetrics());
796: y += bounds.getHeight();
797: g2.drawString(msg, (int) x, (int) y);
798: }
799: }
800:
801:
810: protected AxisState drawAxis(Graphics2D g2, Rectangle2D plotArea,
811: Rectangle2D dataArea) {
812: return this.axis.draw(g2, dataArea.getMinY(), plotArea, dataArea,
813: RectangleEdge.TOP, null);
814: }
815:
816:
825: protected void render(Graphics2D g2,
826: Rectangle2D dataArea,
827: PlotRenderingInfo info) {
828:
829:
830:
831: if (!DatasetUtilities.isEmptyOrNull(this.dataset)) {
832: int seriesCount = this.dataset.getSeriesCount();
833: for (int series = 0; series < seriesCount; series++) {
834: this.renderer.drawSeries(g2, dataArea, info, this,
835: this.dataset, series);
836: }
837: }
838: else {
839: drawNoDataMessage(g2, dataArea);
840: }
841: }
842:
843:
851: protected void drawGridlines(Graphics2D g2, Rectangle2D dataArea,
852: List angularTicks, List radialTicks) {
853:
854:
855: if (this.renderer == null) {
856: return;
857: }
858:
859:
860: if (isAngleGridlinesVisible()) {
861: Stroke gridStroke = getAngleGridlineStroke();
862: Paint gridPaint = getAngleGridlinePaint();
863: if ((gridStroke != null) && (gridPaint != null)) {
864: this.renderer.drawAngularGridLines(g2, this, angularTicks,
865: dataArea);
866: }
867: }
868:
869:
870: if (isRadiusGridlinesVisible()) {
871: Stroke gridStroke = getRadiusGridlineStroke();
872: Paint gridPaint = getRadiusGridlinePaint();
873: if ((gridStroke != null) && (gridPaint != null)) {
874: this.renderer.drawRadialGridLines(g2, this, this.axis,
875: radialTicks, dataArea);
876: }
877: }
878: }
879:
880:
885: public void zoom(double percent) {
886: if (percent > 0.0) {
887: double radius = getMaxRadius();
888: double scaledRadius = radius * percent;
889: this.axis.setUpperBound(scaledRadius);
890: getAxis().setAutoRange(false);
891: }
892: else {
893: getAxis().setAutoRange(true);
894: }
895: }
896:
897:
904: public Range getDataRange(ValueAxis axis) {
905: Range result = null;
906: if (this.dataset != null) {
907: result = Range.combine(result,
908: DatasetUtilities.findRangeBounds(this.dataset));
909: }
910: return result;
911: }
912:
913:
920: public void datasetChanged(DatasetChangeEvent event) {
921:
922: if (this.axis != null) {
923: this.axis.configure();
924: }
925:
926: if (getParent() != null) {
927: getParent().datasetChanged(event);
928: }
929: else {
930: super.datasetChanged(event);
931: }
932: }
933:
934:
941: public void rendererChanged(RendererChangeEvent event) {
942: fireChangeEvent();
943: }
944:
945:
951: public int getSeriesCount() {
952: int result = 0;
953:
954: if (this.dataset != null) {
955: result = this.dataset.getSeriesCount();
956: }
957: return result;
958: }
959:
960:
967: public LegendItemCollection getLegendItems() {
968: LegendItemCollection result = new LegendItemCollection();
969:
970:
971: if (this.dataset != null) {
972: if (this.renderer != null) {
973: int seriesCount = this.dataset.getSeriesCount();
974: for (int i = 0; i < seriesCount; i++) {
975: LegendItem item = this.renderer.getLegendItem(i);
976: result.add(item);
977: }
978: }
979: }
980: return result;
981: }
982:
983:
990: public boolean equals(Object obj) {
991: if (obj == this) {
992: return true;
993: }
994: if (!(obj instanceof PolarPlot)) {
995: return false;
996: }
997: PolarPlot that = (PolarPlot) obj;
998: if (!ObjectUtilities.equal(this.axis, that.axis)) {
999: return false;
1000: }
1001: if (!ObjectUtilities.equal(this.renderer, that.renderer)) {
1002: return false;
1003: }
1004: if (!this.angleTickUnit.equals(that.angleTickUnit)) {
1005: return false;
1006: }
1007: if (this.angleGridlinesVisible != that.angleGridlinesVisible) {
1008: return false;
1009: }
1010: if (this.angleLabelsVisible != that.angleLabelsVisible) {
1011: return false;
1012: }
1013: if (!this.angleLabelFont.equals(that.angleLabelFont)) {
1014: return false;
1015: }
1016: if (!PaintUtilities.equal(this.angleLabelPaint, that.angleLabelPaint)) {
1017: return false;
1018: }
1019: if (!ObjectUtilities.equal(this.angleGridlineStroke,
1020: that.angleGridlineStroke)) {
1021: return false;
1022: }
1023: if (!PaintUtilities.equal(
1024: this.angleGridlinePaint, that.angleGridlinePaint
1025: )) {
1026: return false;
1027: }
1028: if (this.radiusGridlinesVisible != that.radiusGridlinesVisible) {
1029: return false;
1030: }
1031: if (!ObjectUtilities.equal(this.radiusGridlineStroke,
1032: that.radiusGridlineStroke)) {
1033: return false;
1034: }
1035: if (!PaintUtilities.equal(this.radiusGridlinePaint,
1036: that.radiusGridlinePaint)) {
1037: return false;
1038: }
1039: if (!this.cornerTextItems.equals(that.cornerTextItems)) {
1040: return false;
1041: }
1042: return super.equals(obj);
1043: }
1044:
1045:
1053: public Object clone() throws CloneNotSupportedException {
1054:
1055: PolarPlot clone = (PolarPlot) super.clone();
1056: if (this.axis != null) {
1057: clone.axis = (ValueAxis) ObjectUtilities.clone(this.axis);
1058: clone.axis.setPlot(clone);
1059: clone.axis.addChangeListener(clone);
1060: }
1061:
1062: if (clone.dataset != null) {
1063: clone.dataset.addChangeListener(clone);
1064: }
1065:
1066: if (this.renderer != null) {
1067: clone.renderer
1068: = (PolarItemRenderer) ObjectUtilities.clone(this.renderer);
1069: }
1070:
1071: clone.cornerTextItems = new ArrayList(this.cornerTextItems);
1072:
1073: return clone;
1074: }
1075:
1076:
1083: private void writeObject(ObjectOutputStream stream) throws IOException {
1084: stream.defaultWriteObject();
1085: SerialUtilities.writeStroke(this.angleGridlineStroke, stream);
1086: SerialUtilities.writePaint(this.angleGridlinePaint, stream);
1087: SerialUtilities.writeStroke(this.radiusGridlineStroke, stream);
1088: SerialUtilities.writePaint(this.radiusGridlinePaint, stream);
1089: SerialUtilities.writePaint(this.angleLabelPaint, stream);
1090: }
1091:
1092:
1100: private void readObject(ObjectInputStream stream)
1101: throws IOException, ClassNotFoundException {
1102:
1103: stream.defaultReadObject();
1104: this.angleGridlineStroke = SerialUtilities.readStroke(stream);
1105: this.angleGridlinePaint = SerialUtilities.readPaint(stream);
1106: this.radiusGridlineStroke = SerialUtilities.readStroke(stream);
1107: this.radiusGridlinePaint = SerialUtilities.readPaint(stream);
1108: this.angleLabelPaint = SerialUtilities.readPaint(stream);
1109:
1110: if (this.axis != null) {
1111: this.axis.setPlot(this);
1112: this.axis.addChangeListener(this);
1113: }
1114:
1115: if (this.dataset != null) {
1116: this.dataset.addChangeListener(this);
1117: }
1118: }
1119:
1120:
1128: public void zoomDomainAxes(double factor, PlotRenderingInfo state,
1129: Point2D source) {
1130:
1131: }
1132:
1133:
1144: public void zoomDomainAxes(double factor, PlotRenderingInfo state,
1145: Point2D source, boolean useAnchor) {
1146:
1147: }
1148:
1149:
1158: public void zoomDomainAxes(double lowerPercent, double upperPercent,
1159: PlotRenderingInfo state, Point2D source) {
1160:
1161: }
1162:
1163:
1170: public void zoomRangeAxes(double factor, PlotRenderingInfo state,
1171: Point2D source) {
1172: zoom(factor);
1173: }
1174:
1175:
1187: public void zoomRangeAxes(double factor, PlotRenderingInfo info,
1188: Point2D source, boolean useAnchor) {
1189:
1190: if (useAnchor) {
1191:
1192:
1193: double sourceX = source.getX();
1194: double anchorX = this.axis.java2DToValue(sourceX,
1195: info.getDataArea(), RectangleEdge.BOTTOM);
1196: this.axis.resizeRange(factor, anchorX);
1197: }
1198: else {
1199: this.axis.resizeRange(factor);
1200: }
1201:
1202: }
1203:
1204:
1212: public void zoomRangeAxes(double lowerPercent, double upperPercent,
1213: PlotRenderingInfo state, Point2D source) {
1214: zoom((upperPercent + lowerPercent) / 2.0);
1215: }
1216:
1217:
1222: public boolean isDomainZoomable() {
1223: return false;
1224: }
1225:
1226:
1231: public boolean isRangeZoomable() {
1232: return true;
1233: }
1234:
1235:
1240: public PlotOrientation getOrientation() {
1241: return PlotOrientation.HORIZONTAL;
1242: }
1243:
1244:
1249: public double getMaxRadius() {
1250: return this.axis.getUpperBound();
1251: }
1252:
1253:
1264: public Point translateValueThetaRadiusToJava2D(double angleDegrees,
1265: double radius,
1266: Rectangle2D dataArea) {
1267:
1268: double radians = Math.toRadians(angleDegrees - 90.0);
1269:
1270: double minx = dataArea.getMinX() + MARGIN;
1271: double maxx = dataArea.getMaxX() - MARGIN;
1272: double miny = dataArea.getMinY() + MARGIN;
1273: double maxy = dataArea.getMaxY() - MARGIN;
1274:
1275: double lengthX = maxx - minx;
1276: double lengthY = maxy - miny;
1277: double length = Math.min(lengthX, lengthY);
1278:
1279: double midX = minx + lengthX / 2.0;
1280: double midY = miny + lengthY / 2.0;
1281:
1282: double axisMin = this.axis.getLowerBound();
1283: double axisMax = getMaxRadius();
1284: double adjustedRadius = Math.max(radius, axisMin);
1285:
1286: double xv = length / 2.0 * Math.cos(radians);
1287: double yv = length / 2.0 * Math.sin(radians);
1288:
1289: float x = (float) (midX + (xv * (adjustedRadius - axisMin)
1290: / (axisMax - axisMin)));
1291: float y = (float) (midY + (yv * (adjustedRadius - axisMin)
1292: / (axisMax - axisMin)));
1293:
1294: int ix = Math.round(x);
1295: int iy = Math.round(y);
1296:
1297: Point p = new Point(ix, iy);
1298: return p;
1299:
1300: }
1301:
1302: }