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