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 * TaskSeriesCollection.java
029 * -------------------------
030 * (C) Copyright 2002-present, by David Gilbert.
031 *
032 * Original Author:  David Gilbert;
033 * Contributor(s):   Thomas Schuster;
034 *                   Tracy Hiltbrand (equals/hashCode comply with EqualsVerifier);
035 *
036 */
037
038package org.jfree.data.gantt;
039
040import org.jfree.chart.util.Args;
041import org.jfree.chart.util.ObjectUtils;
042import org.jfree.chart.util.PublicCloneable;
043import org.jfree.data.general.AbstractSeriesDataset;
044import org.jfree.data.general.SeriesChangeEvent;
045import org.jfree.data.time.TimePeriod;
046
047import java.io.IOException;
048import java.io.ObjectInputStream;
049import java.io.ObjectOutputStream;
050import java.io.Serializable;
051import java.util.List;
052import java.util.Objects;
053
054/**
055 * A collection of {@link TaskSeries} objects.  This class provides one
056 * implementation of the {@link GanttCategoryDataset} interface.
057 */
058public class TaskSeriesCollection extends AbstractSeriesDataset
059        implements GanttCategoryDataset, Cloneable, PublicCloneable,
060                   Serializable {
061
062    /** For serialization. */
063    private static final long serialVersionUID = -2065799050738449903L;
064
065    /**
066     * Storage for aggregate task keys (the task description is used as the
067     * key).
068     */
069    private List keys;
070
071    /** Storage for the series. */
072    private List data;
073
074    /**
075     * Default constructor.
076     */
077    public TaskSeriesCollection() {
078        this.keys = new java.util.ArrayList();
079        this.data = new java.util.ArrayList();
080    }
081
082    /**
083     * Returns a series from the collection.
084     *
085     * @param key  the series key ({@code null} not permitted).
086     *
087     * @return The series.
088     */
089    public TaskSeries getSeries(Comparable key) {
090        if (key == null) {
091            throw new NullPointerException("Null 'key' argument.");
092        }
093        TaskSeries result = null;
094        int index = getRowIndex(key);
095        if (index >= 0) {
096            result = getSeries(index);
097        }
098        return result;
099    }
100
101    /**
102     * Returns a series from the collection.
103     *
104     * @param series  the series index (zero-based).
105     *
106     * @return The series.
107     */
108    public TaskSeries getSeries(int series) {
109        if ((series < 0) || (series >= getSeriesCount())) {
110            throw new IllegalArgumentException("Series index out of bounds");
111        }
112        return (TaskSeries) this.data.get(series);
113    }
114
115    /**
116     * Returns the number of series in the collection.
117     *
118     * @return The series count.
119     */
120    @Override
121    public int getSeriesCount() {
122        return getRowCount();
123    }
124
125    /**
126     * Returns the name of a series.
127     *
128     * @param series  the series index (zero-based).
129     *
130     * @return The name of a series.
131     */
132    @Override
133    public Comparable getSeriesKey(int series) {
134        TaskSeries ts = (TaskSeries) this.data.get(series);
135        return ts.getKey();
136    }
137
138    /**
139     * Returns the number of rows (series) in the collection.
140     *
141     * @return The series count.
142     */
143    @Override
144    public int getRowCount() {
145        return this.data.size();
146    }
147
148    /**
149     * Returns the row keys.  In this case, each series is a key.
150     *
151     * @return The row keys.
152     */
153    @Override
154    public List getRowKeys() {
155        return this.data;
156    }
157
158    /**
159     * Returns the number of column in the dataset.
160     *
161     * @return The column count.
162     */
163    @Override
164    public int getColumnCount() {
165        return this.keys.size();
166    }
167
168    /**
169     * Returns a list of the column keys in the dataset.
170     *
171     * @return The category list.
172     */
173    @Override
174    public List getColumnKeys() {
175        return this.keys;
176    }
177
178    /**
179     * Returns a column key.
180     *
181     * @param index  the column index.
182     *
183     * @return The column key.
184     */
185    @Override
186    public Comparable getColumnKey(int index) {
187        return (Comparable) this.keys.get(index);
188    }
189
190    /**
191     * Returns the column index for a column key.
192     *
193     * @param columnKey  the column key ({@code null} not permitted).
194     *
195     * @return The column index.
196     */
197    @Override
198    public int getColumnIndex(Comparable columnKey) {
199        Args.nullNotPermitted(columnKey, "columnKey");
200        return this.keys.indexOf(columnKey);
201    }
202
203    /**
204     * Returns the row index for the given row key.
205     *
206     * @param rowKey  the row key.
207     *
208     * @return The index.
209     */
210    @Override
211    public int getRowIndex(Comparable rowKey) {
212        int result = -1;
213        int count = this.data.size();
214        for (int i = 0; i < count; i++) {
215            TaskSeries s = (TaskSeries) this.data.get(i);
216            if (s.getKey().equals(rowKey)) {
217                result = i;
218                break;
219            }
220        }
221        return result;
222    }
223
224    /**
225     * Returns the key for a row.
226     *
227     * @param index  the row index (zero-based).
228     *
229     * @return The key.
230     */
231    @Override
232    public Comparable getRowKey(int index) {
233        TaskSeries series = (TaskSeries) this.data.get(index);
234        return series.getKey();
235    }
236
237    /**
238     * Adds a series to the dataset and sends a
239     * {@link org.jfree.data.general.DatasetChangeEvent} to all registered
240     * listeners.
241     *
242     * @param series  the series ({@code null} not permitted).
243     */
244    public void add(TaskSeries series) {
245        Args.nullNotPermitted(series, "series");
246        this.data.add(series);
247        series.addChangeListener(this);
248
249        // look for any keys that we don't already know about...
250        for (Object o : series.getTasks()) {
251            Task task = (Task) o;
252            String key = task.getDescription();
253            int index = this.keys.indexOf(key);
254            if (index < 0) {
255                this.keys.add(key);
256            }
257        }
258        fireDatasetChanged();
259    }
260
261    /**
262     * Removes a series from the collection and sends
263     * a {@link org.jfree.data.general.DatasetChangeEvent}
264     * to all registered listeners.
265     *
266     * @param series  the series.
267     */
268    public void remove(TaskSeries series) {
269        Args.nullNotPermitted(series, "series");
270        if (this.data.contains(series)) {
271            series.removeChangeListener(this);
272            this.data.remove(series);
273            fireDatasetChanged();
274        }
275    }
276
277    /**
278     * Removes a series from the collection and sends
279     * a {@link org.jfree.data.general.DatasetChangeEvent}
280     * to all registered listeners.
281     *
282     * @param series  the series (zero based index).
283     */
284    public void remove(int series) {
285        if ((series < 0) || (series >= getSeriesCount())) {
286            throw new IllegalArgumentException(
287                "TaskSeriesCollection.remove(): index outside valid range.");
288        }
289
290        // fetch the series, remove the change listener, then remove the series.
291        TaskSeries ts = (TaskSeries) this.data.get(series);
292        ts.removeChangeListener(this);
293        this.data.remove(series);
294        fireDatasetChanged();
295
296    }
297
298    /**
299     * Removes all the series from the collection and sends
300     * a {@link org.jfree.data.general.DatasetChangeEvent}
301     * to all registered listeners.
302     */
303    public void removeAll() {
304
305        // deregister the collection as a change listener to each series in
306        // the collection.
307        for (Object item : this.data) {
308            TaskSeries series = (TaskSeries) item;
309            series.removeChangeListener(this);
310        }
311
312        // remove all the series from the collection and notify listeners.
313        this.data.clear();
314        fireDatasetChanged();
315
316    }
317
318    /**
319     * Returns the value for an item.
320     *
321     * @param rowKey  the row key.
322     * @param columnKey  the column key.
323     *
324     * @return The item value.
325     */
326    @Override
327    public Number getValue(Comparable rowKey, Comparable columnKey) {
328        return getStartValue(rowKey, columnKey);
329    }
330
331    /**
332     * Returns the value for a task.
333     *
334     * @param row  the row index (zero-based).
335     * @param column  the column index (zero-based).
336     *
337     * @return The start value.
338     */
339    @Override
340    public Number getValue(int row, int column) {
341        return getStartValue(row, column);
342    }
343
344    /**
345     * Returns the start value for a task.  This is a date/time value, measured
346     * in milliseconds since 1-Jan-1970.
347     *
348     * @param rowKey  the series.
349     * @param columnKey  the category.
350     *
351     * @return The start value (possibly {@code null}).
352     */
353    @Override
354    public Number getStartValue(Comparable rowKey, Comparable columnKey) {
355        Number result = null;
356        int row = getRowIndex(rowKey);
357        TaskSeries series = (TaskSeries) this.data.get(row);
358        Task task = series.get(columnKey.toString());
359        if (task != null) {
360            TimePeriod duration = task.getDuration();
361            if (duration != null) {
362                result = duration.getStart().getTime();
363            }
364        }
365        return result;
366    }
367
368    /**
369     * Returns the start value for a task.
370     *
371     * @param row  the row index (zero-based).
372     * @param column  the column index (zero-based).
373     *
374     * @return The start value.
375     */
376    @Override
377    public Number getStartValue(int row, int column) {
378        Comparable rowKey = getRowKey(row);
379        Comparable columnKey = getColumnKey(column);
380        return getStartValue(rowKey, columnKey);
381    }
382
383    /**
384     * Returns the end value for a task.  This is a date/time value, measured
385     * in milliseconds since 1-Jan-1970.
386     *
387     * @param rowKey  the series.
388     * @param columnKey  the category.
389     *
390     * @return The end value (possibly {@code null}).
391     */
392    @Override
393    public Number getEndValue(Comparable rowKey, Comparable columnKey) {
394        Number result = null;
395        int row = getRowIndex(rowKey);
396        TaskSeries series = (TaskSeries) this.data.get(row);
397        Task task = series.get(columnKey.toString());
398        if (task != null) {
399            TimePeriod duration = task.getDuration();
400            if (duration != null) {
401                result = duration.getEnd().getTime();
402            }
403        }
404        return result;
405    }
406
407    /**
408     * Returns the end value for a task.
409     *
410     * @param row  the row index (zero-based).
411     * @param column  the column index (zero-based).
412     *
413     * @return The end value.
414     */
415    @Override
416    public Number getEndValue(int row, int column) {
417        Comparable rowKey = getRowKey(row);
418        Comparable columnKey = getColumnKey(column);
419        return getEndValue(rowKey, columnKey);
420    }
421
422    /**
423     * Returns the percent complete for a given item.
424     *
425     * @param row  the row index (zero-based).
426     * @param column  the column index (zero-based).
427     *
428     * @return The percent complete (possibly {@code null}).
429     */
430    @Override
431    public Number getPercentComplete(int row, int column) {
432        Comparable rowKey = getRowKey(row);
433        Comparable columnKey = getColumnKey(column);
434        return getPercentComplete(rowKey, columnKey);
435    }
436
437    /**
438     * Returns the percent complete for a given item.
439     *
440     * @param rowKey  the row key.
441     * @param columnKey  the column key.
442     *
443     * @return The percent complete.
444     */
445    @Override
446    public Number getPercentComplete(Comparable rowKey, Comparable columnKey) {
447        Number result = null;
448        int row = getRowIndex(rowKey);
449        TaskSeries series = (TaskSeries) this.data.get(row);
450        Task task = series.get(columnKey.toString());
451        if (task != null) {
452            result = task.getPercentComplete();
453        }
454        return result;
455    }
456
457    /**
458     * Returns the number of sub-intervals for a given item.
459     *
460     * @param row  the row index (zero-based).
461     * @param column  the column index (zero-based).
462     *
463     * @return The sub-interval count.
464     */
465    @Override
466    public int getSubIntervalCount(int row, int column) {
467        Comparable rowKey = getRowKey(row);
468        Comparable columnKey = getColumnKey(column);
469        return getSubIntervalCount(rowKey, columnKey);
470    }
471
472    /**
473     * Returns the number of sub-intervals for a given item.
474     *
475     * @param rowKey  the row key.
476     * @param columnKey  the column key.
477     *
478     * @return The sub-interval count.
479     */
480    @Override
481    public int getSubIntervalCount(Comparable rowKey, Comparable columnKey) {
482        int result = 0;
483        int row = getRowIndex(rowKey);
484        TaskSeries series = (TaskSeries) this.data.get(row);
485        Task task = series.get(columnKey.toString());
486        if (task != null) {
487            result = task.getSubtaskCount();
488        }
489        return result;
490    }
491
492    /**
493     * Returns the start value of a sub-interval for a given item.
494     *
495     * @param row  the row index (zero-based).
496     * @param column  the column index (zero-based).
497     * @param subinterval  the sub-interval index (zero-based).
498     *
499     * @return The start value (possibly {@code null}).
500     */
501    @Override
502    public Number getStartValue(int row, int column, int subinterval) {
503        Comparable rowKey = getRowKey(row);
504        Comparable columnKey = getColumnKey(column);
505        return getStartValue(rowKey, columnKey, subinterval);
506    }
507
508    /**
509     * Returns the start value of a sub-interval for a given item.
510     *
511     * @param rowKey  the row key.
512     * @param columnKey  the column key.
513     * @param subinterval  the subinterval.
514     *
515     * @return The start value (possibly {@code null}).
516     */
517    @Override
518    public Number getStartValue(Comparable rowKey, Comparable columnKey,
519                                int subinterval) {
520        Number result = null;
521        int row = getRowIndex(rowKey);
522        TaskSeries series = (TaskSeries) this.data.get(row);
523        Task task = series.get(columnKey.toString());
524        if (task != null) {
525            Task sub = task.getSubtask(subinterval);
526            if (sub != null) {
527                TimePeriod duration = sub.getDuration();
528                result = duration.getStart().getTime();
529            }
530        }
531        return result;
532    }
533
534    /**
535     * Returns the end value of a sub-interval for a given item.
536     *
537     * @param row  the row index (zero-based).
538     * @param column  the column index (zero-based).
539     * @param subinterval  the subinterval.
540     *
541     * @return The end value (possibly {@code null}).
542     */
543    @Override
544    public Number getEndValue(int row, int column, int subinterval) {
545        Comparable rowKey = getRowKey(row);
546        Comparable columnKey = getColumnKey(column);
547        return getEndValue(rowKey, columnKey, subinterval);
548    }
549
550    /**
551     * Returns the end value of a sub-interval for a given item.
552     *
553     * @param rowKey  the row key.
554     * @param columnKey  the column key.
555     * @param subinterval  the subinterval.
556     *
557     * @return The end value (possibly {@code null}).
558     */
559    @Override
560    public Number getEndValue(Comparable rowKey, Comparable columnKey,
561                              int subinterval) {
562        Number result = null;
563        int row = getRowIndex(rowKey);
564        TaskSeries series = (TaskSeries) this.data.get(row);
565        Task task = series.get(columnKey.toString());
566        if (task != null) {
567            Task sub = task.getSubtask(subinterval);
568            if (sub != null) {
569                TimePeriod duration = sub.getDuration();
570                result = duration.getEnd().getTime();
571            }
572        }
573        return result;
574    }
575
576    /**
577     * Returns the percentage complete value of a sub-interval for a given item.
578     *
579     * @param row  the row index (zero-based).
580     * @param column  the column index (zero-based).
581     * @param subinterval  the sub-interval.
582     *
583     * @return The percent complete value (possibly {@code null}).
584     */
585    @Override
586    public Number getPercentComplete(int row, int column, int subinterval) {
587        Comparable rowKey = getRowKey(row);
588        Comparable columnKey = getColumnKey(column);
589        return getPercentComplete(rowKey, columnKey, subinterval);
590    }
591
592    /**
593     * Returns the percentage complete value of a sub-interval for a given item.
594     *
595     * @param rowKey  the row key.
596     * @param columnKey  the column key.
597     * @param subinterval  the sub-interval.
598     *
599     * @return The percent complete value (possibly {@code null}).
600     */
601    @Override
602    public Number getPercentComplete(Comparable rowKey, Comparable columnKey,
603                                     int subinterval) {
604        Number result = null;
605        int row = getRowIndex(rowKey);
606        TaskSeries series = (TaskSeries) this.data.get(row);
607        Task task = series.get(columnKey.toString());
608        if (task != null) {
609            Task sub = task.getSubtask(subinterval);
610            if (sub != null) {
611                result = sub.getPercentComplete();
612            }
613        }
614        return result;
615    }
616
617    /**
618     * Called when a series belonging to the dataset changes.
619     *
620     * @param event  information about the change.
621     */
622    @Override
623    public void seriesChanged(SeriesChangeEvent event) {
624        refreshKeys();
625        fireDatasetChanged();
626    }
627
628    /**
629     * Refreshes the keys.
630     */
631    private void refreshKeys() {
632
633        this.keys.clear();
634        for (int i = 0; i < getSeriesCount(); i++) {
635            TaskSeries series = (TaskSeries) this.data.get(i);
636            // look for any keys that we don't already know about...
637            for (Object o : series.getTasks()) {
638                Task task = (Task) o;
639                String key = task.getDescription();
640                int index = this.keys.indexOf(key);
641                if (index < 0) {
642                    this.keys.add(key);
643                }
644            }
645        }
646
647    }
648
649    /**
650     * Tests this instance for equality with an arbitrary object.
651     *
652     * @param obj  the object ({@code null} permitted).
653     *
654     * @return A boolean.
655     */
656    @Override
657    public boolean equals(Object obj) {
658        if (obj == this) {
659            return true;
660        }
661        if (!(obj instanceof TaskSeriesCollection)) {
662            return false;
663        }
664        TaskSeriesCollection that = (TaskSeriesCollection) obj;
665        if (!Objects.equals(this.data, that.data)) {
666            return false;
667        }
668        if (!Objects.equals(this.keys, that.keys)) {
669            return false;
670        }
671        if (!that.canEqual(this)) {
672            return false;
673        }
674        return super.equals(obj);
675    }
676
677    /**
678     * Ensures symmetry between super/subclass implementations of equals. For
679     * more detail, see http://jqno.nl/equalsverifier/manual/inheritance.
680     *
681     * @param other Object
682     * 
683     * @return true ONLY if the parameter is THIS class type
684     */
685    @Override
686    public boolean canEqual(Object other) {
687        // fix the "equals not symmetric" problem
688        return (other instanceof TaskSeriesCollection);
689    }
690
691    @Override
692    public int hashCode() {
693        int hash = super.hashCode(); // equals calls superclass, hashCode must also
694        hash = 79 * hash + Objects.hashCode(this.data);
695        hash = 79 * hash + Objects.hashCode(this.keys);
696        return hash;
697    }
698
699    /**
700     * Returns an independent copy of this dataset.
701     *
702     * @return A clone of the dataset.
703     *
704     * @throws CloneNotSupportedException if there is some problem cloning
705     *     the dataset.
706     */
707    @Override
708    public Object clone() throws CloneNotSupportedException {
709        TaskSeriesCollection clone = (TaskSeriesCollection) super.clone();
710        clone.data = (List) ObjectUtils.deepClone(this.data);
711        clone.keys = new java.util.ArrayList(this.keys);
712        return clone;
713    }
714
715    /**
716     * Provides serialization support.
717     *
718     * @param stream  the output stream.
719     *
720     * @throws IOException  if there is an I/O error.
721     */
722    private void writeObject(ObjectOutputStream stream) throws IOException {
723        stream.defaultWriteObject();
724    }
725
726    /**
727     * Provides serialization support.
728     *
729     * @param stream  the input stream.
730     *
731     * @throws IOException  if there is an I/O error.
732     * @throws ClassNotFoundException  if there is a classpath problem.
733     */
734    private void readObject(ObjectInputStream stream)
735            throws IOException, ClassNotFoundException {
736        stream.defaultReadObject();
737        for (Object item : this.data) {
738            TaskSeries series = (TaskSeries) item;
739            series.addChangeListener(this);
740        }
741    }
742}