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