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