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