001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2013, 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 * TimePeriodValues.java
029 * ---------------------
030 * (C) Copyright 2003-2013, by Object Refinery Limited.
031 *
032 * Original Author:  David Gilbert (for Object Refinery Limited);
033 * Contributor(s):   -;
034 *
035 * Changes
036 * -------
037 * 22-Apr-2003 : Version 1 (DG);
038 * 30-Jul-2003 : Added clone and equals methods while testing (DG);
039 * 11-Mar-2005 : Fixed bug in bounds recalculation - see bug report 
040 *               1161329 (DG);
041 * ------------- JFREECHART 1.0.x ---------------------------------------------
042 * 03-Oct-2006 : Fixed NullPointerException in equals(), fire change event in 
043 *               add() method, updated API docs (DG);
044 * 07-Apr-2008 : Fixed bug with maxMiddleIndex in updateBounds() (DG);
045 * 03-Jul-2013 : Use ParamChecks (DG);
046 *
047 */
048
049package org.jfree.data.time;
050
051import java.io.Serializable;
052import java.util.ArrayList;
053import java.util.List;
054import org.jfree.chart.util.ParamChecks;
055
056import org.jfree.data.general.Series;
057import org.jfree.data.general.SeriesChangeEvent;
058import org.jfree.data.general.SeriesException;
059import org.jfree.util.ObjectUtilities;
060
061/**
062 * A structure containing zero, one or many {@link TimePeriodValue} instances.  
063 * The time periods can overlap, and are maintained in the order that they are 
064 * added to the collection.
065 * <p>
066 * This is similar to the {@link TimeSeries} class, except that the time 
067 * periods can have irregular lengths.
068 */
069public class TimePeriodValues extends Series implements Serializable {
070
071    /** For serialization. */
072    static final long serialVersionUID = -2210593619794989709L;
073    
074    /** Default value for the domain description. */
075    protected static final String DEFAULT_DOMAIN_DESCRIPTION = "Time";
076
077    /** Default value for the range description. */
078    protected static final String DEFAULT_RANGE_DESCRIPTION = "Value";
079
080    /** A description of the domain. */
081    private String domain;
082
083    /** A description of the range. */
084    private String range;
085
086    /** The list of data pairs in the series. */
087    private List data;
088
089    /** Index of the time period with the minimum start milliseconds. */
090    private int minStartIndex = -1;
091    
092    /** Index of the time period with the maximum start milliseconds. */
093    private int maxStartIndex = -1;
094    
095    /** Index of the time period with the minimum middle milliseconds. */
096    private int minMiddleIndex = -1;
097    
098    /** Index of the time period with the maximum middle milliseconds. */
099    private int maxMiddleIndex = -1;
100    
101    /** Index of the time period with the minimum end milliseconds. */
102    private int minEndIndex = -1;
103    
104    /** Index of the time period with the maximum end milliseconds. */
105    private int maxEndIndex = -1;
106
107    /**
108     * Creates a new (empty) collection of time period values.
109     *
110     * @param name  the name of the series (<code>null</code> not permitted).
111     */
112    public TimePeriodValues(String name) {
113        this(name, DEFAULT_DOMAIN_DESCRIPTION, DEFAULT_RANGE_DESCRIPTION);
114    }
115
116    /**
117     * Creates a new time series that contains no data.
118     * <P>
119     * Descriptions can be specified for the domain and range.  One situation
120     * where this is helpful is when generating a chart for the time series -
121     * axis labels can be taken from the domain and range description.
122     *
123     * @param name  the name of the series (<code>null</code> not permitted).
124     * @param domain  the domain description.
125     * @param range  the range description.
126     */
127    public TimePeriodValues(String name, String domain, String range) {
128        super(name);
129        this.domain = domain;
130        this.range = range;
131        this.data = new ArrayList();
132    }
133
134    /**
135     * Returns the domain description.
136     *
137     * @return The domain description (possibly <code>null</code>).
138     * 
139     * @see #getRangeDescription()
140     * @see #setDomainDescription(String)
141     */
142    public String getDomainDescription() {
143        return this.domain;
144    }
145
146    /**
147     * Sets the domain description and fires a property change event (with the
148     * property name <code>Domain</code> if the description changes).
149     *
150     * @param description  the new description (<code>null</code> permitted).
151     * 
152     * @see #getDomainDescription()
153     */
154    public void setDomainDescription(String description) {
155        String old = this.domain;
156        this.domain = description;
157        firePropertyChange("Domain", old, description);
158    }
159
160    /**
161     * Returns the range description.
162     *
163     * @return The range description (possibly <code>null</code>).
164     * 
165     * @see #getDomainDescription()
166     * @see #setRangeDescription(String)
167     */
168    public String getRangeDescription() {
169        return this.range;
170    }
171
172    /**
173     * Sets the range description and fires a property change event with the
174     * name <code>Range</code>.
175     *
176     * @param description  the new description (<code>null</code> permitted).
177     * 
178     * @see #getRangeDescription()
179     */
180    public void setRangeDescription(String description) {
181        String old = this.range;
182        this.range = description;
183        firePropertyChange("Range", old, description);
184    }
185
186    /**
187     * Returns the number of items in the series.
188     *
189     * @return The item count.
190     */
191    @Override
192    public int getItemCount() {
193        return this.data.size();
194    }
195
196    /**
197     * Returns one data item for the series.
198     *
199     * @param index  the item index (in the range <code>0</code> to 
200     *     <code>getItemCount() - 1</code>).
201     *
202     * @return One data item for the series.
203     */
204    public TimePeriodValue getDataItem(int index) {
205        return (TimePeriodValue) this.data.get(index);
206    }
207
208    /**
209     * Returns the time period at the specified index.
210     *
211     * @param index  the item index (in the range <code>0</code> to 
212     *     <code>getItemCount() - 1</code>).
213     *
214     * @return The time period at the specified index.
215     * 
216     * @see #getDataItem(int)
217     */
218    public TimePeriod getTimePeriod(int index) {
219        return getDataItem(index).getPeriod();
220    }
221
222    /**
223     * Returns the value at the specified index.
224     *
225     * @param index  the item index (in the range <code>0</code> to 
226     *     <code>getItemCount() - 1</code>).
227     *
228     * @return The value at the specified index (possibly <code>null</code>).
229     * 
230     * @see #getDataItem(int)
231     */
232    public Number getValue(int index) {
233        return getDataItem(index).getValue();
234    }
235
236    /**
237     * Adds a data item to the series and sends a {@link SeriesChangeEvent} to
238     * all registered listeners.
239     *
240     * @param item  the item (<code>null</code> not permitted).
241     */
242    public void add(TimePeriodValue item) {
243        ParamChecks.nullNotPermitted(item, "item");
244        this.data.add(item);
245        updateBounds(item.getPeriod(), this.data.size() - 1);
246        fireSeriesChanged();
247    }
248    
249    /**
250     * Update the index values for the maximum and minimum bounds.
251     * 
252     * @param period  the time period.
253     * @param index  the index of the time period.
254     */
255    private void updateBounds(TimePeriod period, int index) {
256        
257        long start = period.getStart().getTime();
258        long end = period.getEnd().getTime();
259        long middle = start + ((end - start) / 2);
260
261        if (this.minStartIndex >= 0) {
262            long minStart = getDataItem(this.minStartIndex).getPeriod()
263                .getStart().getTime();
264            if (start < minStart) {
265                this.minStartIndex = index;           
266            }
267        }
268        else {
269            this.minStartIndex = index;
270        }
271        
272        if (this.maxStartIndex >= 0) {
273            long maxStart = getDataItem(this.maxStartIndex).getPeriod()
274                .getStart().getTime();
275            if (start > maxStart) {
276                this.maxStartIndex = index;           
277            }
278        }
279        else {
280            this.maxStartIndex = index;
281        }
282        
283        if (this.minMiddleIndex >= 0) {
284            long s = getDataItem(this.minMiddleIndex).getPeriod().getStart()
285                .getTime();
286            long e = getDataItem(this.minMiddleIndex).getPeriod().getEnd()
287                .getTime();
288            long minMiddle = s + (e - s) / 2;
289            if (middle < minMiddle) {
290                this.minMiddleIndex = index;           
291            }
292        }
293        else {
294            this.minMiddleIndex = index;
295        }
296        
297        if (this.maxMiddleIndex >= 0) {
298            long s = getDataItem(this.maxMiddleIndex).getPeriod().getStart()
299                .getTime();
300            long e = getDataItem(this.maxMiddleIndex).getPeriod().getEnd()
301                .getTime();
302            long maxMiddle = s + (e - s) / 2;
303            if (middle > maxMiddle) {
304                this.maxMiddleIndex = index;           
305            }
306        }
307        else {
308            this.maxMiddleIndex = index;
309        }
310        
311        if (this.minEndIndex >= 0) {
312            long minEnd = getDataItem(this.minEndIndex).getPeriod().getEnd()
313                .getTime();
314            if (end < minEnd) {
315                this.minEndIndex = index;           
316            }
317        }
318        else {
319            this.minEndIndex = index;
320        }
321       
322        if (this.maxEndIndex >= 0) {
323            long maxEnd = getDataItem(this.maxEndIndex).getPeriod().getEnd()
324                .getTime();
325            if (end > maxEnd) {
326                this.maxEndIndex = index;           
327            }
328        }
329        else {
330            this.maxEndIndex = index;
331        }
332        
333    }
334    
335    /**
336     * Recalculates the bounds for the collection of items.
337     */
338    private void recalculateBounds() {
339        this.minStartIndex = -1;
340        this.minMiddleIndex = -1;
341        this.minEndIndex = -1;
342        this.maxStartIndex = -1;
343        this.maxMiddleIndex = -1;
344        this.maxEndIndex = -1;
345        for (int i = 0; i < this.data.size(); i++) {
346            TimePeriodValue tpv = (TimePeriodValue) this.data.get(i);
347            updateBounds(tpv.getPeriod(), i);
348        }
349    }
350
351    /**
352     * Adds a new data item to the series and sends a {@link SeriesChangeEvent}
353     * to all registered listeners.
354     *
355     * @param period  the time period (<code>null</code> not permitted).
356     * @param value  the value.
357     * 
358     * @see #add(TimePeriod, Number)
359     */
360    public void add(TimePeriod period, double value) {
361        TimePeriodValue item = new TimePeriodValue(period, value);
362        add(item);
363    }
364
365    /**
366     * Adds a new data item to the series and sends a {@link SeriesChangeEvent}
367     * to all registered listeners.
368     *
369     * @param period  the time period (<code>null</code> not permitted).
370     * @param value  the value (<code>null</code> permitted).
371     */
372    public void add(TimePeriod period, Number value) {
373        TimePeriodValue item = new TimePeriodValue(period, value);
374        add(item);
375    }
376
377    /**
378     * Updates (changes) the value of a data item and sends a 
379     * {@link SeriesChangeEvent} to all registered listeners.
380     *
381     * @param index  the index of the data item to update.
382     * @param value  the new value (<code>null</code> not permitted).
383     */
384    public void update(int index, Number value) {
385        TimePeriodValue item = getDataItem(index);
386        item.setValue(value);
387        fireSeriesChanged();
388    }
389
390    /**
391     * Deletes data from start until end index (end inclusive) and sends a
392     * {@link SeriesChangeEvent} to all registered listeners.
393     *
394     * @param start  the index of the first period to delete.
395     * @param end  the index of the last period to delete.
396     */
397    public void delete(int start, int end) {
398        for (int i = 0; i <= (end - start); i++) {
399            this.data.remove(start);
400        }
401        recalculateBounds();
402        fireSeriesChanged();
403    }
404    
405    /**
406     * Tests the series for equality with another object.
407     *
408     * @param obj  the object (<code>null</code> permitted).
409     *
410     * @return <code>true</code> or <code>false</code>.
411     */
412    @Override
413    public boolean equals(Object obj) {
414        if (obj == this) {
415            return true;
416        }
417        if (!(obj instanceof TimePeriodValues)) {
418            return false;
419        }
420        if (!super.equals(obj)) {
421            return false;
422        }
423        TimePeriodValues that = (TimePeriodValues) obj;
424        if (!ObjectUtilities.equal(this.getDomainDescription(), 
425                that.getDomainDescription())) {
426            return false;
427        }
428        if (!ObjectUtilities.equal(this.getRangeDescription(), 
429                that.getRangeDescription())) {
430            return false;
431        }
432        int count = getItemCount();
433        if (count != that.getItemCount()) {
434            return false;
435        }
436        for (int i = 0; i < count; i++) {
437            if (!getDataItem(i).equals(that.getDataItem(i))) {
438                return false;
439            }
440        }
441        return true;
442    }
443
444    /**
445     * Returns a hash code value for the object.
446     *
447     * @return The hashcode
448     */
449    @Override
450    public int hashCode() {
451        int result;
452        result = (this.domain != null ? this.domain.hashCode() : 0);
453        result = 29 * result + (this.range != null ? this.range.hashCode() : 0);
454        result = 29 * result + this.data.hashCode();
455        result = 29 * result + this.minStartIndex;
456        result = 29 * result + this.maxStartIndex;
457        result = 29 * result + this.minMiddleIndex;
458        result = 29 * result + this.maxMiddleIndex;
459        result = 29 * result + this.minEndIndex;
460        result = 29 * result + this.maxEndIndex;
461        return result;
462    }
463
464    /**
465     * Returns a clone of the collection.
466     * <P>
467     * Notes:
468     * <ul>
469     *   <li>no need to clone the domain and range descriptions, since String 
470     *       object is immutable;</li>
471     *   <li>we pass over to the more general method createCopy(start, end).
472     *   </li>
473     * </ul>
474     *
475     * @return A clone of the time series.
476     * 
477     * @throws CloneNotSupportedException if there is a cloning problem.
478     */
479    @Override
480    public Object clone() throws CloneNotSupportedException {
481        Object clone = createCopy(0, getItemCount() - 1);
482        return clone;
483    }
484
485    /**
486     * Creates a new instance by copying a subset of the data in this 
487     * collection.
488     *
489     * @param start  the index of the first item to copy.
490     * @param end  the index of the last item to copy.
491     *
492     * @return A copy of a subset of the items.
493     * 
494     * @throws CloneNotSupportedException if there is a cloning problem.
495     */
496    public TimePeriodValues createCopy(int start, int end) 
497        throws CloneNotSupportedException {
498
499        TimePeriodValues copy = (TimePeriodValues) super.clone();
500
501        copy.data = new ArrayList();
502        if (this.data.size() > 0) {
503            for (int index = start; index <= end; index++) {
504                TimePeriodValue item = (TimePeriodValue) this.data.get(index);
505                TimePeriodValue clone = (TimePeriodValue) item.clone();
506                try {
507                    copy.add(clone);
508                }
509                catch (SeriesException e) {
510                    System.err.println("Failed to add cloned item.");
511                }
512            }
513        }
514        return copy;
515
516    }
517    
518    /**
519     * Returns the index of the time period with the minimum start milliseconds.
520     * 
521     * @return The index.
522     */
523    public int getMinStartIndex() {
524        return this.minStartIndex;
525    }
526    
527    /**
528     * Returns the index of the time period with the maximum start milliseconds.
529     * 
530     * @return The index.
531     */
532    public int getMaxStartIndex() {
533        return this.maxStartIndex;
534    }
535
536    /**
537     * Returns the index of the time period with the minimum middle 
538     * milliseconds.
539     * 
540     * @return The index.
541     */
542    public int getMinMiddleIndex() {
543        return this.minMiddleIndex;
544    }
545    
546    /**
547     * Returns the index of the time period with the maximum middle 
548     * milliseconds.
549     * 
550     * @return The index.
551     */
552    public int getMaxMiddleIndex() {
553        return this.maxMiddleIndex;
554    }
555
556    /**
557     * Returns the index of the time period with the minimum end milliseconds.
558     * 
559     * @return The index.
560     */
561    public int getMinEndIndex() {
562        return this.minEndIndex;
563    }
564    
565    /**
566     * Returns the index of the time period with the maximum end milliseconds.
567     * 
568     * @return The index.
569     */
570    public int getMaxEndIndex() {
571        return this.maxEndIndex;
572    }
573
574}