/* ===========================================================
* JFreeChart : a free chart library for the Java(tm) platform
* ===========================================================
*
* (C) Copyright 2000-2011, by Object Refinery Limited and Contributors.
*
* Project Info: http://www.jfree.org/jfreechart/index.html
*
* This library is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* This library is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
* License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
* USA.
*
* [Oracle and Java are registered trademarks of Oracle and/or its affiliates.
* Other names may be trademarks of their respective owners.]
*
* -----------------------
* SegmentedTimeline.java
* -----------------------
* (C) Copyright 2003-2008, by Bill Kelemen and Contributors.
*
* Original Author: Bill Kelemen;
* Contributor(s): David Gilbert (for Object Refinery Limited);
*
* Changes
* -------
* 23-May-2003 : Version 1 (BK);
* 15-Aug-2003 : Implemented Cloneable (DG);
* 01-Jun-2004 : Modified to compile with JDK 1.2.2 (DG);
* 30-Sep-2004 : Replaced getTime().getTime() with getTimeInMillis() (DG);
* 04-Nov-2004 : Reverted change of 30-Sep-2004, won't work with JDK 1.3 (DG);
* 11-Jan-2005 : Removed deprecated code in preparation for 1.0.0 release (DG);
* ------------- JFREECHART 1.0.x ---------------------------------------------
* 14-Nov-2006 : Fix in toTimelineValue(long) to avoid stack overflow (DG);
* 02-Feb-2007 : Removed author tags all over JFreeChart sources (DG);
* 11-Jul-2007 : Fixed time zone bugs (DG);
* 06-Jun-2008 : Performance enhancement posted in forum (DG);
*
*/
package org.jfree.chart.axis;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.SimpleTimeZone;
import java.util.TimeZone;
/**
* A {@link Timeline} that implements a "segmented" timeline with included,
* excluded and exception segments.
* <P>
* A Timeline will present a series of values to be used for an axis. Each
* Timeline must provide transformation methods between domain values and
* timeline values.
* <P>
* A timeline can be used as parameter to a
* {@link org.jfree.chart.axis.DateAxis} to define the values that this axis
* supports. This class implements a timeline formed by segments of equal
* length (ex. days, hours, minutes) where some segments can be included in the
* timeline and others excluded. Therefore timelines like "working days" or
* "working hours" can be created where non-working days or non-working hours
* respectively can be removed from the timeline, and therefore from the axis.
* This creates a smooth plot with equal separation between all included
* segments.
* <P>
* Because Timelines were created mainly for Date related axis, values are
* represented as longs instead of doubles. In this case, the domain value is
* just the number of milliseconds since January 1, 1970, 00:00:00 GMT as
* defined by the getTime() method of {@link java.util.Date}.
* <P>
* In this class, a segment is defined as a unit of time of fixed length.
* Examples of segments are: days, hours, minutes, etc. The size of a segment
* is defined as the number of milliseconds in the segment. Some useful segment
* sizes are defined as constants in this class: DAY_SEGMENT_SIZE,
* HOUR_SEGMENT_SIZE, FIFTEEN_MINUTE_SEGMENT_SIZE and MINUTE_SEGMENT_SIZE.
* <P>
* Segments are group together to form a Segment Group. Each Segment Group will
* contain a number of Segments included and a number of Segments excluded. This
* Segment Group structure will repeat for the whole timeline.
* <P>
* For example, a working days SegmentedTimeline would be formed by a group of
* 7 daily segments, where there are 5 included (Monday through Friday) and 2
* excluded (Saturday and Sunday) segments.
* <P>
* Following is a diagram that explains the major attributes that define a
* segment. Each box is one segment and must be of fixed length (ms, second,
* hour, day, etc).
* <p>
* <pre>
* start time
* |
* v
* 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 ...
* +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+...
* | | | | | |EE|EE| | | | | |EE|EE| | | | | |EE|EE|
* +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+...
* \____________/ \___/ \_/
* \/ | |
* included excluded segment
* segments segments size
* \_________ _______/
* \/
* segment group
* </pre>
* Legend:<br>
* <space> = Included segment<br>
* EE = Excluded segments in the base timeline<br>
* <p>
* In the example, the following segment attributes are presented:
* <ul>
* <li>segment size: the size of each segment in ms.
* <li>start time: the start of the first segment of the first segment group to
* consider.
* <li>included segments: the number of segments to include in the group.
* <li>excluded segments: the number of segments to exclude in the group.
* </ul>
* <p>
* Exception Segments are allowed. These exception segments are defined as
* segments that would have been in the included segments of the Segment Group,
* but should be excluded for special reasons. In the previous working days
* SegmentedTimeline example, holidays would be considered exceptions.
* <P>
* Additionally the <code>startTime</code>, or start of the first Segment of
* the smallest segment group needs to be defined. This startTime could be
* relative to January 1, 1970, 00:00:00 GMT or any other date. This creates a
* point of reference to start counting Segment Groups. For example, for the
* working days SegmentedTimeline, the <code>startTime</code> could be
* 00:00:00 GMT of the first Monday after January 1, 1970. In this class, the
* constant FIRST_MONDAY_AFTER_1900 refers to a reference point of the first
* Monday of the last century.
* <p>
* A SegmentedTimeline can include a baseTimeline. This combination of
* timelines allows the creation of more complex timelines. For example, in
* order to implement a SegmentedTimeline for an intraday stock trading
* application, where the trading period is defined as 9:00 AM through 4:00 PM
* Monday through Friday, two SegmentedTimelines are used. The first one (the
* baseTimeline) would be a working day SegmentedTimeline (daily timeline
* Monday through Friday). On top of this baseTimeline, a second one is defined
* that maps the 9:00 AM to 4:00 PM period. Because the baseTimeline defines a
* timeline of Monday through Friday, the resulting (combined) timeline will
* expose the period 9:00 AM through 4:00 PM only on Monday through Friday,
* and will remove all other intermediate intervals.
* <P>
* Two factory methods newMondayThroughFridayTimeline() and
* newFifteenMinuteTimeline() are provided as examples to create special
* SegmentedTimelines.
*
* @see org.jfree.chart.axis.DateAxis
*/
public class SegmentedTimeline implements Timeline, Cloneable, Serializable {
/** For serialization. */
private static final long serialVersionUID = 1093779862539903110L;
////////////////////////////////////////////////////////////////////////////
// predetermined segments sizes
////////////////////////////////////////////////////////////////////////////
/** Defines a day segment size in ms. */
public static final long DAY_SEGMENT_SIZE = 24 * 60 * 60 * 1000;
/** Defines a one hour segment size in ms. */
public static final long HOUR_SEGMENT_SIZE = 60 * 60 * 1000;
/** Defines a 15-minute segment size in ms. */
public static final long FIFTEEN_MINUTE_SEGMENT_SIZE = 15 * 60 * 1000;
/** Defines a one-minute segment size in ms. */
public static final long MINUTE_SEGMENT_SIZE = 60 * 1000;
////////////////////////////////////////////////////////////////////////////
// other constants
////////////////////////////////////////////////////////////////////////////
/**
* Utility constant that defines the startTime as the first monday after
* 1/1/1970. This should be used when creating a SegmentedTimeline for
* Monday through Friday. See static block below for calculation of this
* constant.
*
* @deprecated As of 1.0.7. This field doesn't take into account changes
* to the default time zone.
*/
public static long FIRST_MONDAY_AFTER_1900;
/**
* Utility TimeZone object that has no DST and an offset equal to the
* default TimeZone. This allows easy arithmetic between days as each one
* will have equal size.
*
* @deprecated As of 1.0.7. This field is initialised based on the
* default time zone, and doesn't take into account subsequent
* changes to the default.
*/
public static TimeZone NO_DST_TIME_ZONE;
/**
* This is the default time zone where the application is running. See
* getTime() below where we make use of certain transformations between
* times in the default time zone and the no-dst time zone used for our
* calculations.
*
* @deprecated As of 1.0.7. When the default time zone is required,
* just call <code>TimeZone.getDefault()</code>.
*/
public static TimeZone DEFAULT_TIME_ZONE = TimeZone.getDefault();
/**
* This will be a utility calendar that has no DST but is shifted relative
* to the default time zone's offset.
*/
private Calendar workingCalendarNoDST;
/**
* This will be a utility calendar that used the default time zone.
*/
private Calendar workingCalendar = Calendar.getInstance();
////////////////////////////////////////////////////////////////////////////
// private attributes
////////////////////////////////////////////////////////////////////////////
/** Segment size in ms. */
private long segmentSize;
/** Number of consecutive segments to include in a segment group. */
private int segmentsIncluded;
/** Number of consecutive segments to exclude in a segment group. */
private int segmentsExcluded;
/** Number of segments in a group (segmentsIncluded + segmentsExcluded). */
private int groupSegmentCount;
/**
* Start of time reference from time zero (1/1/1970).
* This is the start of segment #0.
*/
private long startTime;
/** Consecutive ms in segmentsIncluded (segmentsIncluded * segmentSize). */
private long segmentsIncludedSize;
/** Consecutive ms in segmentsExcluded (segmentsExcluded * segmentSize). */
private long segmentsExcludedSize;
/** ms in a segment group (segmentsIncludedSize + segmentsExcludedSize). */
private long segmentsGroupSize;
/**
* List of exception segments (exceptions segments that would otherwise be
* included based on the periodic (included, excluded) grouping).
*/
private List exceptionSegments = new ArrayList();
/**
* This base timeline is used to specify exceptions at a higher level. For
* example, if we are a intraday timeline and want to exclude holidays,
* instead of having to exclude all intraday segments for the holiday,
* segments from this base timeline can be excluded. This baseTimeline is
* always optional and is only a convenience method.
* <p>
* Additionally, all excluded segments from this baseTimeline will be
* considered exceptions at this level.
*/
private SegmentedTimeline baseTimeline;
/** A flag that controls whether or not to adjust for daylight saving. */
private boolean adjustForDaylightSaving = false;
////////////////////////////////////////////////////////////////////////////
// static block
////////////////////////////////////////////////////////////////////////////
static {
// make a time zone with no DST for our Calendar calculations
int offset = TimeZone.getDefault().getRawOffset();
NO_DST_TIME_ZONE = new SimpleTimeZone(offset, "UTC-" + offset);
// calculate midnight of first monday after 1/1/1900 relative to
// current locale
Calendar cal = new GregorianCalendar(NO_DST_TIME_ZONE);
cal.set(1900, 0, 1, 0, 0, 0);
cal.set(Calendar.MILLISECOND, 0);
while (cal.get(Calendar.DAY_OF_WEEK) != Calendar.MONDAY) {
cal.add(Calendar.DATE, 1);
}
// FIRST_MONDAY_AFTER_1900 = cal.getTime().getTime();
// preceding code won't work with JDK 1.3
FIRST_MONDAY_AFTER_1900 = cal.getTime().getTime();
}
////////////////////////////////////////////////////////////////////////////
// constructors and factory methods
////////////////////////////////////////////////////////////////////////////
/**
* Constructs a new segmented timeline, optionaly using another segmented
* timeline as its base. This chaining of SegmentedTimelines allows further
* segmentation into smaller timelines.
*
* If a base
*
* @param segmentSize the size of a segment in ms. This time unit will be
* used to compute the included and excluded segments of the
* timeline.
* @param segmentsIncluded Number of consecutive segments to include.
* @param segmentsExcluded Number of consecutive segments to exclude.
*/
public SegmentedTimeline(long segmentSize,
int segmentsIncluded,
int segmentsExcluded) {
this.segmentSize = segmentSize;
this.segmentsIncluded = segmentsIncluded;
this.segmentsExcluded = segmentsExcluded;
this.groupSegmentCount = this.segmentsIncluded + this.segmentsExcluded;
this.segmentsIncludedSize = this.segmentsIncluded * this.segmentSize;
this.segmentsExcludedSize = this.segmentsExcluded * this.segmentSize;
this.segmentsGroupSize = this.segmentsIncludedSize
+ this.segmentsExcludedSize;
int offset = TimeZone.getDefault().getRawOffset();
TimeZone z = new SimpleTimeZone(offset, "UTC-" + offset);
this.workingCalendarNoDST = new GregorianCalendar(z,
Locale.getDefault());
}
/**
* Returns the milliseconds for midnight of the first Monday after
* 1-Jan-1900, ignoring daylight savings.
*
* @return The milliseconds.
*
* @since 1.0.7
*/
public static long firstMondayAfter1900() {
int offset = TimeZone.getDefault().getRawOffset();
TimeZone z = new SimpleTimeZone(offset, "UTC-" + offset);
// calculate midnight of first monday after 1/1/1900 relative to
// current locale
Calendar cal = new GregorianCalendar(z);
cal.set(1900, 0, 1, 0, 0, 0);
cal.set(Calendar.MILLISECOND, 0);
while (cal.get(Calendar.DAY_OF_WEEK) != Calendar.MONDAY) {
cal.add(Calendar.DATE, 1);
}
//return cal.getTimeInMillis();
// preceding code won't work with JDK 1.3
return cal.getTime().getTime();
}
/**
* Factory method to create a Monday through Friday SegmentedTimeline.
* <P>
* The <code>startTime</code> of the resulting timeline will be midnight
* of the first Monday after 1/1/1900.
*
* @return A fully initialized SegmentedTimeline.
*/
public static SegmentedTimeline newMondayThroughFridayTimeline() {
SegmentedTimeline timeline
= new SegmentedTimeline(DAY_SEGMENT_SIZE, 5, 2);
timeline.setStartTime(firstMondayAfter1900());
return timeline;
}
/**
* Factory method to create a 15-min, 9:00 AM thought 4:00 PM, Monday
* through Friday SegmentedTimeline.
* <P>
* This timeline uses a segmentSize of FIFTEEN_MIN_SEGMENT_SIZE. The
* segment group is defined as 28 included segments (9:00 AM through
* 4:00 PM) and 68 excluded segments (4:00 PM through 9:00 AM the next day).
* <P>
* In order to exclude Saturdays and Sundays it uses a baseTimeline that
* only includes Monday through Friday days.
* <P>
* The <code>startTime</code> of the resulting timeline will be 9:00 AM
* after the startTime of the baseTimeline. This will correspond to 9:00 AM
* of the first Monday after 1/1/1900.
*
* @return A fully initialized SegmentedTimeline.
*/
public static SegmentedTimeline newFifteenMinuteTimeline() {
SegmentedTimeline timeline = new SegmentedTimeline(
FIFTEEN_MINUTE_SEGMENT_SIZE, 28, 68);
timeline.setStartTime(firstMondayAfter1900() + 36
* timeline.getSegmentSize());
timeline.setBaseTimeline(newMondayThroughFridayTimeline());
return timeline;
}
/**
* Returns the flag that controls whether or not the daylight saving
* adjustment is applied.
*
* @return A boolean.
*/
public boolean getAdjustForDaylightSaving() {
return this.adjustForDaylightSaving;
}
/**
* Sets the flag that controls whether or not the daylight saving adjustment
* is applied.
*
* @param adjust the flag.
*/
public void setAdjustForDaylightSaving(boolean adjust) {
this.adjustForDaylightSaving = adjust;
}
////////////////////////////////////////////////////////////////////////////
// operations
////////////////////////////////////////////////////////////////////////////
/**
* Sets the start time for the timeline. This is the beginning of segment
* zero.
*
* @param millisecond the start time (encoded as in java.util.Date).
*/
public void setStartTime(long millisecond) {
this.startTime = millisecond;
}
/**
* Returns the start time for the timeline. This is the beginning of
* segment zero.
*
* @return The start time.
*/
public long getStartTime() {
return this.startTime;
}
/**
* Returns the number of segments excluded per segment group.
*
* @return The number of segments excluded.
*/
public int getSegmentsExcluded() {
return this.segmentsExcluded;
}
/**
* Returns the size in milliseconds of the segments excluded per segment
* group.
*
* @return The size in milliseconds.
*/
public long getSegmentsExcludedSize() {
return this.segmentsExcludedSize;
}
/**
* Returns the number of segments in a segment group. This will be equal to
* segments included plus segments excluded.
*
* @return The number of segments.
*/
public int getGroupSegmentCount() {
return this.groupSegmentCount;
}
/**
* Returns the size in milliseconds of a segment group. This will be equal
* to size of the segments included plus the size of the segments excluded.
*
* @return The segment group size in milliseconds.
*/
public long getSegmentsGroupSize() {
return this.segmentsGroupSize;
}
/**
* Returns the number of segments included per segment group.
*
* @return The number of segments.
*/
public int getSegmentsIncluded() {
return this.segmentsIncluded;
}
/**
* Returns the size in ms of the segments included per segment group.
*
* @return The segment size in milliseconds.
*/
public long getSegmentsIncludedSize() {
return this.segmentsIncludedSize;
}
/**
* Returns the size of one segment in ms.
*
* @return The segment size in milliseconds.
*/
public long getSegmentSize() {
return this.segmentSize;
}
/**
* Returns a list of all the exception segments. This list is not
* modifiable.
*
* @return The exception segments.
*/
public List getExceptionSegments() {
return Collections.unmodifiableList(this.exceptionSegments);
}
/**
* Sets the exception segments list.
*
* @param exceptionSegments the exception segments.
*/
public void setExceptionSegments(List exceptionSegments) {
this.exceptionSegments = exceptionSegments;
}
/**
* Returns our baseTimeline, or <code>null</code> if none.
*
* @return The base timeline.
*/
public SegmentedTimeline getBaseTimeline() {
return this.baseTimeline;
}
/**
* Sets the base timeline.
*
* @param baseTimeline the timeline.
*/
public void setBaseTimeline(SegmentedTimeline baseTimeline) {
// verify that baseTimeline is compatible with us
if (baseTimeline != null) {
if (baseTimeline.getSegmentSize() < this.segmentSize) {
throw new IllegalArgumentException(
"baseTimeline.getSegmentSize() "
+ "is smaller than segmentSize");
}
else if (baseTimeline.getStartTime() > this.startTime) {
throw new IllegalArgumentException(
"baseTimeline.getStartTime() is after startTime");
}
else if ((baseTimeline.getSegmentSize() % this.segmentSize) != 0) {
throw new IllegalArgumentException(
"baseTimeline.getSegmentSize() is not multiple of "
+ "segmentSize");
}
else if (((this.startTime
- baseTimeline.getStartTime()) % this.segmentSize) != 0) {
throw new IllegalArgumentException(
"baseTimeline is not aligned");
}
}
this.baseTimeline = baseTimeline;
}
/**
* Translates a value relative to the domain value (all Dates) into a value
* relative to the segmented timeline. The values relative to the segmented
* timeline are all consecutives starting at zero at the startTime.
*
* @param millisecond the millisecond (as encoded by java.util.Date).
*
* @return The timeline value.
*/
public long toTimelineValue(long millisecond) {
long result;
long rawMilliseconds = millisecond - this.startTime;
long groupMilliseconds = rawMilliseconds % this.segmentsGroupSize;
long groupIndex = rawMilliseconds / this.segmentsGroupSize;
if (groupMilliseconds >= this.segmentsIncludedSize) {
result = toTimelineValue(this.startTime + this.segmentsGroupSize
* (groupIndex + 1));
}
else {
Segment segment = getSegment(millisecond);
if (segment.inExceptionSegments()) {
int p;
while ((p = binarySearchExceptionSegments(segment)) >= 0) {
segment = getSegment(millisecond = ((Segment)
this.exceptionSegments.get(p)).getSegmentEnd() + 1);
}
result = toTimelineValue(millisecond);
}
else {
long shiftedSegmentedValue = millisecond - this.startTime;
long x = shiftedSegmentedValue % this.segmentsGroupSize;
long y = shiftedSegmentedValue / this.segmentsGroupSize;
long wholeExceptionsBeforeDomainValue =
getExceptionSegmentCount(this.startTime, millisecond - 1);
// long partialTimeInException = 0;
// Segment ss = getSegment(millisecond);
// if (ss.inExceptionSegments()) {
// partialTimeInException = millisecond
// - ss.getSegmentStart();
// }
if (x < this.segmentsIncludedSize) {
result = this.segmentsIncludedSize * y
+ x - wholeExceptionsBeforeDomainValue
* this.segmentSize;
// - partialTimeInException;
}
else {
result = this.segmentsIncludedSize * (y + 1)
- wholeExceptionsBeforeDomainValue
* this.segmentSize;
// - partialTimeInException;
}
}
}
return result;
}
/**
* Translates a date into a value relative to the segmented timeline. The
* values relative to the segmented timeline are all consecutives starting
* at zero at the startTime.
*
* @param date date relative to the domain.
*
* @return The timeline value (in milliseconds).
*/
public long toTimelineValue(Date date) {
return toTimelineValue(getTime(date));
//return toTimelineValue(dateDomainValue.getTime());
}
/**
* Translates a value relative to the timeline into a millisecond.
*
* @param timelineValue the timeline value (in milliseconds).
*
* @return The domain value (in milliseconds).
*/
public long toMillisecond(long timelineValue) {
// calculate the result as if no exceptions
Segment result = new Segment(this.startTime + timelineValue
+ (timelineValue / this.segmentsIncludedSize)
* this.segmentsExcludedSize);
long lastIndex = this.startTime;
// adjust result for any exceptions in the result calculated
while (lastIndex <= result.segmentStart) {
// skip all whole exception segments in the range
long exceptionSegmentCount;
while ((exceptionSegmentCount = getExceptionSegmentCount(
lastIndex, (result.millisecond / this.segmentSize)
* this.segmentSize - 1)) > 0
) {
lastIndex = result.segmentStart;
// move forward exceptionSegmentCount segments skipping
// excluded segments
for (int i = 0; i < exceptionSegmentCount; i++) {
do {
result.inc();
}
while (result.inExcludeSegments());
}
}
lastIndex = result.segmentStart;
// skip exception or excluded segments we may fall on
while (result.inExceptionSegments() || result.inExcludeSegments()) {
result.inc();
lastIndex += this.segmentSize;
}
lastIndex++;
}
return getTimeFromLong(result.millisecond);
}
/**
* Converts a date/time value to take account of daylight savings time.
*
* @param date the milliseconds.
*
* @return The milliseconds.
*/
public long getTimeFromLong(long date) {
long result = date;
if (this.adjustForDaylightSaving) {
this.workingCalendarNoDST.setTime(new Date(date));
this.workingCalendar.set(
this.workingCalendarNoDST.get(Calendar.YEAR),
this.workingCalendarNoDST.get(Calendar.MONTH),
this.workingCalendarNoDST.get(Calendar.DATE),
this.workingCalendarNoDST.get(Calendar.HOUR_OF_DAY),
this.workingCalendarNoDST.get(Calendar.MINUTE),
this.workingCalendarNoDST.get(Calendar.SECOND)
);
this.workingCalendar.set(Calendar.MILLISECOND,
this.workingCalendarNoDST.get(Calendar.MILLISECOND));
// result = this.workingCalendar.getTimeInMillis();
// preceding code won't work with JDK 1.3
result = this.workingCalendar.getTime().getTime();
}
return result;
}
/**
* Returns <code>true</code> if a value is contained in the timeline.
*
* @param millisecond the value to verify.
*
* @return <code>true</code> if value is contained in the timeline.
*/
public boolean containsDomainValue(long millisecond) {
Segment segment = getSegment(millisecond);
return segment.inIncludeSegments();
}
/**
* Returns <code>true</code> if a value is contained in the timeline.
*
* @param date date to verify
*
* @return <code>true</code> if value is contained in the timeline
*/
public boolean containsDomainValue(Date date) {
return containsDomainValue(getTime(date));
}
/**
* Returns <code>true</code> if a range of values are contained in the
* timeline. This is implemented verifying that all segments are in the
* range.
*
* @param domainValueStart start of the range to verify
* @param domainValueEnd end of the range to verify
*
* @return <code>true</code> if the range is contained in the timeline
*/
public boolean containsDomainRange(long domainValueStart,
long domainValueEnd) {
if (domainValueEnd < domainValueStart) {
throw new IllegalArgumentException(
"domainValueEnd (" + domainValueEnd
+ ") < domainValueStart (" + domainValueStart + ")");
}
Segment segment = getSegment(domainValueStart);
boolean contains = true;
do {
contains = (segment.inIncludeSegments());
if (segment.contains(domainValueEnd)) {
break;
}
else {
segment.inc();
}
}
while (contains);
return (contains);
}
/**
* Returns <code>true</code> if a range of values are contained in the
* timeline. This is implemented verifying that all segments are in the
* range.
*
* @param dateDomainValueStart start of the range to verify
* @param dateDomainValueEnd end of the range to verify
*
* @return <code>true</code> if the range is contained in the timeline
*/
public boolean containsDomainRange(Date dateDomainValueStart,
Date dateDomainValueEnd) {
return containsDomainRange(getTime(dateDomainValueStart),
getTime(dateDomainValueEnd));
}
/**
* Adds a segment as an exception. An exception segment is defined as a
* segment to exclude from what would otherwise be considered a valid
* segment of the timeline. An exception segment can not be contained
* inside an already excluded segment. If so, no action will occur (the
* proposed exception segment will be discarded).
* <p>
* The segment is identified by a domainValue into any part of the segment.
* Therefore the segmentStart <= domainValue <= segmentEnd.
*
* @param millisecond domain value to treat as an exception
*/
public void addException(long millisecond) {
addException(new Segment(millisecond));
}
/**
* Adds a segment range as an exception. An exception segment is defined as
* a segment to exclude from what would otherwise be considered a valid
* segment of the timeline. An exception segment can not be contained
* inside an already excluded segment. If so, no action will occur (the
* proposed exception segment will be discarded).
* <p>
* The segment range is identified by a domainValue that begins a valid
* segment and ends with a domainValue that ends a valid segment.
* Therefore the range will contain all segments whose segmentStart
* <= domainValue and segmentEnd <= toDomainValue.
*
* @param fromDomainValue start of domain range to treat as an exception
* @param toDomainValue end of domain range to treat as an exception
*/
public void addException(long fromDomainValue, long toDomainValue) {
addException(new SegmentRange(fromDomainValue, toDomainValue));
}
/**
* Adds a segment as an exception. An exception segment is defined as a
* segment to exclude from what would otherwise be considered a valid
* segment of the timeline. An exception segment can not be contained
* inside an already excluded segment. If so, no action will occur (the
* proposed exception segment will be discarded).
* <p>
* The segment is identified by a Date into any part of the segment.
*
* @param exceptionDate Date into the segment to exclude.
*/
public void addException(Date exceptionDate) {
addException(getTime(exceptionDate));
//addException(exceptionDate.getTime());
}
/**
* Adds a list of dates as segment exceptions. Each exception segment is
* defined as a segment to exclude from what would otherwise be considered
* a valid segment of the timeline. An exception segment can not be
* contained inside an already excluded segment. If so, no action will
* occur (the proposed exception segment will be discarded).
* <p>
* The segment is identified by a Date into any part of the segment.
*
* @param exceptionList List of Date objects that identify the segments to
* exclude.
*/
public void addExceptions(List exceptionList) {
for (Iterator iter = exceptionList.iterator(); iter.hasNext();) {
addException((Date) iter.next());
}
}
/**
* Adds a segment as an exception. An exception segment is defined as a
* segment to exclude from what would otherwise be considered a valid
* segment of the timeline. An exception segment can not be contained
* inside an already excluded segment. This is verified inside this
* method, and if so, no action will occur (the proposed exception segment
* will be discarded).
*
* @param segment the segment to exclude.
*/
private void addException(Segment segment) {
if (segment.inIncludeSegments()) {
int p = binarySearchExceptionSegments(segment);
this.exceptionSegments.add(-(p + 1), segment);
}
}
/**
* Adds a segment relative to the baseTimeline as an exception. Because a
* base segment is normally larger than our segments, this may add one or
* more segment ranges to the exception list.
* <p>
* An exception segment is defined as a segment
* to exclude from what would otherwise be considered a valid segment of
* the timeline. An exception segment can not be contained inside an
* already excluded segment. If so, no action will occur (the proposed
* exception segment will be discarded).
* <p>
* The segment is identified by a domainValue into any part of the
* baseTimeline segment.
*
* @param domainValue domain value to teat as a baseTimeline exception.
*/
public void addBaseTimelineException(long domainValue) {
Segment baseSegment = this.baseTimeline.getSegment(domainValue);
if (baseSegment.inIncludeSegments()) {
// cycle through all the segments contained in the BaseTimeline
// exception segment
Segment segment = getSegment(baseSegment.getSegmentStart());
while (segment.getSegmentStart() <= baseSegment.getSegmentEnd()) {
if (segment.inIncludeSegments()) {
// find all consecutive included segments
long fromDomainValue = segment.getSegmentStart();
long toDomainValue;
do {
toDomainValue = segment.getSegmentEnd();
segment.inc();
}
while (segment.inIncludeSegments());
// add the interval as an exception
addException(fromDomainValue, toDomainValue);
}
else {
// this is not one of our included segment, skip it
segment.inc();
}
}
}
}
/**
* Adds a segment relative to the baseTimeline as an exception. An
* exception segment is defined as a segment to exclude from what would
* otherwise be considered a valid segment of the timeline. An exception
* segment can not be contained inside an already excluded segment. If so,
* no action will occure (the proposed exception segment will be discarded).
* <p>
* The segment is identified by a domainValue into any part of the segment.
* Therefore the segmentStart <= domainValue <= segmentEnd.
*
* @param date date domain value to treat as a baseTimeline exception
*/
public void addBaseTimelineException(Date date) {
addBaseTimelineException(getTime(date));
}
/**
* Adds all excluded segments from the BaseTimeline as exceptions to our
* timeline. This allows us to combine two timelines for more complex
* calculations.
*
* @param fromBaseDomainValue Start of the range where exclusions will be
* extracted.
* @param toBaseDomainValue End of the range to process.
*/
public void addBaseTimelineExclusions(long fromBaseDomainValue,
long toBaseDomainValue) {
// find first excluded base segment starting fromDomainValue
Segment baseSegment = this.baseTimeline.getSegment(fromBaseDomainValue);
while (baseSegment.getSegmentStart() <= toBaseDomainValue
&& !baseSegment.inExcludeSegments()) {
baseSegment.inc();
}
// cycle over all the base segments groups in the range
while (baseSegment.getSegmentStart() <= toBaseDomainValue) {
long baseExclusionRangeEnd = baseSegment.getSegmentStart()
+ this.baseTimeline.getSegmentsExcluded()
* this.baseTimeline.getSegmentSize() - 1;
// cycle through all the segments contained in the base exclusion
// area
Segment segment = getSegment(baseSegment.getSegmentStart());
while (segment.getSegmentStart() <= baseExclusionRangeEnd) {
if (segment.inIncludeSegments()) {
// find all consecutive included segments
long fromDomainValue = segment.getSegmentStart();
long toDomainValue;
do {
toDomainValue = segment.getSegmentEnd();
segment.inc();
}
while (segment.inIncludeSegments());
// add the interval as an exception
addException(new BaseTimelineSegmentRange(
fromDomainValue, toDomainValue));
}
else {
// this is not one of our included segment, skip it
segment.inc();
}
}
// go to next base segment group
baseSegment.inc(this.baseTimeline.getGroupSegmentCount());
}
}
/**
* Returns the number of exception segments wholly contained in the
* (fromDomainValue, toDomainValue) interval.
*
* @param fromMillisecond the beginning of the interval.
* @param toMillisecond the end of the interval.
*
* @return Number of exception segments contained in the interval.
*/
public long getExceptionSegmentCount(long fromMillisecond,
long toMillisecond) {
if (toMillisecond < fromMillisecond) {
return (0);
}
int n = 0;
for (Iterator iter = this.exceptionSegments.iterator();
iter.hasNext();) {
Segment segment = (Segment) iter.next();
Segment intersection = segment.intersect(fromMillisecond,
toMillisecond);
if (intersection != null) {
n += intersection.getSegmentCount();
}
}
return (n);
}
/**
* Returns a segment that contains a domainValue. If the domainValue is
* not contained in the timeline (because it is not contained in the
* baseTimeline), a Segment that contains
* <code>index + segmentSize*m</code> will be returned for the smallest
* <code>m</code> possible.
*
* @param millisecond index into the segment
*
* @return A Segment that contains index, or the next possible Segment.
*/
public Segment getSegment(long millisecond) {
return new Segment(millisecond);
}
/**
* Returns a segment that contains a date. For accurate calculations,
* the calendar should use TIME_ZONE for its calculation (or any other
* similar time zone).
*
* If the date is not contained in the timeline (because it is not
* contained in the baseTimeline), a Segment that contains
* <code>date + segmentSize*m</code> will be returned for the smallest
* <code>m</code> possible.
*
* @param date date into the segment
*
* @return A Segment that contains date, or the next possible Segment.
*/
public Segment getSegment(Date date) {
return (getSegment(getTime(date)));
}
/**
* Convenient method to test equality in two objects, taking into account
* nulls.
*
* @param o first object to compare
* @param p second object to compare
*
* @return <code>true</code> if both objects are equal or both
* <code>null</code>, <code>false</code> otherwise.
*/
private boolean equals(Object o, Object p) {
return (o == p || ((o != null) && o.equals(p)));
}
/**
* Returns true if we are equal to the parameter
*
* @param o Object to verify with us
*
* @return <code>true</code> or <code>false</code>
*/
public boolean equals(Object o) {
if (o instanceof SegmentedTimeline) {
SegmentedTimeline other = (SegmentedTimeline) o;
boolean b0 = (this.segmentSize == other.getSegmentSize());
boolean b1 = (this.segmentsIncluded == other.getSegmentsIncluded());
boolean b2 = (this.segmentsExcluded == other.getSegmentsExcluded());
boolean b3 = (this.startTime == other.getStartTime());
boolean b4 = equals(this.exceptionSegments,
other.getExceptionSegments());
return b0 && b1 && b2 && b3 && b4;
}
else {
return (false);
}
}
/**
* Returns a hash code for this object.
*
* @return A hash code.
*/
public int hashCode() {
int result = 19;
result = 37 * result
+ (int) (this.segmentSize ^ (this.segmentSize >>> 32));
result = 37 * result + (int) (this.startTime ^ (this.startTime >>> 32));
return result;
}
/**
* Preforms a binary serach in the exceptionSegments sorted array. This
* array can contain Segments or SegmentRange objects.
*
* @param segment the key to be searched for.
*
* @return index of the search segment, if it is contained in the list;
* otherwise, <tt>(-(<i>insertion point</i>) - 1)</tt>. The
* <i>insertion point</i> is defined as the point at which the
* segment would be inserted into the list: the index of the first
* element greater than the key, or <tt>list.size()</tt>, if all
* elements in the list are less than the specified segment. Note
* that this guarantees that the return value will be >= 0 if
* and only if the key is found.
*/
private int binarySearchExceptionSegments(Segment segment) {
int low = 0;
int high = this.exceptionSegments.size() - 1;
while (low <= high) {
int mid = (low + high) / 2;
Segment midSegment = (Segment) this.exceptionSegments.get(mid);
// first test for equality (contains or contained)
if (segment.contains(midSegment) || midSegment.contains(segment)) {
return mid;
}
if (midSegment.before(segment)) {
low = mid + 1;
}
else if (midSegment.after(segment)) {
high = mid - 1;
}
else {
throw new IllegalStateException("Invalid condition.");
}
}
return -(low + 1); // key not found
}
/**
* Special method that handles conversion between the Default Time Zone and
* a UTC time zone with no DST. This is needed so all days have the same
* size. This method is the prefered way of converting a Data into
* milliseconds for usage in this class.
*
* @param date Date to convert to long.
*
* @return The milliseconds.
*/
public long getTime(Date date) {
long result = date.getTime();
if (this.adjustForDaylightSaving) {
this.workingCalendar.setTime(date);
this.workingCalendarNoDST.set(
this.workingCalendar.get(Calendar.YEAR),
this.workingCalendar.get(Calendar.MONTH),
this.workingCalendar.get(Calendar.DATE),
this.workingCalendar.get(Calendar.HOUR_OF_DAY),
this.workingCalendar.get(Calendar.MINUTE),
this.workingCalendar.get(Calendar.SECOND));
this.workingCalendarNoDST.set(Calendar.MILLISECOND,
this.workingCalendar.get(Calendar.MILLISECOND));
Date revisedDate = this.workingCalendarNoDST.getTime();
result = revisedDate.getTime();
}
return result;
}
/**
* Converts a millisecond value into a {@link Date} object.
*
* @param value the millisecond value.
*
* @return The date.
*/
public Date getDate(long value) {
this.workingCalendarNoDST.setTime(new Date(value));
return (this.workingCalendarNoDST.getTime());
}
/**
* Returns a clone of the timeline.
*
* @return A clone.
*
* @throws CloneNotSupportedException ??.
*/
public Object clone() throws CloneNotSupportedException {
SegmentedTimeline clone = (SegmentedTimeline) super.clone();
return clone;
}
/**
* Internal class to represent a valid segment for this timeline. A segment
* is valid on a timeline if it is part of its included, excluded or
* exception segments.
* <p>
* Each segment will know its segment number, segmentStart, segmentEnd and
* index inside the segment.
*/
public class Segment implements Comparable, Cloneable, Serializable {
/** The segment number. */
protected long segmentNumber;
/** The segment start. */
protected long segmentStart;
/** The segment end. */
protected long segmentEnd;
/** A reference point within the segment. */
protected long millisecond;
/**
* Protected constructor only used by sub-classes.
*/
protected Segment() {
// empty
}
/**
* Creates a segment for a given point in time.
*
* @param millisecond the millisecond (as encoded by java.util.Date).
*/
protected Segment(long millisecond) {
this.segmentNumber = calculateSegmentNumber(millisecond);
this.segmentStart = SegmentedTimeline.this.startTime
+ this.segmentNumber * SegmentedTimeline.this.segmentSize;
this.segmentEnd
= this.segmentStart + SegmentedTimeline.this.segmentSize - 1;
this.millisecond = millisecond;
}
/**
* Calculates the segment number for a given millisecond.
*
* @param millis the millisecond (as encoded by java.util.Date).
*
* @return The segment number.
*/
public long calculateSegmentNumber(long millis) {
if (millis >= SegmentedTimeline.this.startTime) {
return (millis - SegmentedTimeline.this.startTime)
/ SegmentedTimeline.this.segmentSize;
}
else {
return ((millis - SegmentedTimeline.this.startTime)
/ SegmentedTimeline.this.segmentSize) - 1;
}
}
/**
* Returns the segment number of this segment. Segments start at 0.
*
* @return The segment number.
*/
public long getSegmentNumber() {
return this.segmentNumber;
}
/**
* Returns always one (the number of segments contained in this
* segment).
*
* @return The segment count (always 1 for this class).
*/
public long getSegmentCount() {
return 1;
}
/**
* Gets the start of this segment in ms.
*
* @return The segment start.
*/
public long getSegmentStart() {
return this.segmentStart;
}
/**
* Gets the end of this segment in ms.
*
* @return The segment end.
*/
public long getSegmentEnd() {
return this.segmentEnd;
}
/**
* Returns the millisecond used to reference this segment (always
* between the segmentStart and segmentEnd).
*
* @return The millisecond.
*/
public long getMillisecond() {
return this.millisecond;
}
/**
* Returns a {@link java.util.Date} that represents the reference point
* for this segment.
*
* @return The date.
*/
public Date getDate() {
return SegmentedTimeline.this.getDate(this.millisecond);
}
/**
* Returns true if a particular millisecond is contained in this
* segment.
*
* @param millis the millisecond to verify.
*
* @return <code>true</code> if the millisecond is contained in the
* segment.
*/
public boolean contains(long millis) {
return (this.segmentStart <= millis && millis <= this.segmentEnd);
}
/**
* Returns <code>true</code> if an interval is contained in this
* segment.
*
* @param from the start of the interval.
* @param to the end of the interval.
*
* @return <code>true</code> if the interval is contained in the
* segment.
*/
public boolean contains(long from, long to) {
return (this.segmentStart <= from && to <= this.segmentEnd);
}
/**
* Returns <code>true</code> if a segment is contained in this segment.
*
* @param segment the segment to test for inclusion
*
* @return <code>true</code> if the segment is contained in this
* segment.
*/
public boolean contains(Segment segment) {
return contains(segment.getSegmentStart(), segment.getSegmentEnd());
}
/**
* Returns <code>true</code> if this segment is contained in an
* interval.
*
* @param from the start of the interval.
* @param to the end of the interval.
*
* @return <code>true</code> if this segment is contained in the
* interval.
*/
public boolean contained(long from, long to) {
return (from <= this.segmentStart && this.segmentEnd <= to);
}
/**
* Returns a segment that is the intersection of this segment and the
* interval.
*
* @param from the start of the interval.
* @param to the end of the interval.
*
* @return A segment.
*/
public Segment intersect(long from, long to) {
if (from <= this.segmentStart && this.segmentEnd <= to) {
return this;
}
else {
return null;
}
}
/**
* Returns <code>true</code> if this segment is wholly before another
* segment.
*
* @param other the other segment.
*
* @return A boolean.
*/
public boolean before(Segment other) {
return (this.segmentEnd < other.getSegmentStart());
}
/**
* Returns <code>true</code> if this segment is wholly after another
* segment.
*
* @param other the other segment.
*
* @return A boolean.
*/
public boolean after(Segment other) {
return (this.segmentStart > other.getSegmentEnd());
}
/**
* Tests an object (usually another <code>Segment</code>) for equality
* with this segment.
*
* @param object The other segment to compare with us
*
* @return <code>true</code> if we are the same segment
*/
public boolean equals(Object object) {
if (object instanceof Segment) {
Segment other = (Segment) object;
return (this.segmentNumber == other.getSegmentNumber()
&& this.segmentStart == other.getSegmentStart()
&& this.segmentEnd == other.getSegmentEnd()
&& this.millisecond == other.getMillisecond());
}
else {
return false;
}
}
/**
* Returns a copy of ourselves or <code>null</code> if there was an
* exception during cloning.
*
* @return A copy of this segment.
*/
public Segment copy() {
try {
return (Segment) this.clone();
}
catch (CloneNotSupportedException e) {
return null;
}
}
/**
* Will compare this Segment with another Segment (from Comparable
* interface).
*
* @param object The other Segment to compare with
*
* @return -1: this < object, 0: this.equal(object) and
* +1: this > object
*/
public int compareTo(Object object) {
Segment other = (Segment) object;
if (this.before(other)) {
return -1;
}
else if (this.after(other)) {
return +1;
}
else {
return 0;
}
}
/**
* Returns true if we are an included segment and we are not an
* exception.
*
* @return <code>true</code> or <code>false</code>.
*/
public boolean inIncludeSegments() {
if (getSegmentNumberRelativeToGroup()
< SegmentedTimeline.this.segmentsIncluded) {
return !inExceptionSegments();
}
else {
return false;
}
}
/**
* Returns true if we are an excluded segment.
*
* @return <code>true</code> or <code>false</code>.
*/
public boolean inExcludeSegments() {
return getSegmentNumberRelativeToGroup()
>= SegmentedTimeline.this.segmentsIncluded;
}
/**
* Calculate the segment number relative to the segment group. This
* will be a number between 0 and segmentsGroup-1. This value is
* calculated from the segmentNumber. Special care is taken for
* negative segmentNumbers.
*
* @return The segment number.
*/
private long getSegmentNumberRelativeToGroup() {
long p = (this.segmentNumber
% SegmentedTimeline.this.groupSegmentCount);
if (p < 0) {
p += SegmentedTimeline.this.groupSegmentCount;
}
return p;
}
/**
* Returns true if we are an exception segment. This is implemented via
* a binary search on the exceptionSegments sorted list.
*
* If the segment is not listed as an exception in our list and we have
* a baseTimeline, a check is performed to see if the segment is inside
* an excluded segment from our base. If so, it is also considered an
* exception.
*
* @return <code>true</code> if we are an exception segment.
*/
public boolean inExceptionSegments() {
return binarySearchExceptionSegments(this) >= 0;
}
/**
* Increments the internal attributes of this segment by a number of
* segments.
*
* @param n Number of segments to increment.
*/
public void inc(long n) {
this.segmentNumber += n;
long m = n * SegmentedTimeline.this.segmentSize;
this.segmentStart += m;
this.segmentEnd += m;
this.millisecond += m;
}
/**
* Increments the internal attributes of this segment by one segment.
* The exact time incremented is segmentSize.
*/
public void inc() {
inc(1);
}
/**
* Decrements the internal attributes of this segment by a number of
* segments.
*
* @param n Number of segments to decrement.
*/
public void dec(long n) {
this.segmentNumber -= n;
long m = n * SegmentedTimeline.this.segmentSize;
this.segmentStart -= m;
this.segmentEnd -= m;
this.millisecond -= m;
}
/**
* Decrements the internal attributes of this segment by one segment.
* The exact time decremented is segmentSize.
*/
public void dec() {
dec(1);
}
/**
* Moves the index of this segment to the beginning if the segment.
*/
public void moveIndexToStart() {
this.millisecond = this.segmentStart;
}
/**
* Moves the index of this segment to the end of the segment.
*/
public void moveIndexToEnd() {
this.millisecond = this.segmentEnd;
}
}
/**
* Private internal class to represent a range of segments. This class is
* mainly used to store in one object a range of exception segments. This
* optimizes certain timelines that use a small segment size (like an
* intraday timeline) allowing them to express a day exception as one
* SegmentRange instead of multi Segments.
*/
protected class SegmentRange extends Segment {
/** The number of segments in the range. */
private long segmentCount;
/**
* Creates a SegmentRange between a start and end domain values.
*
* @param fromMillisecond start of the range
* @param toMillisecond end of the range
*/
public SegmentRange(long fromMillisecond, long toMillisecond) {
Segment start = getSegment(fromMillisecond);
Segment end = getSegment(toMillisecond);
// if (start.getSegmentStart() != fromMillisecond
// || end.getSegmentEnd() != toMillisecond) {
// throw new IllegalArgumentException("Invalid Segment Range ["
// + fromMillisecond + "," + toMillisecond + "]");
// }
this.millisecond = fromMillisecond;
this.segmentNumber = calculateSegmentNumber(fromMillisecond);
this.segmentStart = start.segmentStart;
this.segmentEnd = end.segmentEnd;
this.segmentCount
= (end.getSegmentNumber() - start.getSegmentNumber() + 1);
}
/**
* Returns the number of segments contained in this range.
*
* @return The segment count.
*/
public long getSegmentCount() {
return this.segmentCount;
}
/**
* Returns a segment that is the intersection of this segment and the
* interval.
*
* @param from the start of the interval.
* @param to the end of the interval.
*
* @return The intersection.
*/
public Segment intersect(long from, long to) {
// Segment fromSegment = getSegment(from);
// fromSegment.inc();
// Segment toSegment = getSegment(to);
// toSegment.dec();
long start = Math.max(from, this.segmentStart);
long end = Math.min(to, this.segmentEnd);
// long start = Math.max(
// fromSegment.getSegmentStart(), this.segmentStart
// );
// long end = Math.min(toSegment.getSegmentEnd(), this.segmentEnd);
if (start <= end) {
return new SegmentRange(start, end);
}
else {
return null;
}
}
/**
* Returns true if all Segments of this SegmentRenge are an included
* segment and are not an exception.
*
* @return <code>true</code> or </code>false</code>.
*/
public boolean inIncludeSegments() {
for (Segment segment = getSegment(this.segmentStart);
segment.getSegmentStart() < this.segmentEnd;
segment.inc()) {
if (!segment.inIncludeSegments()) {
return (false);
}
}
return true;
}
/**
* Returns true if we are an excluded segment.
*
* @return <code>true</code> or </code>false</code>.
*/
public boolean inExcludeSegments() {
for (Segment segment = getSegment(this.segmentStart);
segment.getSegmentStart() < this.segmentEnd;
segment.inc()) {
if (!segment.inExceptionSegments()) {
return (false);
}
}
return true;
}
/**
* Not implemented for SegmentRange. Always throws
* IllegalArgumentException.
*
* @param n Number of segments to increment.
*/
public void inc(long n) {
throw new IllegalArgumentException(
"Not implemented in SegmentRange");
}
}
/**
* Special <code>SegmentRange</code> that came from the BaseTimeline.
*/
protected class BaseTimelineSegmentRange extends SegmentRange {
/**
* Constructor.
*
* @param fromDomainValue the start value.
* @param toDomainValue the end value.
*/
public BaseTimelineSegmentRange(long fromDomainValue,
long toDomainValue) {
super(fromDomainValue, toDomainValue);
}
}
}
|