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     * SlidingCategoryDataset.java
029     * ---------------------------
030     * (C) Copyright 2008, by Object Refinery Limited.
031     *
032     * Original Author:  David Gilbert (for Object Refinery Limited);
033     * Contributor(s):   -;
034     *
035     * Changes
036     * -------
037     * 08-May-2008 : Version 1 (DG);
038     *
039     */
040    
041    package org.jfree.data.category;
042    
043    import java.util.Collections;
044    import java.util.List;
045    
046    import org.jfree.data.UnknownKeyException;
047    import org.jfree.data.general.AbstractDataset;
048    import org.jfree.data.general.DatasetChangeEvent;
049    import org.jfree.util.PublicCloneable;
050    
051    /**
052     * A {@link CategoryDataset} implementation that presents a subset of the
053     * categories in an underlying dataset.  The index of the first "visible"
054     * category can be modified, which provides a means of "sliding" through
055     * the categories in the underlying dataset.
056     *
057     * @since 1.0.10
058     */
059    public class SlidingCategoryDataset extends AbstractDataset
060            implements CategoryDataset {
061    
062        /** The underlying dataset. */
063        private CategoryDataset underlying;
064    
065        /** The index of the first category to present. */
066        private int firstCategoryIndex;
067    
068        /** The maximum number of categories to present. */
069        private int maximumCategoryCount;
070    
071        /**
072         * Creates a new instance.
073         *
074         * @param underlying  the underlying dataset (<code>null</code> not
075         *     permitted).
076         * @param firstColumn  the index of the first visible column from the
077         *     underlying dataset.
078         * @param maxColumns  the maximumColumnCount.
079         */
080        public SlidingCategoryDataset(CategoryDataset underlying, int firstColumn,
081                int maxColumns) {
082            this.underlying = underlying;
083            this.firstCategoryIndex = firstColumn;
084            this.maximumCategoryCount = maxColumns;
085        }
086    
087        /**
088         * Returns the underlying dataset that was supplied to the constructor.
089         *
090         * @return The underlying dataset (never <code>null</code>).
091         */
092        public CategoryDataset getUnderlyingDataset() {
093            return this.underlying;
094        }
095    
096        /**
097         * Returns the index of the first visible category.
098         *
099         * @return The index.
100         *
101         * @see #setFirstCategoryIndex(int)
102         */
103        public int getFirstCategoryIndex() {
104            return this.firstCategoryIndex;
105        }
106    
107        /**
108         * Sets the index of the first category that should be used from the
109         * underlying dataset, and sends a {@link DatasetChangeEvent} to all
110         * registered listeners.
111         *
112         * @param first  the index.
113         *
114         * @see #getFirstCategoryIndex()
115         */
116        public void setFirstCategoryIndex(int first) {
117            if (first < 0 || first >= this.underlying.getColumnCount()) {
118                throw new IllegalArgumentException("Invalid index.");
119            }
120            this.firstCategoryIndex = first;
121            fireDatasetChanged();
122        }
123    
124        /**
125         * Returns the maximum category count.
126         *
127         * @return The maximum category count.
128         *
129         * @see #setMaximumCategoryCount(int)
130         */
131        public int getMaximumCategoryCount() {
132            return this.maximumCategoryCount;
133        }
134    
135        /**
136         * Sets the maximum category count and sends a {@link DatasetChangeEvent}
137         * to all registered listeners.
138         *
139         * @param max  the maximum.
140         *
141         * @see #getMaximumCategoryCount()
142         */
143        public void setMaximumCategoryCount(int max) {
144            if (max < 0) {
145                throw new IllegalArgumentException("Requires 'max' >= 0.");
146            }
147            this.maximumCategoryCount = max;
148            fireDatasetChanged();
149        }
150    
151        /**
152         * Returns the index of the last column for this dataset, or -1.
153         *
154         * @return The index.
155         */
156        private int lastCategoryIndex() {
157            if (this.maximumCategoryCount == 0) {
158                return -1;
159            }
160            return Math.min(this.firstCategoryIndex + this.maximumCategoryCount,
161                    this.underlying.getColumnCount()) - 1;
162        }
163    
164        /**
165         * Returns the index for the specified column key.
166         *
167         * @param key  the key.
168         *
169         * @return The column index, or -1 if the key is not recognised.
170         */
171        public int getColumnIndex(Comparable key) {
172            int index = this.underlying.getColumnIndex(key);
173            if (index >= this.firstCategoryIndex && index <= lastCategoryIndex()) {
174                return index - this.firstCategoryIndex;
175            }
176            return -1;  // we didn't find the key
177        }
178    
179        /**
180         * Returns the column key for a given index.
181         *
182         * @param column  the column index (zero-based).
183         *
184         * @return The column key.
185         *
186         * @throws IndexOutOfBoundsException if <code>row</code> is out of bounds.
187         */
188        public Comparable getColumnKey(int column) {
189            return this.underlying.getColumnKey(column + this.firstCategoryIndex);
190        }
191    
192        /**
193         * Returns the column keys.
194         *
195         * @return The keys.
196         *
197         * @see #getColumnKey(int)
198         */
199        public List getColumnKeys() {
200            List result = new java.util.ArrayList();
201            int last = lastCategoryIndex();
202            for (int i = this.firstCategoryIndex; i < last; i++) {
203                result.add(this.underlying.getColumnKey(i));
204            }
205            return Collections.unmodifiableList(result);
206        }
207    
208        /**
209         * Returns the row index for a given key.
210         *
211         * @param key  the row key.
212         *
213         * @return The row index, or <code>-1</code> if the key is unrecognised.
214         */
215        public int getRowIndex(Comparable key) {
216            return this.underlying.getRowIndex(key);
217        }
218    
219        /**
220         * Returns the row key for a given index.
221         *
222         * @param row  the row index (zero-based).
223         *
224         * @return The row key.
225         *
226         * @throws IndexOutOfBoundsException if <code>row</code> is out of bounds.
227         */
228        public Comparable getRowKey(int row) {
229            return this.underlying.getRowKey(row);
230        }
231    
232        /**
233         * Returns the row keys.
234         *
235         * @return The keys.
236         */
237        public List getRowKeys() {
238            return this.underlying.getRowKeys();
239        }
240    
241        /**
242         * Returns the value for a pair of keys.
243         *
244         * @param rowKey  the row key (<code>null</code> not permitted).
245         * @param columnKey  the column key (<code>null</code> not permitted).
246         *
247         * @return The value (possibly <code>null</code>).
248         *
249         * @throws UnknownKeyException if either key is not defined in the dataset.
250         */
251        public Number getValue(Comparable rowKey, Comparable columnKey) {
252            int r = getRowIndex(rowKey);
253            int c = getColumnIndex(columnKey);
254            if (c != -1) {
255                return this.underlying.getValue(r, c + this.firstCategoryIndex);
256            }
257            else {
258                throw new UnknownKeyException("Unknown columnKey: " + columnKey);
259            }
260        }
261    
262        /**
263         * Returns the number of columns in the table.
264         *
265         * @return The column count.
266         */
267        public int getColumnCount() {
268            int last = lastCategoryIndex();
269            if (last == -1) {
270                return 0;
271            }
272            else {
273                return Math.max(last - this.firstCategoryIndex + 1, 0);
274            }
275        }
276    
277        /**
278         * Returns the number of rows in the table.
279         *
280         * @return The row count.
281         */
282        public int getRowCount() {
283            return this.underlying.getRowCount();
284        }
285    
286        /**
287         * Returns a value from the table.
288         *
289         * @param row  the row index (zero-based).
290         * @param column  the column index (zero-based).
291         *
292         * @return The value (possibly <code>null</code>).
293         */
294        public Number getValue(int row, int column) {
295            return this.underlying.getValue(row, column + this.firstCategoryIndex);
296        }
297    
298        /**
299         * Tests this <code>SlidingCategoryDataset</code> for equality with an
300         * arbitrary object.
301         *
302         * @param obj  the object (<code>null</code> permitted).
303         *
304         * @return A boolean.
305         */
306        public boolean equals(Object obj) {
307            if (obj == this) {
308                return true;
309            }
310            if (!(obj instanceof SlidingCategoryDataset)) {
311                return false;
312            }
313            SlidingCategoryDataset that = (SlidingCategoryDataset) obj;
314            if (this.firstCategoryIndex != that.firstCategoryIndex) {
315                return false;
316            }
317            if (this.maximumCategoryCount != that.maximumCategoryCount) {
318                return false;
319            }
320            if (!this.underlying.equals(that.underlying)) {
321                return false;
322            }
323            return true;
324        }
325    
326        /**
327         * Returns an independent copy of the dataset.  Note that:
328         * <ul>
329         * <li>the underlying dataset is only cloned if it implements the
330         * {@link PublicCloneable} interface;</li>
331         * <li>the listeners registered with this dataset are not carried over to
332         * the cloned dataset.</li>
333         * </ul>
334         *
335         * @return An independent copy of the dataset.
336         *
337         * @throws CloneNotSupportedException if the dataset cannot be cloned for
338         *         any reason.
339         */
340        public Object clone() throws CloneNotSupportedException {
341            SlidingCategoryDataset clone = (SlidingCategoryDataset) super.clone();
342            if (this.underlying instanceof PublicCloneable) {
343                PublicCloneable pc = (PublicCloneable) this.underlying;
344                clone.underlying = (CategoryDataset) pc.clone();
345            }
346            return clone;
347        }
348    
349    }