www.pudn.com > jfreechart-0.9.12.zip > SegmentedTimeline.java


/* ====================================== 
 * JFreeChart : a free Java chart library 
 * ====================================== 
 * 
 * Project Info:  http://www.jfree.org/jfreechart/index.html 
 * Project Lead:  David Gilbert (david.gilbert@object-refinery.com); 
 * 
 * (C) Copyright 2000-2003, by Object Refinery Limited and Contributors. 
 * 
 * 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., 59 Temple Place, Suite 330, 
 * Boston, MA 02111-1307, USA. 
 * 
 * ----------------------- 
 * SegmentedTimeline.java 
 * ----------------------- 
 * (C) Copyright 2003, by Bill Kelemen and Contributors. 
 * 
 * Original Author:  Bill Kelemen; 
 * Contributor(s):   David Gilbert (for Object Refinery Limited); 
 * 
 * $Id: SegmentedTimeline.java,v 1.13 2003/09/11 08:45:10 mungady Exp $ 
 * 
 * Changes 
 * ------- 
 * 23-May-2003 : Version 1 (BK); 
 * 15-Aug-2003 : Implemented Cloneable (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.SimpleTimeZone; 
import java.util.TimeZone; 
 
/** 
 * A {@link Timeline} that implements a "segmented" timeline with included, excluded and 
 * exception segments. 
 * 

* 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. *

* 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. *

* 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}. *

* 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. *

* 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. *

* 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. *

* 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). *

*

 
 * 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 
 * 
* Legend:
* <space> = Included segment
* EE = Excluded segments in the base timeline
*

* In the example, the following segment attributes are presented: *

*

* 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. *

* Additionally the startTime, 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 startTime * 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. *

* 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 * 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. *

* Two factory methods newMondayThroughFridayTimeline() and * newFifteenMinuteTimeline() are provided as examples to create special * SegmentedTimelines. * * @see org.jfree.chart.axis.DateAxis * * @author Bill Kelemen */ public class SegmentedTimeline implements Timeline, Cloneable, Serializable { //////////////////////////////////////////////////////////////////////////// // 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. */ 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. */ 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. */ 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 = new GregorianCalendar(NO_DST_TIME_ZONE); /** * 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. *

* Additionally, all excluded segments from this baseTimeline will be considered * exceptions at this level. */ private SegmentedTimeline baseTimeline; 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(); } //////////////////////////////////////////////////////////////////////////// // constructors and factory methods //////////////////////////////////////////////////////////////////////////// /** * Constructs a new segmented timeline, optionaly using another segmented * timeline as its base. This chaning 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; } /** * Factory method to create a Monday through Friday SegmentedTimeline. *

* The startTime of the resulting timeline will be midnight of the * firt 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(FIRST_MONDAY_AFTER_1900); return timeline; } /** * Factory method to create a 15-min, 9:00 AM thought 4:00 PM, Monday through * Friday SegmentedTimeline. *

* 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). *

* In order to exclude Saturdays and Sundays it uses a baseTimeline that only * includes Monday through Friday days. *

* The startTime of the resulting timeline will be 9:00 AM after * the startTime of the baseTimeline. This will correspond to 9:00 AM of the * firt 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(FIRST_MONDAY_AFTER_1900 + 36 * timeline.getSegmentSize()); timeline.setBaseTimeline(newMondayThroughFridayTimeline()); return timeline; } //////////////////////////////////////////////////////////////////////////// // 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 null 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() < segmentSize) { throw new IllegalArgumentException("baseTimeline.getSegmentSize() is smaller " + "than segmentSize"); } else if (baseTimeline.getStartTime() > startTime) { throw new IllegalArgumentException("baseTimeline.getStartTime() is after than " + "startTime"); } else if ((baseTimeline.getSegmentSize() % segmentSize) != 0) { throw new IllegalArgumentException( "baseTimeline.getSegmentSize() is not multiple of segmentSize"); } else if (((startTime - baseTimeline.getStartTime()) % 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 - startTime; long groupMilliseconds = rawMilliseconds % segmentsGroupSize; long groupIndex = rawMilliseconds / segmentsGroupSize; if (groupMilliseconds >= segmentsIncludedSize) { result = toTimelineValue(startTime + segmentsGroupSize * (groupIndex + 1)); } else { Segment segment = getSegment(millisecond); if (segment.inExceptionSegments()) { result = toTimelineValue(segment.getSegmentEnd() + 1); } else { long shiftedSegmentedValue = millisecond - startTime; long x = shiftedSegmentedValue % segmentsGroupSize; long y = shiftedSegmentedValue / segmentsGroupSize; long wholeExceptionsBeforeDomainValue = getExceptionSegmentCount(startTime, millisecond - 1); // long partialTimeInException = 0; // Segment ss = getSegment(millisecond); // if (ss.inExceptionSegments()) { // partialTimeInException = millisecond - ss.getSegmentStart(); // } if (x < segmentsIncludedSize) { result = segmentsIncludedSize * y + x - wholeExceptionsBeforeDomainValue * segmentSize;// - partialTimeInException;; } else { result = segmentsIncludedSize * (y + 1) - wholeExceptionsBeforeDomainValue * 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. */ 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. * * @return The domain value. */ public long toMillisecond(long timelineValue) { // calculate the result as if no exceptions Segment result = new Segment(startTime + timelineValue + (timelineValue / segmentsIncludedSize) * segmentsExcludedSize); long lastIndex = 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 - 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 += segmentSize; } lastIndex++; } return result.millisecond; } /** * Returns true if a value is contained in the timeline. * * @param domainValue the value to verify. * * @return true if value is contained in the timeline. */ public boolean containsDomainValue(long millisecond) { Segment segment = getSegment(millisecond); return segment.inIncludeSegments(); } /** * Returns true if a value is contained in the timeline. * @param date date to verify * @return true if value is contained in the timeline */ public boolean containsDomainValue(Date date) { return containsDomainValue(getTime(date)); } /** * Returns true 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 true 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 true 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 true 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). *

* The segment is identified by a domainValue into any part of the segment. * Therefore the segmentStart <= domainValue <= segmentEnd. * * @param domainValue 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). *

