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