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