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