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