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