001    /* ===========================================================
002     * JFreeChart : a free chart library for the Java(tm) platform
003     * ===========================================================
004     *
005     * (C) Copyright 2000-2007, 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     * CombinedRangeCategoryPlot.java
029     * ------------------------------
030     * (C) Copyright 2003-2007, by Object Refinery Limited.
031     *
032     * Original Author:  David Gilbert (for Object Refinery Limited);
033     * Contributor(s):   Nicolas Brodu;
034     *
035     * Changes:
036     * --------
037     * 16-May-2003 : Version 1 (DG);
038     * 08-Aug-2003 : Adjusted totalWeight in remove() method (DG);
039     * 19-Aug-2003 : Implemented Cloneable (DG);
040     * 11-Sep-2003 : Fix cloning support (subplots) (NB);
041     * 15-Sep-2003 : Implemented PublicCloneable.  Fixed errors in cloning and 
042     *               serialization (DG);
043     * 16-Sep-2003 : Changed ChartRenderingInfo --> PlotRenderingInfo (DG);
044     * 17-Sep-2003 : Updated handling of 'clicks' (DG);
045     * 04-May-2004 : Added getter/setter methods for 'gap' attributes (DG);
046     * 12-Nov-2004 : Implements the new Zoomable interface (DG);
047     * 25-Nov-2004 : Small update to clone() implementation (DG);
048     * 21-Feb-2005 : Fixed bug in remove() method (id = 1121172) (DG);
049     * 21-Feb-2005 : The getLegendItems() method now returns the fixed legend
050     *               items if set (DG);
051     * 05-May-2005 : Updated draw() method parameters (DG);
052     * 14-Nov-2007 : Updated setFixedDomainAxisSpaceForSubplots() method (DG);
053     */
054     
055    package org.jfree.chart.plot;
056    
057    import java.awt.Graphics2D;
058    import java.awt.geom.Point2D;
059    import java.awt.geom.Rectangle2D;
060    import java.io.IOException;
061    import java.io.ObjectInputStream;
062    import java.io.Serializable;
063    import java.util.Collections;
064    import java.util.Iterator;
065    import java.util.List;
066    
067    import org.jfree.chart.LegendItemCollection;
068    import org.jfree.chart.axis.AxisSpace;
069    import org.jfree.chart.axis.AxisState;
070    import org.jfree.chart.axis.NumberAxis;
071    import org.jfree.chart.axis.ValueAxis;
072    import org.jfree.chart.event.PlotChangeEvent;
073    import org.jfree.chart.event.PlotChangeListener;
074    import org.jfree.data.Range;
075    import org.jfree.ui.RectangleEdge;
076    import org.jfree.ui.RectangleInsets;
077    import org.jfree.util.ObjectUtilities;
078    import org.jfree.util.PublicCloneable;
079    
080    /**
081     * A combined category plot where the range axis is shared.
082     */
083    public class CombinedRangeCategoryPlot extends CategoryPlot 
084                                           implements Zoomable,
085                                                      Cloneable, PublicCloneable, 
086                                                      Serializable,
087                                                      PlotChangeListener {
088    
089        /** For serialization. */
090        private static final long serialVersionUID = 7260210007554504515L;
091        
092        /** Storage for the subplot references. */
093        private List subplots;
094    
095        /** Total weight of all charts. */
096        private int totalWeight;
097    
098        /** The gap between subplots. */
099        private double gap;
100    
101        /** Temporary storage for the subplot areas. */
102        private transient Rectangle2D[] subplotArea;  // TODO: move to plot state
103    
104        /**
105         * Default constructor.
106         */
107        public CombinedRangeCategoryPlot() {
108            this(new NumberAxis());
109        }
110        
111        /**
112         * Creates a new plot.
113         *
114         * @param rangeAxis  the shared range axis.
115         */
116        public CombinedRangeCategoryPlot(ValueAxis rangeAxis) {
117            super(null, null, rangeAxis, null);
118            this.subplots = new java.util.ArrayList();
119            this.totalWeight = 0;
120            this.gap = 5.0;
121        }
122    
123        /**
124         * Returns the space between subplots.
125         *
126         * @return The gap (in Java2D units).
127         */
128        public double getGap() {
129            return this.gap;
130        }
131    
132        /**
133         * Sets the amount of space between subplots and sends a 
134         * {@link PlotChangeEvent} to all registered listeners.
135         *
136         * @param gap  the gap between subplots (in Java2D units).
137         */
138        public void setGap(double gap) {
139            this.gap = gap;
140            notifyListeners(new PlotChangeEvent(this));
141        }
142    
143        /**
144         * Adds a subplot (with a default 'weight' of 1) and sends a 
145         * {@link PlotChangeEvent} to all registered listeners.
146         * <br><br>
147         * You must ensure that the subplot has a non-null domain axis.  The range
148         * axis for the subplot will be set to <code>null</code>.  
149         *
150         * @param subplot  the subplot (<code>null</code> not permitted).
151         */
152        public void add(CategoryPlot subplot) {
153            // defer argument checking
154            add(subplot, 1);
155        }
156    
157        /**
158         * Adds a subplot and sends a {@link PlotChangeEvent} to all registered 
159         * listeners.
160         * <br><br>
161         * You must ensure that the subplot has a non-null domain axis.  The range
162         * axis for the subplot will be set to <code>null</code>.  
163         *
164         * @param subplot  the subplot (<code>null</code> not permitted).
165         * @param weight  the weight (must be >= 1).
166         */
167        public void add(CategoryPlot subplot, int weight) {
168            if (subplot == null) {
169                throw new IllegalArgumentException("Null 'subplot' argument.");
170            }
171            if (weight <= 0) {
172                throw new IllegalArgumentException("Require weight >= 1.");
173            }
174            // store the plot and its weight
175            subplot.setParent(this);
176            subplot.setWeight(weight);
177            subplot.setInsets(new RectangleInsets(0.0, 0.0, 0.0, 0.0));
178            subplot.setRangeAxis(null);
179            subplot.setOrientation(getOrientation());
180            subplot.addChangeListener(this);
181            this.subplots.add(subplot);
182            this.totalWeight += weight;
183            
184            // configure the range axis...
185            ValueAxis axis = getRangeAxis();
186            if (axis != null) {
187                axis.configure();
188            }
189            notifyListeners(new PlotChangeEvent(this));
190        }
191    
192        /**
193         * Removes a subplot from the combined chart.
194         *
195         * @param subplot  the subplot (<code>null</code> not permitted).
196         */
197        public void remove(CategoryPlot subplot) {
198            if (subplot == null) {
199                throw new IllegalArgumentException(" Null 'subplot' argument.");   
200            }
201            int position = -1;
202            int size = this.subplots.size();
203            int i = 0;
204            while (position == -1 && i < size) {
205                if (this.subplots.get(i) == subplot) {
206                    position = i;
207                }
208                i++;
209            }
210            if (position != -1) {
211                this.subplots.remove(position);
212                subplot.setParent(null);
213                subplot.removeChangeListener(this);
214                this.totalWeight -= subplot.getWeight();
215            
216                ValueAxis range = getRangeAxis();
217                if (range != null) {
218                    range.configure();
219                }
220    
221                ValueAxis range2 = getRangeAxis(1);
222                if (range2 != null) {
223                    range2.configure();
224                }
225                notifyListeners(new PlotChangeEvent(this));
226            }
227        }
228    
229        /**
230         * Returns the list of subplots.
231         *
232         * @return The list (unmodifiable).
233         */
234        public List getSubplots() {
235            return Collections.unmodifiableList(this.subplots);
236        }
237    
238        /**
239         * Calculates the space required for the axes.
240         * 
241         * @param g2  the graphics device.
242         * @param plotArea  the plot area.
243         * 
244         * @return The space required for the axes.
245         */
246        protected AxisSpace calculateAxisSpace(Graphics2D g2, 
247                                               Rectangle2D plotArea) {
248            
249            AxisSpace space = new AxisSpace();  
250            PlotOrientation orientation = getOrientation();
251            
252            // work out the space required by the domain axis...
253            AxisSpace fixed = getFixedRangeAxisSpace();
254            if (fixed != null) {
255                if (orientation == PlotOrientation.VERTICAL) {
256                    space.setLeft(fixed.getLeft());
257                    space.setRight(fixed.getRight());
258                }
259                else if (orientation == PlotOrientation.HORIZONTAL) {
260                    space.setTop(fixed.getTop());
261                    space.setBottom(fixed.getBottom());                
262                }
263            }
264            else {
265                ValueAxis valueAxis = getRangeAxis();
266                RectangleEdge valueEdge = Plot.resolveRangeAxisLocation(
267                        getRangeAxisLocation(), orientation);
268                if (valueAxis != null) {
269                    space = valueAxis.reserveSpace(g2, this, plotArea, valueEdge, 
270                            space);
271                }
272            }
273            
274            Rectangle2D adjustedPlotArea = space.shrink(plotArea, null);
275            // work out the maximum height or width of the non-shared axes...
276            int n = this.subplots.size();
277    
278            // calculate plotAreas of all sub-plots, maximum vertical/horizontal 
279            // axis width/height
280            this.subplotArea = new Rectangle2D[n];
281            double x = adjustedPlotArea.getX();
282            double y = adjustedPlotArea.getY();
283            double usableSize = 0.0;
284            if (orientation == PlotOrientation.VERTICAL) {
285                usableSize = adjustedPlotArea.getWidth() - this.gap * (n - 1);
286            }
287            else if (orientation == PlotOrientation.HORIZONTAL) {
288                usableSize = adjustedPlotArea.getHeight() - this.gap * (n - 1);
289            }
290    
291            for (int i = 0; i < n; i++) {
292                CategoryPlot plot = (CategoryPlot) this.subplots.get(i);
293    
294                // calculate sub-plot area
295                if (orientation == PlotOrientation.VERTICAL) {
296                    double w = usableSize * plot.getWeight() / this.totalWeight;
297                    this.subplotArea[i] = new Rectangle2D.Double(x, y, w, 
298                            adjustedPlotArea.getHeight());
299                    x = x + w + this.gap;
300                }
301                else if (orientation == PlotOrientation.HORIZONTAL) {
302                    double h = usableSize * plot.getWeight() / this.totalWeight;
303                    this.subplotArea[i] = new Rectangle2D.Double(x, y, 
304                            adjustedPlotArea.getWidth(), h);
305                    y = y + h + this.gap;
306                }
307    
308                AxisSpace subSpace = plot.calculateDomainAxisSpace(g2, 
309                        this.subplotArea[i], null);
310                space.ensureAtLeast(subSpace);
311    
312            }
313    
314            return space;
315        }
316    
317        /**
318         * Draws the plot on a Java 2D graphics device (such as the screen or a 
319         * printer).  Will perform all the placement calculations for each 
320         * sub-plots and then tell these to draw themselves.
321         *
322         * @param g2  the graphics device.
323         * @param area  the area within which the plot (including axis labels)
324         *              should be drawn.
325         * @param anchor  the anchor point (<code>null</code> permitted).
326         * @param parentState  the parent state.
327         * @param info  collects information about the drawing (<code>null</code> 
328         *              permitted).
329         */
330        public void draw(Graphics2D g2, Rectangle2D area, Point2D anchor,
331                         PlotState parentState,
332                         PlotRenderingInfo info) {
333    
334            // set up info collection...
335            if (info != null) {
336                info.setPlotArea(area);
337            }
338    
339            // adjust the drawing area for plot insets (if any)...
340            RectangleInsets insets = getInsets();
341            insets.trim(area);
342    
343            // calculate the data area...
344            AxisSpace space = calculateAxisSpace(g2, area);
345            Rectangle2D dataArea = space.shrink(area, null);
346    
347            // set the width and height of non-shared axis of all sub-plots
348            setFixedDomainAxisSpaceForSubplots(space);
349    
350            // draw the shared axis
351            ValueAxis axis = getRangeAxis();
352            RectangleEdge rangeEdge = getRangeAxisEdge();
353            double cursor = RectangleEdge.coordinate(dataArea, rangeEdge);
354            AxisState state = axis.draw(g2, cursor, area, dataArea, rangeEdge, 
355                    info);
356            if (parentState == null) {
357                parentState = new PlotState();
358            }
359            parentState.getSharedAxisStates().put(axis, state);
360            
361            // draw all the charts
362            for (int i = 0; i < this.subplots.size(); i++) {
363                CategoryPlot plot = (CategoryPlot) this.subplots.get(i);
364                PlotRenderingInfo subplotInfo = null;
365                if (info != null) {
366                    subplotInfo = new PlotRenderingInfo(info.getOwner());
367                    info.addSubplotInfo(subplotInfo);
368                }
369                plot.draw(g2, this.subplotArea[i], null, parentState, subplotInfo);
370            }
371    
372            if (info != null) {
373                info.setDataArea(dataArea);
374            }
375    
376        }
377    
378        /**
379         * Sets the orientation for the plot (and all the subplots).
380         * 
381         * @param orientation  the orientation.
382         */
383        public void setOrientation(PlotOrientation orientation) {
384    
385            super.setOrientation(orientation);
386    
387            Iterator iterator = this.subplots.iterator();
388            while (iterator.hasNext()) {
389                CategoryPlot plot = (CategoryPlot) iterator.next();
390                plot.setOrientation(orientation);
391            }
392    
393        }
394        
395        /**
396          * Returns the range for the axis.  This is the combined range of all the
397          * subplots.
398          *
399          * @param axis  the axis.
400          *
401          * @return The range.
402          */
403         public Range getDataRange(ValueAxis axis) {
404    
405             Range result = null;
406             if (this.subplots != null) {
407                 Iterator iterator = this.subplots.iterator();
408                 while (iterator.hasNext()) {
409                     CategoryPlot subplot = (CategoryPlot) iterator.next();
410                     result = Range.combine(result, subplot.getDataRange(axis));
411                 }
412             }
413             return result;
414    
415         }
416    
417        /**
418         * Returns a collection of legend items for the plot.
419         *
420         * @return The legend items.
421         */
422        public LegendItemCollection getLegendItems() {
423            LegendItemCollection result = getFixedLegendItems();
424            if (result == null) {
425                result = new LegendItemCollection();
426                if (this.subplots != null) {
427                    Iterator iterator = this.subplots.iterator();
428                    while (iterator.hasNext()) {
429                        CategoryPlot plot = (CategoryPlot) iterator.next();
430                        LegendItemCollection more = plot.getLegendItems();
431                        result.addAll(more);
432                    }
433                }
434            }
435            return result;
436        }
437        
438        /**
439         * Sets the size (width or height, depending on the orientation of the 
440         * plot) for the domain axis of each subplot.
441         *
442         * @param space  the space.
443         */
444        protected void setFixedDomainAxisSpaceForSubplots(AxisSpace space) {
445            Iterator iterator = this.subplots.iterator();
446            while (iterator.hasNext()) {
447                CategoryPlot plot = (CategoryPlot) iterator.next();
448                plot.setFixedDomainAxisSpace(space, false);
449            }
450        }
451    
452        /**
453         * Handles a 'click' on the plot by updating the anchor value.
454         *
455         * @param x  x-coordinate of the click.
456         * @param y  y-coordinate of the click.
457         * @param info  information about the plot's dimensions.
458         *
459         */
460        public void handleClick(int x, int y, PlotRenderingInfo info) {
461    
462            Rectangle2D dataArea = info.getDataArea();
463            if (dataArea.contains(x, y)) {
464                for (int i = 0; i < this.subplots.size(); i++) {
465                    CategoryPlot subplot = (CategoryPlot) this.subplots.get(i);
466                    PlotRenderingInfo subplotInfo = info.getSubplotInfo(i);
467                    subplot.handleClick(x, y, subplotInfo);
468                }
469            }
470    
471        }
472    
473        /**
474         * Receives a {@link PlotChangeEvent} and responds by notifying all 
475         * listeners.
476         * 
477         * @param event  the event.
478         */
479        public void plotChanged(PlotChangeEvent event) {
480            notifyListeners(event);
481        }
482    
483        /** 
484         * Tests the plot for equality with an arbitrary object.
485         * 
486         * @param obj  the object (<code>null</code> permitted).
487         * 
488         * @return <code>true</code> or <code>false</code>.
489         */
490        public boolean equals(Object obj) {
491            if (obj == this) {
492                return true;
493            }
494            if (!(obj instanceof CombinedRangeCategoryPlot)) {
495                return false;
496            }
497            if (!super.equals(obj)) {
498                return false;
499            }
500            CombinedRangeCategoryPlot that = (CombinedRangeCategoryPlot) obj;
501            if (!ObjectUtilities.equal(this.subplots, that.subplots)) {
502                return false;
503            }
504            if (this.totalWeight != that.totalWeight) {
505                return false;
506            }
507            if (this.gap != that.gap) {
508                return false;
509            }
510            return true;       
511        }
512    
513        /**
514         * Returns a clone of the plot.
515         * 
516         * @return A clone.
517         * 
518         * @throws CloneNotSupportedException  this class will not throw this 
519         *         exception, but subclasses (if any) might.
520         */
521        public Object clone() throws CloneNotSupportedException {
522            CombinedRangeCategoryPlot result 
523                = (CombinedRangeCategoryPlot) super.clone(); 
524            result.subplots = (List) ObjectUtilities.deepClone(this.subplots);
525            for (Iterator it = result.subplots.iterator(); it.hasNext();) {
526                Plot child = (Plot) it.next();
527                child.setParent(result);
528            }
529            
530            // after setting up all the subplots, the shared range axis may need 
531            // reconfiguring
532            ValueAxis rangeAxis = result.getRangeAxis();
533            if (rangeAxis != null) {
534                rangeAxis.configure();
535            }
536            
537            return result;
538        }
539    
540        /**
541         * Provides serialization support.
542         *
543         * @param stream  the input stream.
544         *
545         * @throws IOException  if there is an I/O error.
546         * @throws ClassNotFoundException  if there is a classpath problem.
547         */
548        private void readObject(ObjectInputStream stream) 
549            throws IOException, ClassNotFoundException {
550    
551            stream.defaultReadObject();
552            
553            // the range axis is deserialized before the subplots, so its value 
554            // range is likely to be incorrect...
555            ValueAxis rangeAxis = getRangeAxis();
556            if (rangeAxis != null) {
557                rangeAxis.configure();
558            }
559            
560        }
561    
562    }