001    /* ===========================================================
002     * JFreeChart : a free chart library for the Java(tm) platform
003     * ===========================================================
004     *
005     * (C) Copyright 2000-2006, 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-2006, by Object Refinery Limited.
031     *
032     * Original Author:  David Gilbert (for Object Refinery Limited);
033     * Contributor(s):   -;
034     *
035     * $Id: TimePeriodValues.java,v 1.8.2.2 2006/10/03 15:16:33 mungady Exp $
036     *
037     * Changes
038     * -------
039     * 22-Apr-2003 : Version 1 (DG);
040     * 30-Jul-2003 : Added clone and equals methods while testing (DG);
041     * 11-Mar-2005 : Fixed bug in bounds recalculation - see bug report 
042     *               1161329 (DG);
043     * ------------- JFREECHART 1.0.0 ---------------------------------------------
044     * 03-Oct-2006 : Fixed NullPointerException in equals(), fire change event in 
045     *               add() method, updated API docs (DG);
046     *
047     */
048    
049    package org.jfree.data.time;
050    
051    import java.io.Serializable;
052    import java.util.ArrayList;
053    import java.util.List;
054    
055    import org.jfree.data.general.Series;
056    import org.jfree.data.general.SeriesChangeEvent;
057    import org.jfree.data.general.SeriesException;
058    import org.jfree.util.ObjectUtilities;
059    
060    /**
061     * A structure containing zero, one or many {@link TimePeriodValue} instances.  
062     * The time periods can overlap, and are maintained in the order that they are 
063     * added to the collection.
064     * <p>
065     * This is similar to the {@link TimeSeries} class, except that the time 
066     * periods can have irregular lengths.
067     */
068    public class TimePeriodValues extends Series implements Serializable {
069    
070        /** For serialization. */
071        static final long serialVersionUID = -2210593619794989709L;
072        
073        /** Default value for the domain description. */
074        protected static final String DEFAULT_DOMAIN_DESCRIPTION = "Time";
075    
076        /** Default value for the range description. */
077        protected static final String DEFAULT_RANGE_DESCRIPTION = "Value";
078    
079        /** A description of the domain. */
080        private String domain;
081    
082        /** A description of the range. */
083        private String range;
084    
085        /** The list of data pairs in the series. */
086        private List data;
087    
088        /** Index of the time period with the minimum start milliseconds. */
089        private int minStartIndex = -1;
090        
091        /** Index of the time period with the maximum start milliseconds. */
092        private int maxStartIndex = -1;
093        
094        /** Index of the time period with the minimum middle milliseconds. */
095        private int minMiddleIndex = -1;
096        
097        /** Index of the time period with the maximum middle milliseconds. */
098        private int maxMiddleIndex = -1;
099        
100        /** Index of the time period with the minimum end milliseconds. */
101        private int minEndIndex = -1;
102        
103        /** Index of the time period with the maximum end milliseconds. */
104        private int maxEndIndex = -1;
105    
106        /**
107         * Creates a new (empty) collection of time period values.
108         *
109         * @param name  the name of the series (<code>null</code> not permitted).
110         */
111        public TimePeriodValues(String name) {
112            this(name, DEFAULT_DOMAIN_DESCRIPTION, DEFAULT_RANGE_DESCRIPTION);
113        }
114    
115        /**
116         * Creates a new time series that contains no data.
117         * <P>
118         * Descriptions can be specified for the domain and range.  One situation
119         * where this is helpful is when generating a chart for the time series -
120         * axis labels can be taken from the domain and range description.
121         *
122         * @param name  the name of the series (<code>null</code> not permitted).
123         * @param domain  the domain description.
124         * @param range  the range description.
125         */
126        public TimePeriodValues(String name, String domain, String range) {
127            super(name);
128            this.domain = domain;
129            this.range = range;
130            this.data = new ArrayList();
131        }
132    
133        /**
134         * Returns the domain description.
135         *
136         * @return The domain description (possibly <code>null</code>).
137         * 
138         * @see #getRangeDescription()
139         * @see #setDomainDescription(String)
140         */
141        public String getDomainDescription() {
142            return this.domain;
143        }
144    
145        /**
146         * Sets the domain description and fires a property change event (with the
147         * property name <code>Domain</code> if the description changes).
148         *
149         * @param description  the new description (<code>null</code> permitted).
150         * 
151         * @see #getDomainDescription()
152         */
153        public void setDomainDescription(String description) {
154            String old = this.domain;
155            this.domain = description;
156            firePropertyChange("Domain", old, description);
157        }
158    
159        /**
160         * Returns the range description.
161         *
162         * @return The range description (possibly <code>null</code>).
163         * 
164         * @see #getDomainDescription()
165         * @see #setRangeDescription(String)
166         */
167        public String getRangeDescription() {
168            return this.range;
169        }
170    
171        /**
172         * Sets the range description and fires a property change event with the
173         * name <code>Range</code>.
174         *
175         * @param description  the new description (<code>null</code> permitted).
176         * 
177         * @see #getRangeDescription()
178         */
179        public void setRangeDescription(String description) {
180            String old = this.range;
181            this.range = description;
182            firePropertyChange("Range", old, description);
183        }
184    
185        /**
186         * Returns the number of items in the series.
187         *
188         * @return The item count.
189         */
190        public int getItemCount() {
191            return this.data.size();
192        }
193    
194        /**
195         * Returns one data item for the series.
196         *
197         * @param index  the item index (in the range <code>0</code> to 
198         *     <code>getItemCount() - 1</code>).
199         *
200         * @return One data item for the series.
201         */
202        public TimePeriodValue getDataItem(int index) {
203            return (TimePeriodValue) this.data.get(index);
204        }
205    
206        /**
207         * Returns the time period at the specified index.
208         *
209         * @param index  the item index (in the range <code>0</code> to 
210         *     <code>getItemCount() - 1</code>).
211         *
212         * @return The time period at the specified index.
213         * 
214         * @see #getDataItem(int)
215         */
216        public TimePeriod getTimePeriod(int index) {
217            return getDataItem(index).getPeriod();
218        }
219    
220        /**
221         * Returns the value at the specified index.
222         *
223         * @param index  the item index (in the range <code>0</code> to 
224         *     <code>getItemCount() - 1</code>).
225         *
226         * @return The value at the specified index (possibly <code>null</code>).
227         * 
228         * @see #getDataItem(int)
229         */
230        public Number getValue(int index) {
231            return getDataItem(index).getValue();
232        }
233    
234        /**
235         * Adds a data item to the series and sends a {@link SeriesChangeEvent} to
236         * all registered listeners.
237         *
238         * @param item  the item (<code>null</code> not permitted).
239         */
240        public void add(TimePeriodValue item) {
241            if (item == null) {
242                throw new IllegalArgumentException("Null item not allowed.");
243            }
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.minMiddleIndex).getPeriod().getStart()
299                    .getTime();
300                long e = getDataItem(this.minMiddleIndex).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        public boolean equals(Object obj) {
413            if (obj == this) {
414                return true;
415            }
416            if (!(obj instanceof TimePeriodValues)) {
417                return false;
418            }
419            if (!super.equals(obj)) {
420                return false;
421            }
422            TimePeriodValues that = (TimePeriodValues) obj;
423            if (!ObjectUtilities.equal(this.getDomainDescription(), 
424                    that.getDomainDescription())) {
425                return false;
426            }
427            if (!ObjectUtilities.equal(this.getRangeDescription(), 
428                    that.getRangeDescription())) {
429                return false;
430            }
431            int count = getItemCount();
432            if (count != that.getItemCount()) {
433                return false;
434            }
435            for (int i = 0; i < count; i++) {
436                if (!getDataItem(i).equals(that.getDataItem(i))) {
437                    return false;
438                }
439            }
440            return true;
441        }
442    
443        /**
444         * Returns a hash code value for the object.
445         *
446         * @return The hashcode
447         */
448        public int hashCode() {
449            int result;
450            result = (this.domain != null ? this.domain.hashCode() : 0);
451            result = 29 * result + (this.range != null ? this.range.hashCode() : 0);
452            result = 29 * result + this.data.hashCode();
453            result = 29 * result + this.minStartIndex;
454            result = 29 * result + this.maxStartIndex;
455            result = 29 * result + this.minMiddleIndex;
456            result = 29 * result + this.maxMiddleIndex;
457            result = 29 * result + this.minEndIndex;
458            result = 29 * result + this.maxEndIndex;
459            return result;
460        }
461    
462        /**
463         * Returns a clone of the collection.
464         * <P>
465         * Notes:
466         * <ul>
467         *   <li>no need to clone the domain and range descriptions, since String 
468         *       object is immutable;</li>
469         *   <li>we pass over to the more general method createCopy(start, end).
470         *   </li>
471         * </ul>
472         *
473         * @return A clone of the time series.
474         * 
475         * @throws CloneNotSupportedException if there is a cloning problem.
476         */
477        public Object clone() throws CloneNotSupportedException {
478            Object clone = createCopy(0, getItemCount() - 1);
479            return clone;
480        }
481    
482        /**
483         * Creates a new instance by copying a subset of the data in this 
484         * collection.
485         *
486         * @param start  the index of the first item to copy.
487         * @param end  the index of the last item to copy.
488         *
489         * @return A copy of a subset of the items.
490         * 
491         * @throws CloneNotSupportedException if there is a cloning problem.
492         */
493        public TimePeriodValues createCopy(int start, int end) 
494            throws CloneNotSupportedException {
495    
496            TimePeriodValues copy = (TimePeriodValues) super.clone();
497    
498            copy.data = new ArrayList();
499            if (this.data.size() > 0) {
500                for (int index = start; index <= end; index++) {
501                    TimePeriodValue item = (TimePeriodValue) this.data.get(index);
502                    TimePeriodValue clone = (TimePeriodValue) item.clone();
503                    try {
504                        copy.add(clone);
505                    }
506                    catch (SeriesException e) {
507                        System.err.println("Failed to add cloned item.");
508                    }
509                }
510            }
511            return copy;
512    
513        }
514        
515        /**
516         * Returns the index of the time period with the minimum start milliseconds.
517         * 
518         * @return The index.
519         */
520        public int getMinStartIndex() {
521            return this.minStartIndex;
522        }
523        
524        /**
525         * Returns the index of the time period with the maximum start milliseconds.
526         * 
527         * @return The index.
528         */
529        public int getMaxStartIndex() {
530            return this.maxStartIndex;
531        }
532    
533        /**
534         * Returns the index of the time period with the minimum middle 
535         * milliseconds.
536         * 
537         * @return The index.
538         */
539        public int getMinMiddleIndex() {
540            return this.minMiddleIndex;
541        }
542        
543        /**
544         * Returns the index of the time period with the maximum middle 
545         * milliseconds.
546         * 
547         * @return The index.
548         */
549        public int getMaxMiddleIndex() {
550            return this.maxMiddleIndex;
551        }
552    
553        /**
554         * Returns the index of the time period with the minimum end milliseconds.
555         * 
556         * @return The index.
557         */
558        public int getMinEndIndex() {
559            return this.minEndIndex;
560        }
561        
562        /**
563         * Returns the index of the time period with the maximum end milliseconds.
564         * 
565         * @return The index.
566         */
567        public int getMaxEndIndex() {
568            return this.maxEndIndex;
569        }
570    
571    }