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