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