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