* 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). *

* 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). *

* 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); 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. *

* 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). *

* 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 = 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 discarted). *

* The segment is identified by a domainValue into any part of the segment. * Therefore the segmentStart <= domainValue <= segmentEnd. * * @param date date domain value to teat as a baseTimeline exception */ public void addBaseTimelineException(Date date) { //addBaseTimelineException(getTime(date)); addBaseTimelineException(date.getTime()); } /** * 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 = 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() + baseTimeline.getSegmentsExcluded() * 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(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 = 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 index + segmentSize*m will be returned for the * smallest m 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 date + segmentSize*m will be returned * for the smallest m 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 object taking into account nulls. * @param o first object to compare * @param p second object to compare * @return true if both objects are equal or both null, false 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 true or false */ 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(exceptionSegments, other.getExceptionSegments()); return b0 && b1 && b2 && b3 && b4; } else { return (false); } } /** * 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, (-(insertion point) - 1). The * insertion point 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 list.size(), 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 = exceptionSegments.size() - 1; while (low <= high) { int mid = (low + high) / 2; Segment midSegment = (Segment) 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) { workingCalendar.setTime(date); workingCalendarNoDST.set(workingCalendar.get(Calendar.YEAR), workingCalendar.get(Calendar.MONTH), workingCalendar.get(Calendar.DATE), workingCalendar.get(Calendar.HOUR_OF_DAY), workingCalendar.get(Calendar.MINUTE), workingCalendar.get(Calendar.SECOND)); workingCalendarNoDST.set(Calendar.MILLISECOND, workingCalendar.get(Calendar.MILLISECOND)); Date revisedDate = 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) { workingCalendarNoDST.setTime(new Date(value)); return (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. *

* 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() { } /** * 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 = startTime + segmentNumber * segmentSize; this.segmentEnd = segmentStart + segmentSize - 1; this.millisecond = millisecond; } /** * Calculates the segment number for a given millisecond. * * @param millisecond the millisecond (as encoded by java.util.Date). * * @return The segment number. */ public long calculateSegmentNumber(long millisecond) { if (millisecond >= startTime) { return (millisecond - startTime) / segmentSize; } else { return ((millisecond - startTime) / 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; } /** * Gets the index in this segment. Index will always be between the segmentStart * and segmentEnd. * * @return The index. * * @deprecated Use getMillisecond(). */ public long getIndex() { return getMillisecond(); } /** * 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 millisecond the millisecond to verify. * * @return true if the millisecond is contained in the segment. */ public boolean contains(long millisecond) { return (this.segmentStart <= millisecond && millisecond <= this.segmentEnd); } /** * Returns true if an interval is contained in this segment. * * @param from the start of the interval. * @param to the end of the interval. * * @return true if the interval is contained in the segment. */ public boolean contains(long from, long to) { return (this.segmentStart <= from && to <= this.segmentEnd); } /** * Returns true if a segment is contained in this segment. * * @param segment the segment to test for inclusion * * @return true if the segment is contained in this segment. */ public boolean contains(Segment segment) { return contains(segment.getSegmentStart(), segment.getSegmentEnd()); } /** * Returns true if this segment is contained in an interval. * * @param from the start of the interval. * @param to the end of the interval. * * @return true 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 <= segmentStart && segmentEnd <= to) { return this; } else { return null; } } /** * Returns true 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 true 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 Segment) for equality with this segment. * * @param object The other segment to compare with us * * @return true 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 null 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 true or false. */ public boolean inIncludeSegments() { if (getSegmentNumberRelativeToGroup() < segmentsIncluded) { return !inExceptionSegments(); } else { return false; } } /** * Returns true if we are an excluded segment. * * @return true or false. */ public boolean inExcludeSegments() { return getSegmentNumberRelativeToGroup() >= 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 % groupSegmentCount); if (p < 0) { p += 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 true 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) { segmentNumber += n; long m = n * segmentSize; segmentStart += m; 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) { segmentNumber -= n; long m = n * segmentSize; segmentStart -= m; 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 = segmentStart; } /** * Moves the index of this segment to the end of the segment. */ public void moveIndexToEnd() { this.millisecond = 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 true or false. */ public boolean inIncludeSegments() { for (Segment segment = getSegment(segmentStart); segment.getSegmentStart() < segmentEnd; segment.inc()) { if (!segment.inIncludeSegments()) { return (false); } } return true; } /** * Returns true if we are an excluded segment. * * @return true or false. */ public boolean inExcludeSegments() { for (Segment segment = getSegment(segmentStart); segment.getSegmentStart() < 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 SegmentRange 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); } } }