001 /* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2009, by Object Refinery Limited and Contributors.
006 *
007 * Project Info: http://www.jfree.org/jfreechart/index.html
008 *
009 * This library is free software; you can redistribute it and/or modify it
010 * under the terms of the GNU Lesser General Public License as published by
011 * the Free Software Foundation; either version 2.1 of the License, or
012 * (at your option) any later version.
013 *
014 * This library is distributed in the hope that it will be useful, but
015 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
016 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
017 * License for more details.
018 *
019 * You should have received a copy of the GNU Lesser General Public
020 * License along with this library; if not, write to the Free Software
021 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
022 * USA.
023 *
024 * [Java is a trademark or registered trademark of Sun Microsystems, Inc.
025 * in the United States and other countries.]
026 *
027 * -----------------
028 * CategoryAxis.java
029 * -----------------
030 * (C) Copyright 2000-2009, by Object Refinery Limited and Contributors.
031 *
032 * Original Author: David Gilbert;
033 * Contributor(s): Pady Srinivasan (patch 1217634);
034 * Peter Kolb (patches 2497611 and 2603321);
035 *
036 * Changes
037 * -------
038 * 21-Aug-2001 : Added standard header. Fixed DOS encoding problem (DG);
039 * 18-Sep-2001 : Updated header (DG);
040 * 04-Dec-2001 : Changed constructors to protected, and tidied up default
041 * values (DG);
042 * 19-Apr-2002 : Updated import statements (DG);
043 * 05-Sep-2002 : Updated constructor for changes in Axis class (DG);
044 * 06-Nov-2002 : Moved margins from the CategoryPlot class (DG);
045 * 08-Nov-2002 : Moved to new package com.jrefinery.chart.axis (DG);
046 * 22-Jan-2002 : Removed monolithic constructor (DG);
047 * 26-Mar-2003 : Implemented Serializable (DG);
048 * 09-May-2003 : Merged HorizontalCategoryAxis and VerticalCategoryAxis into
049 * this class (DG);
050 * 13-Aug-2003 : Implemented Cloneable (DG);
051 * 29-Oct-2003 : Added workaround for font alignment in PDF output (DG);
052 * 05-Nov-2003 : Fixed serialization bug (DG);
053 * 26-Nov-2003 : Added category label offset (DG);
054 * 06-Jan-2004 : Moved axis line attributes to Axis class, rationalised
055 * category label position attributes (DG);
056 * 07-Jan-2004 : Added new implementation for linewrapping of category
057 * labels (DG);
058 * 17-Feb-2004 : Moved deprecated code to bottom of source file (DG);
059 * 10-Mar-2004 : Changed Dimension --> Dimension2D in text classes (DG);
060 * 16-Mar-2004 : Added support for tooltips on category labels (DG);
061 * 01-Apr-2004 : Changed java.awt.geom.Dimension2D to org.jfree.ui.Size2D
062 * because of JDK bug 4976448 which persists on JDK 1.3.1 (DG);
063 * 03-Sep-2004 : Added 'maxCategoryLabelLines' attribute (DG);
064 * 04-Oct-2004 : Renamed ShapeUtils --> ShapeUtilities (DG);
065 * 11-Jan-2005 : Removed deprecated methods in preparation for 1.0.0
066 * release (DG);
067 * 21-Jan-2005 : Modified return type for RectangleAnchor.coordinates()
068 * method (DG);
069 * 21-Apr-2005 : Replaced Insets with RectangleInsets (DG);
070 * 26-Apr-2005 : Removed LOGGER (DG);
071 * 08-Jun-2005 : Fixed bug in axis layout (DG);
072 * 22-Nov-2005 : Added a method to access the tool tip text for a category
073 * label (DG);
074 * 23-Nov-2005 : Added per-category font and paint options - see patch
075 * 1217634 (DG);
076 * ------------- JFreeChart 1.0.x ---------------------------------------------
077 * 11-Jan-2006 : Fixed null pointer exception in drawCategoryLabels - see bug
078 * 1403043 (DG);
079 * 18-Aug-2006 : Fix for bug drawing category labels, thanks to Adriaan
080 * Joubert (1277726) (DG);
081 * 02-Oct-2006 : Updated category label entity (DG);
082 * 30-Oct-2006 : Updated refreshTicks() method to account for possibility of
083 * multiple domain axes (DG);
084 * 07-Mar-2007 : Fixed bug in axis label positioning (DG);
085 * 27-Sep-2007 : Added getCategorySeriesMiddle() method (DG);
086 * 21-Nov-2007 : Fixed performance bug noted by FindBugs in the
087 * equalPaintMaps() method (DG);
088 * 23-Apr-2008 : Fixed bug 1942059, bad use of insets in
089 * calculateTextBlockWidth() (DG);
090 * 26-Jun-2008 : Added new getCategoryMiddle() method (DG);
091 * 27-Oct-2008 : Set font on Graphics2D when creating category labels (DG);
092 * 14-Jan-2009 : Added new variant of getCategorySeriesMiddle() to make it
093 * simpler for renderers with hidden series (PK);
094 * 19-Mar-2009 : Added entity support - see patch 2603321 by Peter Kolb (DG);
095 * 16-Apr-2009 : Added tick mark drawing (DG);
096 *
097 */
098
099 package org.jfree.chart.axis;
100
101 import java.awt.Font;
102 import java.awt.Graphics2D;
103 import java.awt.Paint;
104 import java.awt.Shape;
105 import java.awt.geom.Line2D;
106 import java.awt.geom.Point2D;
107 import java.awt.geom.Rectangle2D;
108 import java.io.IOException;
109 import java.io.ObjectInputStream;
110 import java.io.ObjectOutputStream;
111 import java.io.Serializable;
112 import java.util.HashMap;
113 import java.util.Iterator;
114 import java.util.List;
115 import java.util.Map;
116 import java.util.Set;
117
118 import org.jfree.chart.entity.CategoryLabelEntity;
119 import org.jfree.chart.entity.EntityCollection;
120 import org.jfree.chart.event.AxisChangeEvent;
121 import org.jfree.chart.plot.CategoryPlot;
122 import org.jfree.chart.plot.Plot;
123 import org.jfree.chart.plot.PlotRenderingInfo;
124 import org.jfree.data.category.CategoryDataset;
125 import org.jfree.io.SerialUtilities;
126 import org.jfree.text.G2TextMeasurer;
127 import org.jfree.text.TextBlock;
128 import org.jfree.text.TextUtilities;
129 import org.jfree.ui.RectangleAnchor;
130 import org.jfree.ui.RectangleEdge;
131 import org.jfree.ui.RectangleInsets;
132 import org.jfree.ui.Size2D;
133 import org.jfree.util.ObjectUtilities;
134 import org.jfree.util.PaintUtilities;
135 import org.jfree.util.ShapeUtilities;
136
137 /**
138 * An axis that displays categories.
139 */
140 public class CategoryAxis extends Axis implements Cloneable, Serializable {
141
142 /** For serialization. */
143 private static final long serialVersionUID = 5886554608114265863L;
144
145 /**
146 * The default margin for the axis (used for both lower and upper margins).
147 */
148 public static final double DEFAULT_AXIS_MARGIN = 0.05;
149
150 /**
151 * The default margin between categories (a percentage of the overall axis
152 * length).
153 */
154 public static final double DEFAULT_CATEGORY_MARGIN = 0.20;
155
156 /** The amount of space reserved at the start of the axis. */
157 private double lowerMargin;
158
159 /** The amount of space reserved at the end of the axis. */
160 private double upperMargin;
161
162 /** The amount of space reserved between categories. */
163 private double categoryMargin;
164
165 /** The maximum number of lines for category labels. */
166 private int maximumCategoryLabelLines;
167
168 /**
169 * A ratio that is multiplied by the width of one category to determine the
170 * maximum label width.
171 */
172 private float maximumCategoryLabelWidthRatio;
173
174 /** The category label offset. */
175 private int categoryLabelPositionOffset;
176
177 /**
178 * A structure defining the category label positions for each axis
179 * location.
180 */
181 private CategoryLabelPositions categoryLabelPositions;
182
183 /** Storage for tick label font overrides (if any). */
184 private Map tickLabelFontMap;
185
186 /** Storage for tick label paint overrides (if any). */
187 private transient Map tickLabelPaintMap;
188
189 /** Storage for the category label tooltips (if any). */
190 private Map categoryLabelToolTips;
191
192 /**
193 * Creates a new category axis with no label.
194 */
195 public CategoryAxis() {
196 this(null);
197 }
198
199 /**
200 * Constructs a category axis, using default values where necessary.
201 *
202 * @param label the axis label (<code>null</code> permitted).
203 */
204 public CategoryAxis(String label) {
205
206 super(label);
207
208 this.lowerMargin = DEFAULT_AXIS_MARGIN;
209 this.upperMargin = DEFAULT_AXIS_MARGIN;
210 this.categoryMargin = DEFAULT_CATEGORY_MARGIN;
211 this.maximumCategoryLabelLines = 1;
212 this.maximumCategoryLabelWidthRatio = 0.0f;
213
214 this.categoryLabelPositionOffset = 4;
215 this.categoryLabelPositions = CategoryLabelPositions.STANDARD;
216 this.tickLabelFontMap = new HashMap();
217 this.tickLabelPaintMap = new HashMap();
218 this.categoryLabelToolTips = new HashMap();
219
220 }
221
222 /**
223 * Returns the lower margin for the axis.
224 *
225 * @return The margin.
226 *
227 * @see #getUpperMargin()
228 * @see #setLowerMargin(double)
229 */
230 public double getLowerMargin() {
231 return this.lowerMargin;
232 }
233
234 /**
235 * Sets the lower margin for the axis and sends an {@link AxisChangeEvent}
236 * to all registered listeners.
237 *
238 * @param margin the margin as a percentage of the axis length (for
239 * example, 0.05 is five percent).
240 *
241 * @see #getLowerMargin()
242 */
243 public void setLowerMargin(double margin) {
244 this.lowerMargin = margin;
245 notifyListeners(new AxisChangeEvent(this));
246 }
247
248 /**
249 * Returns the upper margin for the axis.
250 *
251 * @return The margin.
252 *
253 * @see #getLowerMargin()
254 * @see #setUpperMargin(double)
255 */
256 public double getUpperMargin() {
257 return this.upperMargin;
258 }
259
260 /**
261 * Sets the upper margin for the axis and sends an {@link AxisChangeEvent}
262 * to all registered listeners.
263 *
264 * @param margin the margin as a percentage of the axis length (for
265 * example, 0.05 is five percent).
266 *
267 * @see #getUpperMargin()
268 */
269 public void setUpperMargin(double margin) {
270 this.upperMargin = margin;
271 notifyListeners(new AxisChangeEvent(this));
272 }
273
274 /**
275 * Returns the category margin.
276 *
277 * @return The margin.
278 *
279 * @see #setCategoryMargin(double)
280 */
281 public double getCategoryMargin() {
282 return this.categoryMargin;
283 }
284
285 /**
286 * Sets the category margin and sends an {@link AxisChangeEvent} to all
287 * registered listeners. The overall category margin is distributed over
288 * N-1 gaps, where N is the number of categories on the axis.
289 *
290 * @param margin the margin as a percentage of the axis length (for
291 * example, 0.05 is five percent).
292 *
293 * @see #getCategoryMargin()
294 */
295 public void setCategoryMargin(double margin) {
296 this.categoryMargin = margin;
297 notifyListeners(new AxisChangeEvent(this));
298 }
299
300 /**
301 * Returns the maximum number of lines to use for each category label.
302 *
303 * @return The maximum number of lines.
304 *
305 * @see #setMaximumCategoryLabelLines(int)
306 */
307 public int getMaximumCategoryLabelLines() {
308 return this.maximumCategoryLabelLines;
309 }
310
311 /**
312 * Sets the maximum number of lines to use for each category label and
313 * sends an {@link AxisChangeEvent} to all registered listeners.
314 *
315 * @param lines the maximum number of lines.
316 *
317 * @see #getMaximumCategoryLabelLines()
318 */
319 public void setMaximumCategoryLabelLines(int lines) {
320 this.maximumCategoryLabelLines = lines;
321 notifyListeners(new AxisChangeEvent(this));
322 }
323
324 /**
325 * Returns the category label width ratio.
326 *
327 * @return The ratio.
328 *
329 * @see #setMaximumCategoryLabelWidthRatio(float)
330 */
331 public float getMaximumCategoryLabelWidthRatio() {
332 return this.maximumCategoryLabelWidthRatio;
333 }
334
335 /**
336 * Sets the maximum category label width ratio and sends an
337 * {@link AxisChangeEvent} to all registered listeners.
338 *
339 * @param ratio the ratio.
340 *
341 * @see #getMaximumCategoryLabelWidthRatio()
342 */
343 public void setMaximumCategoryLabelWidthRatio(float ratio) {
344 this.maximumCategoryLabelWidthRatio = ratio;
345 notifyListeners(new AxisChangeEvent(this));
346 }
347
348 /**
349 * Returns the offset between the axis and the category labels (before
350 * label positioning is taken into account).
351 *
352 * @return The offset (in Java2D units).
353 *
354 * @see #setCategoryLabelPositionOffset(int)
355 */
356 public int getCategoryLabelPositionOffset() {
357 return this.categoryLabelPositionOffset;
358 }
359
360 /**
361 * Sets the offset between the axis and the category labels (before label
362 * positioning is taken into account).
363 *
364 * @param offset the offset (in Java2D units).
365 *
366 * @see #getCategoryLabelPositionOffset()
367 */
368 public void setCategoryLabelPositionOffset(int offset) {
369 this.categoryLabelPositionOffset = offset;
370 notifyListeners(new AxisChangeEvent(this));
371 }
372
373 /**
374 * Returns the category label position specification (this contains label
375 * positioning info for all four possible axis locations).
376 *
377 * @return The positions (never <code>null</code>).
378 *
379 * @see #setCategoryLabelPositions(CategoryLabelPositions)
380 */
381 public CategoryLabelPositions getCategoryLabelPositions() {
382 return this.categoryLabelPositions;
383 }
384
385 /**
386 * Sets the category label position specification for the axis and sends an
387 * {@link AxisChangeEvent} to all registered listeners.
388 *
389 * @param positions the positions (<code>null</code> not permitted).
390 *
391 * @see #getCategoryLabelPositions()
392 */
393 public void setCategoryLabelPositions(CategoryLabelPositions positions) {
394 if (positions == null) {
395 throw new IllegalArgumentException("Null 'positions' argument.");
396 }
397 this.categoryLabelPositions = positions;
398 notifyListeners(new AxisChangeEvent(this));
399 }
400
401 /**
402 * Returns the font for the tick label for the given category.
403 *
404 * @param category the category (<code>null</code> not permitted).
405 *
406 * @return The font (never <code>null</code>).
407 *
408 * @see #setTickLabelFont(Comparable, Font)
409 */
410 public Font getTickLabelFont(Comparable category) {
411 if (category == null) {
412 throw new IllegalArgumentException("Null 'category' argument.");
413 }
414 Font result = (Font) this.tickLabelFontMap.get(category);
415 // if there is no specific font, use the general one...
416 if (result == null) {
417 result = getTickLabelFont();
418 }
419 return result;
420 }
421
422 /**
423 * Sets the font for the tick label for the specified category and sends
424 * an {@link AxisChangeEvent} to all registered listeners.
425 *
426 * @param category the category (<code>null</code> not permitted).
427 * @param font the font (<code>null</code> permitted).
428 *
429 * @see #getTickLabelFont(Comparable)
430 */
431 public void setTickLabelFont(Comparable category, Font font) {
432 if (category == null) {
433 throw new IllegalArgumentException("Null 'category' argument.");
434 }
435 if (font == null) {
436 this.tickLabelFontMap.remove(category);
437 }
438 else {
439 this.tickLabelFontMap.put(category, font);
440 }
441 notifyListeners(new AxisChangeEvent(this));
442 }
443
444 /**
445 * Returns the paint for the tick label for the given category.
446 *
447 * @param category the category (<code>null</code> not permitted).
448 *
449 * @return The paint (never <code>null</code>).
450 *
451 * @see #setTickLabelPaint(Paint)
452 */
453 public Paint getTickLabelPaint(Comparable category) {
454 if (category == null) {
455 throw new IllegalArgumentException("Null 'category' argument.");
456 }
457 Paint result = (Paint) this.tickLabelPaintMap.get(category);
458 // if there is no specific paint, use the general one...
459 if (result == null) {
460 result = getTickLabelPaint();
461 }
462 return result;
463 }
464
465 /**
466 * Sets the paint for the tick label for the specified category and sends
467 * an {@link AxisChangeEvent} to all registered listeners.
468 *
469 * @param category the category (<code>null</code> not permitted).
470 * @param paint the paint (<code>null</code> permitted).
471 *
472 * @see #getTickLabelPaint(Comparable)
473 */
474 public void setTickLabelPaint(Comparable category, Paint paint) {
475 if (category == null) {
476 throw new IllegalArgumentException("Null 'category' argument.");
477 }
478 if (paint == null) {
479 this.tickLabelPaintMap.remove(category);
480 }
481 else {
482 this.tickLabelPaintMap.put(category, paint);
483 }
484 notifyListeners(new AxisChangeEvent(this));
485 }
486
487 /**
488 * Adds a tooltip to the specified category and sends an
489 * {@link AxisChangeEvent} to all registered listeners.
490 *
491 * @param category the category (<code>null<code> not permitted).
492 * @param tooltip the tooltip text (<code>null</code> permitted).
493 *
494 * @see #removeCategoryLabelToolTip(Comparable)
495 */
496 public void addCategoryLabelToolTip(Comparable category, String tooltip) {
497 if (category == null) {
498 throw new IllegalArgumentException("Null 'category' argument.");
499 }
500 this.categoryLabelToolTips.put(category, tooltip);
501 notifyListeners(new AxisChangeEvent(this));
502 }
503
504 /**
505 * Returns the tool tip text for the label belonging to the specified
506 * category.
507 *
508 * @param category the category (<code>null</code> not permitted).
509 *
510 * @return The tool tip text (possibly <code>null</code>).
511 *
512 * @see #addCategoryLabelToolTip(Comparable, String)
513 * @see #removeCategoryLabelToolTip(Comparable)
514 */
515 public String getCategoryLabelToolTip(Comparable category) {
516 if (category == null) {
517 throw new IllegalArgumentException("Null 'category' argument.");
518 }
519 return (String) this.categoryLabelToolTips.get(category);
520 }
521
522 /**
523 * Removes the tooltip for the specified category and sends an
524 * {@link AxisChangeEvent} to all registered listeners.
525 *
526 * @param category the category (<code>null<code> not permitted).
527 *
528 * @see #addCategoryLabelToolTip(Comparable, String)
529 * @see #clearCategoryLabelToolTips()
530 */
531 public void removeCategoryLabelToolTip(Comparable category) {
532 if (category == null) {
533 throw new IllegalArgumentException("Null 'category' argument.");
534 }
535 this.categoryLabelToolTips.remove(category);
536 notifyListeners(new AxisChangeEvent(this));
537 }
538
539 /**
540 * Clears the category label tooltips and sends an {@link AxisChangeEvent}
541 * to all registered listeners.
542 *
543 * @see #addCategoryLabelToolTip(Comparable, String)
544 * @see #removeCategoryLabelToolTip(Comparable)
545 */
546 public void clearCategoryLabelToolTips() {
547 this.categoryLabelToolTips.clear();
548 notifyListeners(new AxisChangeEvent(this));
549 }
550
551 /**
552 * Returns the Java 2D coordinate for a category.
553 *
554 * @param anchor the anchor point.
555 * @param category the category index.
556 * @param categoryCount the category count.
557 * @param area the data area.
558 * @param edge the location of the axis.
559 *
560 * @return The coordinate.
561 */
562 public double getCategoryJava2DCoordinate(CategoryAnchor anchor,
563 int category,
564 int categoryCount,
565 Rectangle2D area,
566 RectangleEdge edge) {
567
568 double result = 0.0;
569 if (anchor == CategoryAnchor.START) {
570 result = getCategoryStart(category, categoryCount, area, edge);
571 }
572 else if (anchor == CategoryAnchor.MIDDLE) {
573 result = getCategoryMiddle(category, categoryCount, area, edge);
574 }
575 else if (anchor == CategoryAnchor.END) {
576 result = getCategoryEnd(category, categoryCount, area, edge);
577 }
578 return result;
579
580 }
581
582 /**
583 * Returns the starting coordinate for the specified category.
584 *
585 * @param category the category.
586 * @param categoryCount the number of categories.
587 * @param area the data area.
588 * @param edge the axis location.
589 *
590 * @return The coordinate.
591 *
592 * @see #getCategoryMiddle(int, int, Rectangle2D, RectangleEdge)
593 * @see #getCategoryEnd(int, int, Rectangle2D, RectangleEdge)
594 */
595 public double getCategoryStart(int category, int categoryCount,
596 Rectangle2D area,
597 RectangleEdge edge) {
598
599 double result = 0.0;
600 if ((edge == RectangleEdge.TOP) || (edge == RectangleEdge.BOTTOM)) {
601 result = area.getX() + area.getWidth() * getLowerMargin();
602 }
603 else if ((edge == RectangleEdge.LEFT)
604 || (edge == RectangleEdge.RIGHT)) {
605 result = area.getMinY() + area.getHeight() * getLowerMargin();
606 }
607
608 double categorySize = calculateCategorySize(categoryCount, area, edge);
609 double categoryGapWidth = calculateCategoryGapSize(categoryCount, area,
610 edge);
611
612 result = result + category * (categorySize + categoryGapWidth);
613 return result;
614
615 }
616
617 /**
618 * Returns the middle coordinate for the specified category.
619 *
620 * @param category the category.
621 * @param categoryCount the number of categories.
622 * @param area the data area.
623 * @param edge the axis location.
624 *
625 * @return The coordinate.
626 *
627 * @see #getCategoryStart(int, int, Rectangle2D, RectangleEdge)
628 * @see #getCategoryEnd(int, int, Rectangle2D, RectangleEdge)
629 */
630 public double getCategoryMiddle(int category, int categoryCount,
631 Rectangle2D area, RectangleEdge edge) {
632
633 if (category < 0 || category >= categoryCount) {
634 throw new IllegalArgumentException("Invalid category index: "
635 + category);
636 }
637 return getCategoryStart(category, categoryCount, area, edge)
638 + calculateCategorySize(categoryCount, area, edge) / 2;
639
640 }
641
642 /**
643 * Returns the end coordinate for the specified category.
644 *
645 * @param category the category.
646 * @param categoryCount the number of categories.
647 * @param area the data area.
648 * @param edge the axis location.
649 *
650 * @return The coordinate.
651 *
652 * @see #getCategoryStart(int, int, Rectangle2D, RectangleEdge)
653 * @see #getCategoryMiddle(int, int, Rectangle2D, RectangleEdge)
654 */
655 public double getCategoryEnd(int category, int categoryCount,
656 Rectangle2D area, RectangleEdge edge) {
657
658 return getCategoryStart(category, categoryCount, area, edge)
659 + calculateCategorySize(categoryCount, area, edge);
660
661 }
662
663 /**
664 * A convenience method that returns the axis coordinate for the centre of
665 * a category.
666 *
667 * @param category the category key (<code>null</code> not permitted).
668 * @param categories the categories (<code>null</code> not permitted).
669 * @param area the data area (<code>null</code> not permitted).
670 * @param edge the edge along which the axis lies (<code>null</code> not
671 * permitted).
672 *
673 * @return The centre coordinate.
674 *
675 * @since 1.0.11
676 *
677 * @see #getCategorySeriesMiddle(Comparable, Comparable, CategoryDataset,
678 * double, Rectangle2D, RectangleEdge)
679 */
680 public double getCategoryMiddle(Comparable category,
681 List categories, Rectangle2D area, RectangleEdge edge) {
682 if (categories == null) {
683 throw new IllegalArgumentException("Null 'categories' argument.");
684 }
685 int categoryIndex = categories.indexOf(category);
686 int categoryCount = categories.size();
687 return getCategoryMiddle(categoryIndex, categoryCount, area, edge);
688 }
689
690 /**
691 * Returns the middle coordinate (in Java2D space) for a series within a
692 * category.
693 *
694 * @param category the category (<code>null</code> not permitted).
695 * @param seriesKey the series key (<code>null</code> not permitted).
696 * @param dataset the dataset (<code>null</code> not permitted).
697 * @param itemMargin the item margin (0.0 <= itemMargin < 1.0);
698 * @param area the area (<code>null</code> not permitted).
699 * @param edge the edge (<code>null</code> not permitted).
700 *
701 * @return The coordinate in Java2D space.
702 *
703 * @since 1.0.7
704 */
705 public double getCategorySeriesMiddle(Comparable category,
706 Comparable seriesKey, CategoryDataset dataset, double itemMargin,
707 Rectangle2D area, RectangleEdge edge) {
708
709 int categoryIndex = dataset.getColumnIndex(category);
710 int categoryCount = dataset.getColumnCount();
711 int seriesIndex = dataset.getRowIndex(seriesKey);
712 int seriesCount = dataset.getRowCount();
713 double start = getCategoryStart(categoryIndex, categoryCount, area,
714 edge);
715 double end = getCategoryEnd(categoryIndex, categoryCount, area, edge);
716 double width = end - start;
717 if (seriesCount == 1) {
718 return start + width / 2.0;
719 }
720 else {
721 double gap = (width * itemMargin) / (seriesCount - 1);
722 double ww = (width * (1 - itemMargin)) / seriesCount;
723 return start + (seriesIndex * (ww + gap)) + ww / 2.0;
724 }
725 }
726
727 /**
728 * Returns the middle coordinate (in Java2D space) for a series within a
729 * category.
730 *
731 * @param categoryIndex the category index.
732 * @param categoryCount the category count.
733 * @param seriesIndex the series index.
734 * @param seriesCount the series count.
735 * @param itemMargin the item margin (0.0 <= itemMargin < 1.0);
736 * @param area the area (<code>null</code> not permitted).
737 * @param edge the edge (<code>null</code> not permitted).
738 *
739 * @return The coordinate in Java2D space.
740 *
741 * @since 1.0.13
742 */
743 public double getCategorySeriesMiddle(int categoryIndex, int categoryCount,
744 int seriesIndex, int seriesCount, double itemMargin,
745 Rectangle2D area, RectangleEdge edge) {
746
747 double start = getCategoryStart(categoryIndex, categoryCount, area,
748 edge);
749 double end = getCategoryEnd(categoryIndex, categoryCount, area, edge);
750 double width = end - start;
751 if (seriesCount == 1) {
752 return start + width / 2.0;
753 }
754 else {
755 double gap = (width * itemMargin) / (seriesCount - 1);
756 double ww = (width * (1 - itemMargin)) / seriesCount;
757 return start + (seriesIndex * (ww + gap)) + ww / 2.0;
758 }
759 }
760
761 /**
762 * Calculates the size (width or height, depending on the location of the
763 * axis) of a category.
764 *
765 * @param categoryCount the number of categories.
766 * @param area the area within which the categories will be drawn.
767 * @param edge the axis location.
768 *
769 * @return The category size.
770 */
771 protected double calculateCategorySize(int categoryCount, Rectangle2D area,
772 RectangleEdge edge) {
773
774 double result = 0.0;
775 double available = 0.0;
776
777 if ((edge == RectangleEdge.TOP) || (edge == RectangleEdge.BOTTOM)) {
778 available = area.getWidth();
779 }
780 else if ((edge == RectangleEdge.LEFT)
781 || (edge == RectangleEdge.RIGHT)) {
782 available = area.getHeight();
783 }
784 if (categoryCount > 1) {
785 result = available * (1 - getLowerMargin() - getUpperMargin()
786 - getCategoryMargin());
787 result = result / categoryCount;
788 }
789 else {
790 result = available * (1 - getLowerMargin() - getUpperMargin());
791 }
792 return result;
793
794 }
795
796 /**
797 * Calculates the size (width or height, depending on the location of the
798 * axis) of a category gap.
799 *
800 * @param categoryCount the number of categories.
801 * @param area the area within which the categories will be drawn.
802 * @param edge the axis location.
803 *
804 * @return The category gap width.
805 */
806 protected double calculateCategoryGapSize(int categoryCount,
807 Rectangle2D area,
808 RectangleEdge edge) {
809
810 double result = 0.0;
811 double available = 0.0;
812
813 if ((edge == RectangleEdge.TOP) || (edge == RectangleEdge.BOTTOM)) {
814 available = area.getWidth();
815 }
816 else if ((edge == RectangleEdge.LEFT)
817 || (edge == RectangleEdge.RIGHT)) {
818 available = area.getHeight();
819 }
820
821 if (categoryCount > 1) {
822 result = available * getCategoryMargin() / (categoryCount - 1);
823 }
824
825 return result;
826
827 }
828
829 /**
830 * Estimates the space required for the axis, given a specific drawing area.
831 *
832 * @param g2 the graphics device (used to obtain font information).
833 * @param plot the plot that the axis belongs to.
834 * @param plotArea the area within which the axis should be drawn.
835 * @param edge the axis location (top or bottom).
836 * @param space the space already reserved.
837 *
838 * @return The space required to draw the axis.
839 */
840 public AxisSpace reserveSpace(Graphics2D g2, Plot plot,
841 Rectangle2D plotArea,
842 RectangleEdge edge, AxisSpace space) {
843
844 // create a new space object if one wasn't supplied...
845 if (space == null) {
846 space = new AxisSpace();
847 }
848
849 // if the axis is not visible, no additional space is required...
850 if (!isVisible()) {
851 return space;
852 }
853
854 // calculate the max size of the tick labels (if visible)...
855 double tickLabelHeight = 0.0;
856 double tickLabelWidth = 0.0;
857 if (isTickLabelsVisible()) {
858 g2.setFont(getTickLabelFont());
859 AxisState state = new AxisState();
860 // we call refresh ticks just to get the maximum width or height
861 refreshTicks(g2, state, plotArea, edge);
862 if (edge == RectangleEdge.TOP) {
863 tickLabelHeight = state.getMax();
864 }
865 else if (edge == RectangleEdge.BOTTOM) {
866 tickLabelHeight = state.getMax();
867 }
868 else if (edge == RectangleEdge.LEFT) {
869 tickLabelWidth = state.getMax();
870 }
871 else if (edge == RectangleEdge.RIGHT) {
872 tickLabelWidth = state.getMax();
873 }
874 }
875
876 // get the axis label size and update the space object...
877 Rectangle2D labelEnclosure = getLabelEnclosure(g2, edge);
878 double labelHeight = 0.0;
879 double labelWidth = 0.0;
880 if (RectangleEdge.isTopOrBottom(edge)) {
881 labelHeight = labelEnclosure.getHeight();
882 space.add(labelHeight + tickLabelHeight
883 + this.categoryLabelPositionOffset, edge);
884 }
885 else if (RectangleEdge.isLeftOrRight(edge)) {
886 labelWidth = labelEnclosure.getWidth();
887 space.add(labelWidth + tickLabelWidth
888 + this.categoryLabelPositionOffset, edge);
889 }
890 return space;
891
892 }
893
894 /**
895 * Configures the axis against the current plot.
896 */
897 public void configure() {
898 // nothing required
899 }
900
901 /**
902 * Draws the axis on a Java 2D graphics device (such as the screen or a
903 * printer).
904 *
905 * @param g2 the graphics device (<code>null</code> not permitted).
906 * @param cursor the cursor location.
907 * @param plotArea the area within which the axis should be drawn
908 * (<code>null</code> not permitted).
909 * @param dataArea the area within which the plot is being drawn
910 * (<code>null</code> not permitted).
911 * @param edge the location of the axis (<code>null</code> not permitted).
912 * @param plotState collects information about the plot
913 * (<code>null</code> permitted).
914 *
915 * @return The axis state (never <code>null</code>).
916 */
917 public AxisState draw(Graphics2D g2, double cursor, Rectangle2D plotArea,
918 Rectangle2D dataArea, RectangleEdge edge,
919 PlotRenderingInfo plotState) {
920
921 // if the axis is not visible, don't draw it...
922 if (!isVisible()) {
923 return new AxisState(cursor);
924 }
925
926 if (isAxisLineVisible()) {
927 drawAxisLine(g2, cursor, dataArea, edge);
928 }
929 AxisState state = new AxisState(cursor);
930 if (isTickMarksVisible()) {
931 drawTickMarks(g2, cursor, dataArea, edge, state);
932 }
933
934 // draw the category labels and axis label
935 state = drawCategoryLabels(g2, plotArea, dataArea, edge, state,
936 plotState);
937 state = drawLabel(getLabel(), g2, plotArea, dataArea, edge, state);
938 createAndAddEntity(cursor, state, dataArea, edge, plotState);
939 return state;
940
941 }
942
943 /**
944 * Draws the category labels and returns the updated axis state.
945 *
946 * @param g2 the graphics device (<code>null</code> not permitted).
947 * @param dataArea the area inside the axes (<code>null</code> not
948 * permitted).
949 * @param edge the axis location (<code>null</code> not permitted).
950 * @param state the axis state (<code>null</code> not permitted).
951 * @param plotState collects information about the plot (<code>null</code>
952 * permitted).
953 *
954 * @return The updated axis state (never <code>null</code>).
955 *
956 * @deprecated Use {@link #drawCategoryLabels(Graphics2D, Rectangle2D,
957 * Rectangle2D, RectangleEdge, AxisState, PlotRenderingInfo)}.
958 */
959 protected AxisState drawCategoryLabels(Graphics2D g2,
960 Rectangle2D dataArea,
961 RectangleEdge edge,
962 AxisState state,
963 PlotRenderingInfo plotState) {
964
965 // this method is deprecated because we really need the plotArea
966 // when drawing the labels - see bug 1277726
967 return drawCategoryLabels(g2, dataArea, dataArea, edge, state,
968 plotState);
969 }
970
971 /**
972 * Draws the category labels and returns the updated axis state.
973 *
974 * @param g2 the graphics device (<code>null</code> not permitted).
975 * @param plotArea the plot area (<code>null</code> not permitted).
976 * @param dataArea the area inside the axes (<code>null</code> not
977 * permitted).
978 * @param edge the axis location (<code>null</code> not permitted).
979 * @param state the axis state (<code>null</code> not permitted).
980 * @param plotState collects information about the plot (<code>null</code>
981 * permitted).
982 *
983 * @return The updated axis state (never <code>null</code>).
984 */
985 protected AxisState drawCategoryLabels(Graphics2D g2,
986 Rectangle2D plotArea,
987 Rectangle2D dataArea,
988 RectangleEdge edge,
989 AxisState state,
990 PlotRenderingInfo plotState) {
991
992 if (state == null) {
993 throw new IllegalArgumentException("Null 'state' argument.");
994 }
995
996 if (isTickLabelsVisible()) {
997 List ticks = refreshTicks(g2, state, plotArea, edge);
998 state.setTicks(ticks);
999
1000 int categoryIndex = 0;
1001 Iterator iterator = ticks.iterator();
1002 while (iterator.hasNext()) {
1003
1004 CategoryTick tick = (CategoryTick) iterator.next();
1005 g2.setFont(getTickLabelFont(tick.getCategory()));
1006 g2.setPaint(getTickLabelPaint(tick.getCategory()));
1007
1008 CategoryLabelPosition position
1009 = this.categoryLabelPositions.getLabelPosition(edge);
1010 double x0 = 0.0;
1011 double x1 = 0.0;
1012 double y0 = 0.0;
1013 double y1 = 0.0;
1014 if (edge == RectangleEdge.TOP) {
1015 x0 = getCategoryStart(categoryIndex, ticks.size(),
1016 dataArea, edge);
1017 x1 = getCategoryEnd(categoryIndex, ticks.size(), dataArea,
1018 edge);
1019 y1 = state.getCursor() - this.categoryLabelPositionOffset;
1020 y0 = y1 - state.getMax();
1021 }
1022 else if (edge == RectangleEdge.BOTTOM) {
1023 x0 = getCategoryStart(categoryIndex, ticks.size(),
1024 dataArea, edge);
1025 x1 = getCategoryEnd(categoryIndex, ticks.size(), dataArea,
1026 edge);
1027 y0 = state.getCursor() + this.categoryLabelPositionOffset;
1028 y1 = y0 + state.getMax();
1029 }
1030 else if (edge == RectangleEdge.LEFT) {
1031 y0 = getCategoryStart(categoryIndex, ticks.size(),
1032 dataArea, edge);
1033 y1 = getCategoryEnd(categoryIndex, ticks.size(), dataArea,
1034 edge);
1035 x1 = state.getCursor() - this.categoryLabelPositionOffset;
1036 x0 = x1 - state.getMax();
1037 }
1038 else if (edge == RectangleEdge.RIGHT) {
1039 y0 = getCategoryStart(categoryIndex, ticks.size(),
1040 dataArea, edge);
1041 y1 = getCategoryEnd(categoryIndex, ticks.size(), dataArea,
1042 edge);
1043 x0 = state.getCursor() + this.categoryLabelPositionOffset;
1044 x1 = x0 - state.getMax();
1045 }
1046 Rectangle2D area = new Rectangle2D.Double(x0, y0, (x1 - x0),
1047 (y1 - y0));
1048 Point2D anchorPoint = RectangleAnchor.coordinates(area,
1049 position.getCategoryAnchor());
1050 TextBlock block = tick.getLabel();
1051 block.draw(g2, (float) anchorPoint.getX(),
1052 (float) anchorPoint.getY(), position.getLabelAnchor(),
1053 (float) anchorPoint.getX(), (float) anchorPoint.getY(),
1054 position.getAngle());
1055 Shape bounds = block.calculateBounds(g2,
1056 (float) anchorPoint.getX(), (float) anchorPoint.getY(),
1057 position.getLabelAnchor(), (float) anchorPoint.getX(),
1058 (float) anchorPoint.getY(), position.getAngle());
1059 if (plotState != null && plotState.getOwner() != null) {
1060 EntityCollection entities
1061 = plotState.getOwner().getEntityCollection();
1062 if (entities != null) {
1063 String tooltip = getCategoryLabelToolTip(
1064 tick.getCategory());
1065 entities.add(new CategoryLabelEntity(tick.getCategory(),
1066 bounds, tooltip, null));
1067 }
1068 }
1069 categoryIndex++;
1070 }
1071
1072 if (edge.equals(RectangleEdge.TOP)) {
1073 double h = state.getMax() + this.categoryLabelPositionOffset;
1074 state.cursorUp(h);
1075 }
1076 else if (edge.equals(RectangleEdge.BOTTOM)) {
1077 double h = state.getMax() + this.categoryLabelPositionOffset;
1078 state.cursorDown(h);
1079 }
1080 else if (edge == RectangleEdge.LEFT) {
1081 double w = state.getMax() + this.categoryLabelPositionOffset;
1082 state.cursorLeft(w);
1083 }
1084 else if (edge == RectangleEdge.RIGHT) {
1085 double w = state.getMax() + this.categoryLabelPositionOffset;
1086 state.cursorRight(w);
1087 }
1088 }
1089 return state;
1090 }
1091
1092 /**
1093 * Creates a temporary list of ticks that can be used when drawing the axis.
1094 *
1095 * @param g2 the graphics device (used to get font measurements).
1096 * @param state the axis state.
1097 * @param dataArea the area inside the axes.
1098 * @param edge the location of the axis.
1099 *
1100 * @return A list of ticks.
1101 */
1102 public List refreshTicks(Graphics2D g2,
1103 AxisState state,
1104 Rectangle2D dataArea,
1105 RectangleEdge edge) {
1106
1107 List ticks = new java.util.ArrayList();
1108
1109 // sanity check for data area...
1110 if (dataArea.getHeight() <= 0.0 || dataArea.getWidth() < 0.0) {
1111 return ticks;
1112 }
1113
1114 CategoryPlot plot = (CategoryPlot) getPlot();
1115 List categories = plot.getCategoriesForAxis(this);
1116 double max = 0.0;
1117
1118 if (categories != null) {
1119 CategoryLabelPosition position
1120 = this.categoryLabelPositions.getLabelPosition(edge);
1121 float r = this.maximumCategoryLabelWidthRatio;
1122 if (r <= 0.0) {
1123 r = position.getWidthRatio();
1124 }
1125
1126 float l = 0.0f;
1127 if (position.getWidthType() == CategoryLabelWidthType.CATEGORY) {
1128 l = (float) calculateCategorySize(categories.size(), dataArea,
1129 edge);
1130 }
1131 else {
1132 if (RectangleEdge.isLeftOrRight(edge)) {
1133 l = (float) dataArea.getWidth();
1134 }
1135 else {
1136 l = (float) dataArea.getHeight();
1137 }
1138 }
1139 int categoryIndex = 0;
1140 Iterator iterator = categories.iterator();
1141 while (iterator.hasNext()) {
1142 Comparable category = (Comparable) iterator.next();
1143 g2.setFont(getTickLabelFont(category));
1144 TextBlock label = createLabel(category, l * r, edge, g2);
1145 if (edge == RectangleEdge.TOP || edge == RectangleEdge.BOTTOM) {
1146 max = Math.max(max, calculateTextBlockHeight(label,
1147 position, g2));
1148 }
1149 else if (edge == RectangleEdge.LEFT
1150 || edge == RectangleEdge.RIGHT) {
1151 max = Math.max(max, calculateTextBlockWidth(label,
1152 position, g2));
1153 }
1154 Tick tick = new CategoryTick(category, label,
1155 position.getLabelAnchor(),
1156 position.getRotationAnchor(), position.getAngle());
1157 ticks.add(tick);
1158 categoryIndex = categoryIndex + 1;
1159 }
1160 }
1161 state.setMax(max);
1162 return ticks;
1163
1164 }
1165
1166 /**
1167 * Draws the tick marks.
1168 *
1169 * @since 1.0.13
1170 */
1171 public void drawTickMarks(Graphics2D g2, double cursor,
1172 Rectangle2D dataArea, RectangleEdge edge, AxisState state) {
1173
1174 Plot p = getPlot();
1175 if (p == null) {
1176 return;
1177 }
1178 CategoryPlot plot = (CategoryPlot) p;
1179 double il = getTickMarkInsideLength();
1180 double ol = getTickMarkOutsideLength();
1181 Line2D line = new Line2D.Double();
1182 List categories = plot.getCategoriesForAxis(this);
1183 g2.setPaint(getTickMarkPaint());
1184 g2.setStroke(getTickMarkStroke());
1185 if (edge.equals(RectangleEdge.TOP)) {
1186 Iterator iterator = categories.iterator();
1187 while (iterator.hasNext()) {
1188 Comparable key = (Comparable) iterator.next();
1189 double x = getCategoryMiddle(key, categories, dataArea, edge);
1190 line.setLine(x, cursor, x, cursor + il);
1191 g2.draw(line);
1192 line.setLine(x, cursor, x, cursor - ol);
1193 g2.draw(line);
1194 }
1195 state.cursorUp(ol);
1196 }
1197 else if (edge.equals(RectangleEdge.BOTTOM)) {
1198 Iterator iterator = categories.iterator();
1199 while (iterator.hasNext()) {
1200 Comparable key = (Comparable) iterator.next();
1201 double x = getCategoryMiddle(key, categories, dataArea, edge);
1202 line.setLine(x, cursor, x, cursor - il);
1203 g2.draw(line);
1204 line.setLine(x, cursor, x, cursor + ol);
1205 g2.draw(line);
1206 }
1207 state.cursorDown(ol);
1208 }
1209 else if (edge.equals(RectangleEdge.LEFT)) {
1210 Iterator iterator = categories.iterator();
1211 while (iterator.hasNext()) {
1212 Comparable key = (Comparable) iterator.next();
1213 double y = getCategoryMiddle(key, categories, dataArea, edge);
1214 line.setLine(cursor, y, cursor + il, y);
1215 g2.draw(line);
1216 line.setLine(cursor, y, cursor - ol, y);
1217 g2.draw(line);
1218 }
1219 state.cursorLeft(ol);
1220 }
1221 else if (edge.equals(RectangleEdge.RIGHT)) {
1222 Iterator iterator = categories.iterator();
1223 while (iterator.hasNext()) {
1224 Comparable key = (Comparable) iterator.next();
1225 double y = getCategoryMiddle(key, categories, dataArea, edge);
1226 line.setLine(cursor, y, cursor - il, y);
1227 g2.draw(line);
1228 line.setLine(cursor, y, cursor + ol, y);
1229 g2.draw(line);
1230 }
1231 state.cursorRight(ol);
1232 }
1233 }
1234
1235 /**
1236 * Creates a label.
1237 *
1238 * @param category the category.
1239 * @param width the available width.
1240 * @param edge the edge on which the axis appears.
1241 * @param g2 the graphics device.
1242 *
1243 * @return A label.
1244 */
1245 protected TextBlock createLabel(Comparable category, float width,
1246 RectangleEdge edge, Graphics2D g2) {
1247 TextBlock label = TextUtilities.createTextBlock(category.toString(),
1248 getTickLabelFont(category), getTickLabelPaint(category), width,
1249 this.maximumCategoryLabelLines, new G2TextMeasurer(g2));
1250 return label;
1251 }
1252
1253 /**
1254 * A utility method for determining the width of a text block.
1255 *
1256 * @param block the text block.
1257 * @param position the position.
1258 * @param g2 the graphics device.
1259 *
1260 * @return The width.
1261 */
1262 protected double calculateTextBlockWidth(TextBlock block,
1263 CategoryLabelPosition position, Graphics2D g2) {
1264
1265 RectangleInsets insets = getTickLabelInsets();
1266 Size2D size = block.calculateDimensions(g2);
1267 Rectangle2D box = new Rectangle2D.Double(0.0, 0.0, size.getWidth(),
1268 size.getHeight());
1269 Shape rotatedBox = ShapeUtilities.rotateShape(box, position.getAngle(),
1270 0.0f, 0.0f);
1271 double w = rotatedBox.getBounds2D().getWidth() + insets.getLeft()
1272 + insets.getRight();
1273 return w;
1274
1275 }
1276
1277 /**
1278 * A utility method for determining the height of a text block.
1279 *
1280 * @param block the text block.
1281 * @param position the label position.
1282 * @param g2 the graphics device.
1283 *
1284 * @return The height.
1285 */
1286 protected double calculateTextBlockHeight(TextBlock block,
1287 CategoryLabelPosition position,
1288 Graphics2D g2) {
1289
1290 RectangleInsets insets = getTickLabelInsets();
1291 Size2D size = block.calculateDimensions(g2);
1292 Rectangle2D box = new Rectangle2D.Double(0.0, 0.0, size.getWidth(),
1293 size.getHeight());
1294 Shape rotatedBox = ShapeUtilities.rotateShape(box, position.getAngle(),
1295 0.0f, 0.0f);
1296 double h = rotatedBox.getBounds2D().getHeight()
1297 + insets.getTop() + insets.getBottom();
1298 return h;
1299
1300 }
1301
1302 /**
1303 * Creates a clone of the axis.
1304 *
1305 * @return A clone.
1306 *
1307 * @throws CloneNotSupportedException if some component of the axis does
1308 * not support cloning.
1309 */
1310 public Object clone() throws CloneNotSupportedException {
1311 CategoryAxis clone = (CategoryAxis) super.clone();
1312 clone.tickLabelFontMap = new HashMap(this.tickLabelFontMap);
1313 clone.tickLabelPaintMap = new HashMap(this.tickLabelPaintMap);
1314 clone.categoryLabelToolTips = new HashMap(this.categoryLabelToolTips);
1315 return clone;
1316 }
1317
1318 /**
1319 * Tests this axis for equality with an arbitrary object.
1320 *
1321 * @param obj the object (<code>null</code> permitted).
1322 *
1323 * @return A boolean.
1324 */
1325 public boolean equals(Object obj) {
1326 if (obj == this) {
1327 return true;
1328 }
1329 if (!(obj instanceof CategoryAxis)) {
1330 return false;
1331 }
1332 if (!super.equals(obj)) {
1333 return false;
1334 }
1335 CategoryAxis that = (CategoryAxis) obj;
1336 if (that.lowerMargin != this.lowerMargin) {
1337 return false;
1338 }
1339 if (that.upperMargin != this.upperMargin) {
1340 return false;
1341 }
1342 if (that.categoryMargin != this.categoryMargin) {
1343 return false;
1344 }
1345 if (that.maximumCategoryLabelWidthRatio
1346 != this.maximumCategoryLabelWidthRatio) {
1347 return false;
1348 }
1349 if (that.categoryLabelPositionOffset
1350 != this.categoryLabelPositionOffset) {
1351 return false;
1352 }
1353 if (!ObjectUtilities.equal(that.categoryLabelPositions,
1354 this.categoryLabelPositions)) {
1355 return false;
1356 }
1357 if (!ObjectUtilities.equal(that.categoryLabelToolTips,
1358 this.categoryLabelToolTips)) {
1359 return false;
1360 }
1361 if (!ObjectUtilities.equal(this.tickLabelFontMap,
1362 that.tickLabelFontMap)) {
1363 return false;
1364 }
1365 if (!equalPaintMaps(this.tickLabelPaintMap, that.tickLabelPaintMap)) {
1366 return false;
1367 }
1368 return true;
1369 }
1370
1371 /**
1372 * Returns a hash code for this object.
1373 *
1374 * @return A hash code.
1375 */
1376 public int hashCode() {
1377 if (getLabel() != null) {
1378 return getLabel().hashCode();
1379 }
1380 else {
1381 return 0;
1382 }
1383 }
1384
1385 /**
1386 * Provides serialization support.
1387 *
1388 * @param stream the output stream.
1389 *
1390 * @throws IOException if there is an I/O error.
1391 */
1392 private void writeObject(ObjectOutputStream stream) throws IOException {
1393 stream.defaultWriteObject();
1394 writePaintMap(this.tickLabelPaintMap, stream);
1395 }
1396
1397 /**
1398 * Provides serialization support.
1399 *
1400 * @param stream the input stream.
1401 *
1402 * @throws IOException if there is an I/O error.
1403 * @throws ClassNotFoundException if there is a classpath problem.
1404 */
1405 private void readObject(ObjectInputStream stream)
1406 throws IOException, ClassNotFoundException {
1407 stream.defaultReadObject();
1408 this.tickLabelPaintMap = readPaintMap(stream);
1409 }
1410
1411 /**
1412 * Reads a <code>Map</code> of (<code>Comparable</code>, <code>Paint</code>)
1413 * elements from a stream.
1414 *
1415 * @param in the input stream.
1416 *
1417 * @return The map.
1418 *
1419 * @throws IOException
1420 * @throws ClassNotFoundException
1421 *
1422 * @see #writePaintMap(Map, ObjectOutputStream)
1423 */
1424 private Map readPaintMap(ObjectInputStream in)
1425 throws IOException, ClassNotFoundException {
1426 boolean isNull = in.readBoolean();
1427 if (isNull) {
1428 return null;
1429 }
1430 Map result = new HashMap();
1431 int count = in.readInt();
1432 for (int i = 0; i < count; i++) {
1433 Comparable category = (Comparable) in.readObject();
1434 Paint paint = SerialUtilities.readPaint(in);
1435 result.put(category, paint);
1436 }
1437 return result;
1438 }
1439
1440 /**
1441 * Writes a map of (<code>Comparable</code>, <code>Paint</code>)
1442 * elements to a stream.
1443 *
1444 * @param map the map (<code>null</code> permitted).
1445 *
1446 * @param out
1447 * @throws IOException
1448 *
1449 * @see #readPaintMap(ObjectInputStream)
1450 */
1451 private void writePaintMap(Map map, ObjectOutputStream out)
1452 throws IOException {
1453 if (map == null) {
1454 out.writeBoolean(true);
1455 }
1456 else {
1457 out.writeBoolean(false);
1458 Set keys = map.keySet();
1459 int count = keys.size();
1460 out.writeInt(count);
1461 Iterator iterator = keys.iterator();
1462 while (iterator.hasNext()) {
1463 Comparable key = (Comparable) iterator.next();
1464 out.writeObject(key);
1465 SerialUtilities.writePaint((Paint) map.get(key), out);
1466 }
1467 }
1468 }
1469
1470 /**
1471 * Tests two maps containing (<code>Comparable</code>, <code>Paint</code>)
1472 * elements for equality.
1473 *
1474 * @param map1 the first map (<code>null</code> not permitted).
1475 * @param map2 the second map (<code>null</code> not permitted).
1476 *
1477 * @return A boolean.
1478 */
1479 private boolean equalPaintMaps(Map map1, Map map2) {
1480 if (map1.size() != map2.size()) {
1481 return false;
1482 }
1483 Set entries = map1.entrySet();
1484 Iterator iterator = entries.iterator();
1485 while (iterator.hasNext()) {
1486 Map.Entry entry = (Map.Entry) iterator.next();
1487 Paint p1 = (Paint) entry.getValue();
1488 Paint p2 = (Paint) map2.get(entry.getKey());
1489 if (!PaintUtilities.equal(p1, p2)) {
1490 return false;
1491 }
1492 }
1493 return true;
1494 }
1495
1496 }