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 * DefaultIntervalCategoryDataset.java
029 * -----------------------------------
030 * (C) Copyright 2002-2021, by Jeremy Bowman and Contributors.
031 *
032 * Original Author:  Jeremy Bowman;
033 * Contributor(s):   David Gilbert;
034 *
035 */
036
037package org.jfree.data.category;
038
039import java.util.ArrayList;
040import java.util.Arrays;
041import java.util.Collections;
042import java.util.List;
043import java.util.ResourceBundle;
044import org.jfree.chart.internal.Args;
045
046import org.jfree.data.DataUtils;
047import org.jfree.data.UnknownKeyException;
048import org.jfree.data.general.AbstractSeriesDataset;
049
050/**
051 * A convenience class that provides a default implementation of the
052 * {@link IntervalCategoryDataset} interface.
053 * <p>
054 * The standard constructor accepts data in a two dimensional array where the
055 * first dimension is the series, and the second dimension is the category.
056 */
057public class DefaultIntervalCategoryDataset extends AbstractSeriesDataset
058        implements IntervalCategoryDataset {
059
060    /** The series keys. */
061    private Comparable[] seriesKeys;
062
063    /** The category keys. */
064    private Comparable[] categoryKeys;
065
066    /** Storage for the start value data. */
067    private Number[][] startData;
068
069    /** Storage for the end value data. */
070    private Number[][] endData;
071
072    /**
073     * Creates a new dataset using the specified data values and automatically
074     * generated series and category keys.
075     *
076     * @param starts  the starting values for the intervals ({@code null}
077     *                not permitted).
078     * @param ends  the ending values for the intervals ({@code null} not
079     *                permitted).
080     */
081    public DefaultIntervalCategoryDataset(double[][] starts, double[][] ends) {
082        this(DataUtils.createNumberArray2D(starts),
083                DataUtils.createNumberArray2D(ends));
084    }
085
086    /**
087     * Constructs a dataset and populates it with data from the array.
088     * <p>
089     * The arrays are indexed as data[series][category].  Series and category
090     * names are automatically generated - you can change them using the
091     * {@link #setSeriesKeys(Comparable[])} and
092     * {@link #setCategoryKeys(Comparable[])} methods.
093     *
094     * @param starts  the start values data.
095     * @param ends  the end values data.
096     */
097    public DefaultIntervalCategoryDataset(Number[][] starts, Number[][] ends) {
098        this(null, null, starts, ends);
099    }
100
101    /**
102     * Constructs a DefaultIntervalCategoryDataset, populates it with data
103     * from the arrays, and uses the supplied names for the series.
104     * <p>
105     * Category names are generated automatically ("Category 1", "Category 2",
106     * etc).
107     *
108     * @param seriesNames  the series names (if {@code null}, series names
109     *         will be generated automatically).
110     * @param starts  the start values data, indexed as data[series][category].
111     * @param ends  the end values data, indexed as data[series][category].
112     */
113    public DefaultIntervalCategoryDataset(String[] seriesNames,
114            Number[][] starts, Number[][] ends) {
115        this(seriesNames, null, starts, ends);
116    }
117
118    /**
119     * Constructs a DefaultIntervalCategoryDataset, populates it with data
120     * from the arrays, and uses the supplied names for the series and the
121     * supplied objects for the categories.
122     *
123     * @param seriesKeys  the series keys (if {@code null}, series keys
124     *         will be generated automatically).
125     * @param categoryKeys  the category keys (if {@code null}, category
126     *         keys will be generated automatically).
127     * @param starts  the start values data, indexed as data[series][category].
128     * @param ends  the end values data, indexed as data[series][category].
129     */
130    public DefaultIntervalCategoryDataset(Comparable[] seriesKeys,
131            Comparable[] categoryKeys, Number[][] starts, Number[][] ends) {
132
133        this.startData = starts;
134        this.endData = ends;
135
136        if (starts != null && ends != null) {
137            ResourceBundle resources = ResourceBundle.getBundle("org.jfree.data.resources.DataPackageResources");
138
139            int seriesCount = starts.length;
140            if (seriesCount != ends.length) {
141                String errMsg = "DefaultIntervalCategoryDataset: the number "
142                    + "of series in the start value dataset does "
143                    + "not match the number of series in the end "
144                    + "value dataset.";
145                throw new IllegalArgumentException(errMsg);
146            }
147            if (seriesCount > 0) {
148
149                // set up the series names...
150                if (seriesKeys != null) {
151
152                    if (seriesKeys.length != seriesCount) {
153                        throw new IllegalArgumentException(
154                                "The number of series keys does not "
155                                + "match the number of series in the data.");
156                    }
157
158                    this.seriesKeys = seriesKeys;
159                }
160                else {
161                    String prefix = resources.getString(
162                            "series.default-prefix") + " ";
163                    this.seriesKeys = generateKeys(seriesCount, prefix);
164                }
165
166                // set up the category names...
167                int categoryCount = starts[0].length;
168                if (categoryCount != ends[0].length) {
169                    String errMsg = "DefaultIntervalCategoryDataset: the "
170                                + "number of categories in the start value "
171                                + "dataset does not match the number of "
172                                + "categories in the end value dataset.";
173                    throw new IllegalArgumentException(errMsg);
174                }
175                if (categoryKeys != null) {
176                    if (categoryKeys.length != categoryCount) {
177                        throw new IllegalArgumentException(
178                                "The number of category keys does not match "
179                                + "the number of categories in the data.");
180                    }
181                    this.categoryKeys = categoryKeys;
182                }
183                else {
184                    String prefix = resources.getString(
185                            "categories.default-prefix") + " ";
186                    this.categoryKeys = generateKeys(categoryCount, prefix);
187                }
188
189            }
190            else {
191                this.seriesKeys = new Comparable[0];
192                this.categoryKeys = new Comparable[0];
193            }
194        }
195
196    }
197
198    /**
199     * Returns the number of series in the dataset (possibly zero).
200     *
201     * @return The number of series in the dataset.
202     *
203     * @see #getRowCount()
204     * @see #getCategoryCount()
205     */
206    @Override
207    public int getSeriesCount() {
208        int result = 0;
209        if (this.startData != null) {
210            result = this.startData.length;
211        }
212        return result;
213    }
214
215    /**
216     * Returns a series index.
217     *
218     * @param seriesKey  the series key.
219     *
220     * @return The series index.
221     *
222     * @see #getRowIndex(Comparable)
223     * @see #getSeriesKey(int)
224     */
225    public int getSeriesIndex(Comparable seriesKey) {
226        int result = -1;
227        for (int i = 0; i < this.seriesKeys.length; i++) {
228            if (seriesKey.equals(this.seriesKeys[i])) {
229                result = i;
230                break;
231            }
232        }
233        return result;
234    }
235
236    /**
237     * Returns the name of the specified series.
238     *
239     * @param series  the index of the required series (zero-based).
240     *
241     * @return The name of the specified series.
242     *
243     * @see #getSeriesIndex(Comparable)
244     */
245    @Override
246    public Comparable getSeriesKey(int series) {
247        if ((series >= getSeriesCount()) || (series < 0)) {
248            throw new IllegalArgumentException("No such series : " + series);
249        }
250        return this.seriesKeys[series];
251    }
252
253    /**
254     * Sets the names of the series in the dataset.
255     *
256     * @param seriesKeys  the new keys ({@code null} not permitted, the
257     *         length of the array must match the number of series in the
258     *         dataset).
259     *
260     * @see #setCategoryKeys(Comparable[])
261     */
262    public void setSeriesKeys(Comparable[] seriesKeys) {
263        Args.nullNotPermitted(seriesKeys, "seriesKeys");
264        if (seriesKeys.length != getSeriesCount()) {
265            throw new IllegalArgumentException(
266                    "The number of series keys does not match the data.");
267        }
268        this.seriesKeys = seriesKeys;
269        fireDatasetChanged();
270    }
271
272    /**
273     * Returns the number of categories in the dataset.
274     *
275     * @return The number of categories in the dataset.
276     *
277     * @see #getColumnCount()
278     */
279    public int getCategoryCount() {
280        int result = 0;
281        if (this.startData != null) {
282            if (getSeriesCount() > 0) {
283                result = this.startData[0].length;
284            }
285        }
286        return result;
287    }
288
289    /**
290     * Returns a list of the categories in the dataset.  This method supports
291     * the {@link CategoryDataset} interface.
292     *
293     * @return A list of the categories in the dataset.
294     *
295     * @see #getRowKeys()
296     */
297    @Override
298    public List getColumnKeys() {
299        // the CategoryDataset interface expects a list of categories, but
300        // we've stored them in an array...
301        if (this.categoryKeys == null) {
302            return new ArrayList();
303        }
304        else {
305            return Collections.unmodifiableList(Arrays.asList(
306                    this.categoryKeys));
307        }
308    }
309
310    /**
311     * Sets the categories for the dataset.
312     *
313     * @param categoryKeys  an array of objects representing the categories in
314     *                      the dataset.
315     *
316     * @see #getRowKeys()
317     * @see #setSeriesKeys(Comparable[])
318     */
319    public void setCategoryKeys(Comparable[] categoryKeys) {
320        Args.nullNotPermitted(categoryKeys, "categoryKeys");
321        if (categoryKeys.length != getCategoryCount()) {
322            throw new IllegalArgumentException(
323                    "The number of categories does not match the data.");
324        }
325        for (int i = 0; i < categoryKeys.length; i++) {
326            if (categoryKeys[i] == null) {
327                throw new IllegalArgumentException(
328                    "DefaultIntervalCategoryDataset.setCategoryKeys(): "
329                    + "null category not permitted.");
330            }
331        }
332        this.categoryKeys = categoryKeys;
333        fireDatasetChanged();
334    }
335
336    /**
337     * Returns the data value for one category in a series.
338     * <P>
339     * This method is part of the CategoryDataset interface.  Not particularly
340     * meaningful for this class...returns the end value.
341     *
342     * @param series    The required series (zero based index).
343     * @param category  The required category.
344     *
345     * @return The data value for one category in a series (null possible).
346     *
347     * @see #getEndValue(Comparable, Comparable)
348     */
349    @Override
350    public Number getValue(Comparable series, Comparable category) {
351        int seriesIndex = getSeriesIndex(series);
352        if (seriesIndex < 0) {
353            throw new UnknownKeyException("Unknown 'series' key.");
354        }
355        int itemIndex = getColumnIndex(category);
356        if (itemIndex < 0) {
357            throw new UnknownKeyException("Unknown 'category' key.");
358        }
359        return getValue(seriesIndex, itemIndex);
360    }
361
362    /**
363     * Returns the data value for one category in a series.
364     * <P>
365     * This method is part of the CategoryDataset interface.  Not particularly
366     * meaningful for this class...returns the end value.
367     *
368     * @param series  the required series (zero based index).
369     * @param category  the required category.
370     *
371     * @return The data value for one category in a series (null possible).
372     *
373     * @see #getEndValue(int, int)
374     */
375    @Override
376    public Number getValue(int series, int category) {
377        return getEndValue(series, category);
378    }
379
380    /**
381     * Returns the start data value for one category in a series.
382     *
383     * @param series  the required series.
384     * @param category  the required category.
385     *
386     * @return The start data value for one category in a series
387     *         (possibly {@code null}).
388     *
389     * @see #getStartValue(int, int)
390     */
391    @Override
392    public Number getStartValue(Comparable series, Comparable category) {
393        int seriesIndex = getSeriesIndex(series);
394        if (seriesIndex < 0) {
395            throw new UnknownKeyException("Unknown 'series' key.");
396        }
397        int itemIndex = getColumnIndex(category);
398        if (itemIndex < 0) {
399            throw new UnknownKeyException("Unknown 'category' key.");
400        }
401        return getStartValue(seriesIndex, itemIndex);
402    }
403
404    /**
405     * Returns the start data value for one category in a series.
406     *
407     * @param series  the required series (zero based index).
408     * @param category  the required category.
409     *
410     * @return The start data value for one category in a series
411     *         (possibly {@code null}).
412     *
413     * @see #getStartValue(Comparable, Comparable)
414     */
415    @Override
416    public Number getStartValue(int series, int category) {
417
418        // check arguments...
419        if ((series < 0) || (series >= getSeriesCount())) {
420            throw new IllegalArgumentException(
421                "DefaultIntervalCategoryDataset.getValue(): "
422                + "series index out of range.");
423        }
424
425        if ((category < 0) || (category >= getCategoryCount())) {
426            throw new IllegalArgumentException(
427                "DefaultIntervalCategoryDataset.getValue(): "
428                + "category index out of range.");
429        }
430
431        // fetch the value...
432        return this.startData[series][category];
433
434    }
435
436    /**
437     * Returns the end data value for one category in a series.
438     *
439     * @param series  the required series.
440     * @param category  the required category.
441     *
442     * @return The end data value for one category in a series (null possible).
443     *
444     * @see #getEndValue(int, int)
445     */
446    @Override
447    public Number getEndValue(Comparable series, Comparable category) {
448        int seriesIndex = getSeriesIndex(series);
449        if (seriesIndex < 0) {
450            throw new UnknownKeyException("Unknown 'series' key.");
451        }
452        int itemIndex = getColumnIndex(category);
453        if (itemIndex < 0) {
454            throw new UnknownKeyException("Unknown 'category' key.");
455        }
456        return getEndValue(seriesIndex, itemIndex);
457    }
458
459    /**
460     * Returns the end data value for one category in a series.
461     *
462     * @param series  the required series (zero based index).
463     * @param category  the required category.
464     *
465     * @return The end data value for one category in a series (null possible).
466     *
467     * @see #getEndValue(Comparable, Comparable)
468     */
469    @Override
470    public Number getEndValue(int series, int category) {
471        if ((series < 0) || (series >= getSeriesCount())) {
472            throw new IllegalArgumentException(
473                "DefaultIntervalCategoryDataset.getValue(): "
474                + "series index out of range.");
475        }
476
477        if ((category < 0) || (category >= getCategoryCount())) {
478            throw new IllegalArgumentException(
479                "DefaultIntervalCategoryDataset.getValue(): "
480                + "category index out of range.");
481        }
482
483        return this.endData[series][category];
484    }
485
486    /**
487     * Sets the start data value for one category in a series.
488     *
489     * @param series  the series (zero-based index).
490     * @param category  the category.
491     *
492     * @param value The value.
493     *
494     * @see #setEndValue(int, Comparable, Number)
495     */
496    public void setStartValue(int series, Comparable category, Number value) {
497
498        // does the series exist?
499        if ((series < 0) || (series > getSeriesCount() - 1)) {
500            throw new IllegalArgumentException(
501                "DefaultIntervalCategoryDataset.setValue: "
502                + "series outside valid range.");
503        }
504
505        // is the category valid?
506        int categoryIndex = getCategoryIndex(category);
507        if (categoryIndex < 0) {
508            throw new IllegalArgumentException(
509                "DefaultIntervalCategoryDataset.setValue: "
510                + "unrecognised category.");
511        }
512
513        // update the data...
514        this.startData[series][categoryIndex] = value;
515        fireDatasetChanged();
516
517    }
518
519    /**
520     * Sets the end data value for one category in a series.
521     *
522     * @param series  the series (zero-based index).
523     * @param category  the category.
524     *
525     * @param value the value.
526     *
527     * @see #setStartValue(int, Comparable, Number)
528     */
529    public void setEndValue(int series, Comparable category, Number value) {
530
531        // does the series exist?
532        if ((series < 0) || (series > getSeriesCount() - 1)) {
533            throw new IllegalArgumentException(
534                "DefaultIntervalCategoryDataset.setValue: "
535                + "series outside valid range.");
536        }
537
538        // is the category valid?
539        int categoryIndex = getCategoryIndex(category);
540        if (categoryIndex < 0) {
541            throw new IllegalArgumentException(
542                "DefaultIntervalCategoryDataset.setValue: "
543                + "unrecognised category.");
544        }
545
546        // update the data...
547        this.endData[series][categoryIndex] = value;
548        fireDatasetChanged();
549
550    }
551
552    /**
553     * Returns the index for the given category.
554     *
555     * @param category  the category ({@code null} not permitted).
556     *
557     * @return The index.
558     *
559     * @see #getColumnIndex(Comparable)
560     */
561    public int getCategoryIndex(Comparable category) {
562        int result = -1;
563        for (int i = 0; i < this.categoryKeys.length; i++) {
564            if (category.equals(this.categoryKeys[i])) {
565                result = i;
566                break;
567            }
568        }
569        return result;
570    }
571
572    /**
573     * Generates an array of keys, by appending a space plus an integer
574     * (starting with 1) to the supplied prefix string.
575     *
576     * @param count  the number of keys required.
577     * @param prefix  the name prefix.
578     *
579     * @return An array of <i>prefixN</i> with N = { 1 .. count}.
580     */
581    private Comparable[] generateKeys(int count, String prefix) {
582        Comparable[] result = new Comparable[count];
583        String name;
584        for (int i = 0; i < count; i++) {
585            name = prefix + (i + 1);
586            result[i] = name;
587        }
588        return result;
589    }
590
591    /**
592     * Returns a column key.
593     *
594     * @param column  the column index.
595     *
596     * @return The column key.
597     *
598     * @see #getRowKey(int)
599     */
600    @Override
601    public Comparable getColumnKey(int column) {
602        return this.categoryKeys[column];
603    }
604
605    /**
606     * Returns a column index.
607     *
608     * @param columnKey  the column key ({@code null} not permitted).
609     *
610     * @return The column index.
611     *
612     * @see #getCategoryIndex(Comparable)
613     */
614    @Override
615    public int getColumnIndex(Comparable columnKey) {
616        Args.nullNotPermitted(columnKey, "columnKey");
617        return getCategoryIndex(columnKey);
618    }
619
620    /**
621     * Returns a row index.
622     *
623     * @param rowKey  the row key.
624     *
625     * @return The row index.
626     *
627     * @see #getSeriesIndex(Comparable)
628     */
629    @Override
630    public int getRowIndex(Comparable rowKey) {
631        return getSeriesIndex(rowKey);
632    }
633
634    /**
635     * Returns a list of the series in the dataset.  This method supports the
636     * {@link CategoryDataset} interface.
637     *
638     * @return A list of the series in the dataset.
639     *
640     * @see #getColumnKeys()
641     */
642    @Override
643    public List getRowKeys() {
644        // the CategoryDataset interface expects a list of series, but
645        // we've stored them in an array...
646        if (this.seriesKeys == null) {
647            return new java.util.ArrayList();
648        }
649        else {
650            return Collections.unmodifiableList(Arrays.asList(this.seriesKeys));
651        }
652    }
653
654    /**
655     * Returns the name of the specified series.
656     *
657     * @param row  the index of the required row/series (zero-based).
658     *
659     * @return The name of the specified series.
660     *
661     * @see #getColumnKey(int)
662     */
663    @Override
664    public Comparable getRowKey(int row) {
665        if ((row >= getRowCount()) || (row < 0)) {
666            throw new IllegalArgumentException(
667                    "The 'row' argument is out of bounds.");
668        }
669        return this.seriesKeys[row];
670    }
671
672    /**
673     * Returns the number of categories in the dataset.  This method is part of
674     * the {@link CategoryDataset} interface.
675     *
676     * @return The number of categories in the dataset.
677     *
678     * @see #getCategoryCount()
679     * @see #getRowCount()
680     */
681    @Override
682    public int getColumnCount() {
683        return this.categoryKeys.length;
684    }
685
686    /**
687     * Returns the number of series in the dataset (possibly zero).
688     *
689     * @return The number of series in the dataset.
690     *
691     * @see #getSeriesCount()
692     * @see #getColumnCount()
693     */
694    @Override
695    public int getRowCount() {
696        return this.seriesKeys.length;
697    }
698
699    /**
700     * Tests this dataset for equality with an arbitrary object.
701     *
702     * @param obj  the object ({@code null} permitted).
703     *
704     * @return A boolean.
705     */
706    @Override
707    public boolean equals(Object obj) {
708        if (obj == this) {
709            return true;
710        }
711        if (!(obj instanceof DefaultIntervalCategoryDataset)) {
712            return false;
713        }
714        DefaultIntervalCategoryDataset that
715                = (DefaultIntervalCategoryDataset) obj;
716        if (!Arrays.equals(this.seriesKeys, that.seriesKeys)) {
717            return false;
718        }
719        if (!Arrays.equals(this.categoryKeys, that.categoryKeys)) {
720            return false;
721        }
722        if (!equal(this.startData, that.startData)) {
723            return false;
724        }
725        if (!equal(this.endData, that.endData)) {
726            return false;
727        }
728        // seem to be the same...
729        return true;
730    }
731
732    @Override
733    public int hashCode()
734    {
735        int hash = 3;
736        hash = 61 * hash + Arrays.deepHashCode( this.seriesKeys );
737        hash = 61 * hash + Arrays.deepHashCode( this.categoryKeys );
738        hash = 61 * hash + Arrays.deepHashCode( this.startData );
739        hash = 61 * hash + Arrays.deepHashCode( this.endData );
740        return hash;
741    }
742
743    /**
744     * Returns a clone of this dataset.
745     *
746     * @return A clone.
747     *
748     * @throws CloneNotSupportedException if there is a problem cloning the
749     *         dataset.
750     */
751    @Override
752    public Object clone() throws CloneNotSupportedException {
753        DefaultIntervalCategoryDataset clone
754                = (DefaultIntervalCategoryDataset) super.clone();
755        clone.categoryKeys = (Comparable[]) this.categoryKeys.clone();
756        clone.seriesKeys = (Comparable[]) this.seriesKeys.clone();
757        clone.startData = clone(this.startData);
758        clone.endData = clone(this.endData);
759        return clone;
760    }
761
762    /**
763     * Tests two double[][] arrays for equality.
764     *
765     * @param array1  the first array ({@code null} permitted).
766     * @param array2  the second arrray ({@code null} permitted).
767     *
768     * @return A boolean.
769     */
770    private static boolean equal(Number[][] array1, Number[][] array2) {
771        if (array1 == null) {
772            return (array2 == null);
773        }
774        if (array2 == null) {
775            return false;
776        }
777        if (array1.length != array2.length) {
778            return false;
779        }
780        for (int i = 0; i < array1.length; i++) {
781            if (!Arrays.equals(array1[i], array2[i])) {
782                return false;
783            }
784        }
785        return true;
786    }
787
788    /**
789     * Clones a two dimensional array of {@code Number} objects.
790     *
791     * @param array  the array ({@code null} not permitted).
792     *
793     * @return A clone of the array.
794     */
795    private static Number[][] clone(Number[][] array) {
796        Args.nullNotPermitted(array, "array");
797        Number[][] result = new Number[array.length][];
798        for (int i = 0; i < array.length; i++) {
799            Number[] child = array[i];
800            Number[] copychild = new Number[child.length];
801            System.arraycopy(child, 0, copychild, 0, child.length);
802            result[i] = copychild;
803        }
804        return result;
805    }
806
807}