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