效果图:
pom
<dependency>
<groupId>org.jfree</groupId>
<artifactId>jfreechart</artifactId>
<version>1.5.5</version>
</dependency>
KLinkChart
package com.xyy.analyse.util.jfree.klink;
import com.xyy.analyse.util.date.SimpleDateFormatPool;
import com.xyy.analyse.util.jfree.entity.KLinkData;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateUtils;
import org.jfree.chart.ChartFrame;
import org.jfree.chart.ChartUtils;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.DateAxis;
import org.jfree.chart.axis.DateTickMarkPosition;
import org.jfree.chart.axis.DateTickUnit;
import org.jfree.chart.axis.DateTickUnitType;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.axis.NumberTickUnit;
import org.jfree.chart.axis.ValueAxis;
import org.jfree.chart.plot.CombinedDomainXYPlot;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.renderer.xy.CandlestickRenderer;
import org.jfree.chart.renderer.xy.XYBarRenderer;
import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
import org.jfree.data.time.Day;
import org.jfree.data.time.TimeSeries;
import org.jfree.data.time.TimeSeriesCollection;
import org.jfree.data.time.TimeSeriesDataItem;
import org.jfree.data.time.Year;
import org.jfree.data.time.ohlc.OHLCSeries;
import org.jfree.data.time.ohlc.OHLCSeriesCollection;
import java.awt.Color;
import java.awt.Paint;
import java.io.File;
import java.io.IOException;
import java.io.Serial;
import java.time.LocalDate;
import java.util.Comparator;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
/**
* k线图
*/
public class KLinkChart {
/**
* 高
*/
private int height = 960;
/**
* 款
*/
private int width = 1080;
/**
* 数据轴时间类型
*/
protected TimeUnitEnum timeUnit = TimeUnitEnum.DAY;
/**
* 数据
*/
protected KLinkData kLinkData;
// K线图
private final OHLCSeriesCollection ohlcSeriesCollection = new OHLCSeriesCollection();
// 成交量图
private final TimeSeriesCollection timeSeriesCollection = new TimeSeriesCollection();
// k线均价图
private final TimeSeriesCollection avgTimeSeriesCollection = new TimeSeriesCollection();
//获取成交量均线图数据
private final TimeSeriesCollection volMaSeriesConllection = new TimeSeriesCollection();
private KLinkChart(KLinkData kLinkData) {
this.kLinkData = kLinkData;
}
public static KLinkChart getInstance(KLinkData kLinkData) {
return new KLinkChart(kLinkData);
}
public void show() {
ChartFrame frame = new ChartFrame("analyse", processingData());
frame.pack();
frame.setVisible(true);
}
public void toPNG() throws IOException {
JFreeChart chart = this.processingData();
String fileName = StringUtils.isBlank(kLinkData.getTitle()) ? UUID.randomUUID().toString() : kLinkData.getTitle();
String projectPath = System.getProperty("user.dir");
File file = new File(projectPath + "/" + fileName + ".png");
ChartUtils.saveChartAsPNG(file, chart, kLinkData.getWidth(), kLinkData.getHeight());// 把报表保存为文件
}
private JFreeChart processingData() {
// 高开低收数据序列
final OHLCSeries ohlcSeries = new OHLCSeries("");
//对应时间成交量数据
final TimeSeries timeSeries = new TimeSeries(kLinkData.getTitle());
// 均线图(收盘价为准)
final TimeSeries closeTimeSeries = new TimeSeries("all_price");
final List<KLinkData.Dataset> datasets = kLinkData.getDatasets();
for (KLinkData.Dataset dataset : datasets) {
final Day timePeriod = new Day(DateUtil.localDate2Date(dataset.date()));
//股票K线图的四个数据,依次是开,高,低,收
ohlcSeries.add(timePeriod,
dataset.open().doubleValue(), dataset.high().doubleValue(),
dataset.low().doubleValue(), dataset.close().doubleValue());
// 成交量
timeSeries.add(timePeriod, dataset.volume().doubleValue());
// 收盘价
closeTimeSeries.addOrUpdate(timePeriod, dataset.close().doubleValue());
}
ohlcSeriesCollection.addSeries(ohlcSeries);
timeSeriesCollection.addSeries(timeSeries);
// 计算K线均线
final List<TimeSeriesDataItem> closeTimeSeriesDataList = closeTimeSeries.getItems();
kLinkData.getEma().parallelStream()
.map(ma -> this.computeMa(closeTimeSeriesDataList, ma))
.forEach(avgTimeSeriesCollection::addSeries);
// 计算成交量均线
final List<TimeSeriesDataItem> volMaTimeSeriesDataList = timeSeries.getItems();
kLinkData.getEma().parallelStream()
.map(ma -> this.computeMa(volMaTimeSeriesDataList, ma))
.forEach(volMaSeriesConllection::addSeries);
// k线数据, 交易量数据, k线均线数据, 交易量均线数据
return getJFreeChart(
// 获取 k线 最高值和最低值
datasets.stream().max(Comparator.comparing(KLinkData.Dataset::high)).get().high().doubleValue(),
datasets.stream().min(Comparator.comparing(KLinkData.Dataset::low)).get().low().doubleValue(),
// 获取成交量最高值和最低值
datasets.stream().max(Comparator.comparing(KLinkData.Dataset::volume)).get().volume().doubleValue(),
datasets.stream().min(Comparator.comparing(KLinkData.Dataset::volume)).get().volume().doubleValue(),
// 成交时间
datasets.stream().max(Comparator.comparing(KLinkData.Dataset::date)).get().date(),
datasets.stream().min(Comparator.comparing(KLinkData.Dataset::date)).get().date()
);
}
/**
* 计算均线
*
* @param items x,y 轴上的值
* @param ma 均线
*/
private TimeSeries computeMa(List<TimeSeriesDataItem> items, Integer ma) {
final TimeSeries maSeries = new TimeSeries(ma + "-MA");
LinkedList<TimeSeriesDataItem> maItems = new LinkedList<>();
for (TimeSeriesDataItem seriesDataItem : items) {
if (maItems.size() == ma) {
// 当元素相同时, 删除最前方元素, 并将新的元素插入到最后
maItems.addLast(seriesDataItem);
maItems.removeFirst();
// 收集里面的数据
final double sum = maItems.stream()
.mapToDouble(e -> e.getValue().doubleValue())
.sum() / ma;
maSeries.addOrUpdate(seriesDataItem.getPeriod(), sum);
continue;
}
maItems.addLast(seriesDataItem);
}
return maSeries;
}
/**
* 获取JFreeChart对象
*
* @param maxValue 最高价
* @param minValue 最低价
* @param maxVolume 最高成交
* @param minVolume 最低成交
* @param maxDate 结束时间
* @param minDate 开始时间
*/
private JFreeChart getJFreeChart(double maxValue, double minValue, double maxVolume, double minVolume,
LocalDate maxDate, LocalDate minDate
) {
// K线及均线
final ValueAxis xAxis = getXAxis(maxDate, minDate);
XYPlot ohlcSeriesXYPlot = new XYPlot(null, xAxis, getYAxis(minValue, maxValue), null);
// k线数据及画图器
final KLinkCandlestickRenderer kLinkRenderer = getKLinkRenderer();
final XYLineAndShapeRenderer avgLinkRenderer = getAvgLinkRenderer();
ohlcSeriesXYPlot.setDataset(0, ohlcSeriesCollection);
ohlcSeriesXYPlot.setRenderer(0, kLinkRenderer);
ohlcSeriesXYPlot.setDataset(1, avgTimeSeriesCollection);
ohlcSeriesXYPlot.setRenderer(1, avgLinkRenderer);
// 设置成交量
XYBarRenderer xyBarRender = new XYBarRenderer() {
//为了避免出现警告消息,特设定此值
@Serial
private static final long serialVersionUID = 1L;
//匿名内部类用来处理当日的成交量柱形图的颜色与K线图的颜色保持一致
@Override
public Paint getItemPaint(int i, int j) {
//收盘价高于开盘价,股票上涨,选用股票上涨的颜色
if (i == 0) {
if (ohlcSeriesCollection.getCloseValue(i, j) > ohlcSeriesCollection.getOpenValue(i, j)) {
return kLinkRenderer.getUpPaint();
} else {
return kLinkRenderer.getDownPaint();
}
} else {
return avgLinkRenderer.getItemPaint(i, j);
}
}
};
//设置柱形图之间的间隔
xyBarRender.setMargin(0.1);
//建立第二个画图区域对象,主要此时的x轴设为了null值,因为要与第一个画图区域对象共享x轴
XYPlot timeSeriesXYPlot = new XYPlot(null, null, getVolYAxis(maxVolume, minVolume), null);
timeSeriesXYPlot.setDataset(0, timeSeriesCollection);
timeSeriesXYPlot.setRenderer(0, xyBarRender);
timeSeriesXYPlot.setDataset(1, volMaSeriesConllection);
timeSeriesXYPlot.setRenderer(1, avgLinkRenderer);//第二个画图区域使用线和点来表示成交量
CombinedDomainXYPlot combineddomainxyplot = new CombinedDomainXYPlot(xAxis);//建立一个恰当的联合图形区域对象,以x轴为共享轴
combineddomainxyplot.add(ohlcSeriesXYPlot, 2);//添加图形区域对象,后面的数字是计算这个区域对象应该占据多大的区域2/3
combineddomainxyplot.add(timeSeriesXYPlot, 1);//添加图形区域对象,后面的数字是计算这个区域对象应该占据多大的区域1/3
combineddomainxyplot.setGap(10);//设置两个图形区域对象之间的间隔空间
return new JFreeChart(kLinkData.getTitle(), JFreeChart.DEFAULT_TITLE_FONT, combineddomainxyplot, false);
}
private NumberAxis getVolYAxis(double maxVolume, double minVolume) {
NumberAxis y2Axis = new NumberAxis();//设置Y轴,为数值,后面的设置,参考上面的y轴设置
y2Axis.setAutoRange(false);
y2Axis.setRange(minVolume * 0.9, maxVolume * 1.1);
y2Axis.setTickUnit(new NumberTickUnit((maxVolume * 1.1 - minVolume * 0.9) / 4));
return y2Axis;
}
/**
* k线画图器
*/
private KLinkCandlestickRenderer getKLinkRenderer() {
// 设置k线画图器
final KLinkCandlestickRenderer renderer = new KLinkCandlestickRenderer();
// 设置使用自定义边框线
renderer.setUseOutlinePaint(true);
//设置如何对K线图的宽度进行设定
renderer.setAutoWidthMethod(CandlestickRenderer.WIDTHMETHOD_AVERAGE);
//设置各个K线图之间的间隔
renderer.setAutoWidthGap(0.001);
//设置股票上涨的K线图颜色
renderer.setUpPaint(Color.RED);
//设置股票下跌的K线图颜色
renderer.setDownPaint(Color.GREEN);
return renderer;
}
/**
* 均线画图器
*
* @return
*/
private XYLineAndShapeRenderer getAvgLinkRenderer() {
XYLineAndShapeRenderer renderer = new XYLineAndShapeRenderer();
renderer.setDefaultItemLabelsVisible(true);
// 不显示数据点模型
renderer.setDefaultShapesVisible(false);
return renderer;
}
private ValueAxis getXAxis(LocalDate maxDate, LocalDate minDate) {
//设置x轴,也就是时间轴
DateAxis xAxis = new DateAxis();
//设置不采用自动设置时间范围
xAxis.setAutoRange(false);
//设置时间范围,注意时间的最大值要比已有的时间最大值要多一天
xAxis.setRange(DateUtil.localDate2Date(minDate), DateUtils.addDays(DateUtil.localDate2Date(maxDate), 1));
//设置时间线显示的规则,用这个方法就摒除掉了周六和周日这些没有交易的日期(很多人都不知道有此方法),使图形看上去连续
final SegmentedTimeline segmentedTimeline = getSegmentedTimeline(maxDate, minDate);
xAxis.setTimeline(segmentedTimeline);
//设置不采用自动选择刻度值
xAxis.setAutoTickUnitSelection(false);
//设置标记的位置
xAxis.setTickMarkPosition(DateTickMarkPosition.MIDDLE);
//设置标准的时间刻度单位
xAxis.setStandardTickUnits(DateAxis.createStandardDateTickUnits());
//设置时间刻度的间隔,一般以周为单位
xAxis.setTickUnit(new DateTickUnit(DateTickUnitType.DAY, 7));
//设置显示时间的格式
xAxis.setDateFormatOverride(SimpleDateFormatPool.get("yyyy-MM-dd"));
return xAxis;
}
private ValueAxis getYAxis(double minValue, double maxValue) {
//设定y轴,就是数字轴
NumberAxis yAxis = new NumberAxis();
//不不使用自动设定范围
yAxis.setAutoRange(false);
//设定y轴值的范围,比最低值要低一些,比最大值要大一些,这样图形看起来会美观些
yAxis.setRange(minValue * 0.9, maxValue * 1.1);
//设置刻度显示的密度
yAxis.setTickUnit(new NumberTickUnit((maxValue * 1.1 - minValue * 0.9) / 10));
return yAxis;
}
private SegmentedTimeline getSegmentedTimeline(LocalDate maxDate, LocalDate minDate) {
final SegmentedTimeline segmentedTimeline = SegmentedTimeline.newMondayThroughFridayTimeline();
final Set<LocalDate> dateSet = kLinkData.getDatasets().stream().map(KLinkData.Dataset::date).collect(Collectors.toSet());
// 生成最小时间和最大时间之间的所有日期
for (LocalDate date = minDate; !date.isAfter(maxDate); date = date.plusDays(1)) {
if (!dateSet.contains(date)) {
segmentedTimeline.addException(DateUtil.localDate2Date(date));
}
}
return segmentedTimeline;
}
}
KLinkData
package com.xyy.analyse.util.jfree.entity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@EqualsAndHashCode(callSuper = true)
@Data
public class KLinkData {
/**
* 高
*/
private int height = 960;
/**
* 款
*/
private int width = 1080;
/**
* 标题
*/
private String title = "";
private List<Dataset> datasets;
/**
* 均线
*/
private List<Integer> ema;
public KLinkData(List<Dataset> datasets) {
this.datasets = datasets;
this.ema = new ArrayList<>();
}
public KLinkData(List<Dataset> datasets, Integer... ema) {
this(datasets);
if (ema != null && ema.length > 0) {
this.ema.addAll(Arrays.asList(ema));
}
}
/**
* @param date 时间
* @param open 开盘
* @param close 收盘
* @param low 最低
* @param high 最高
* @param volume 成交量
*/
public record Dataset(LocalDate date,
BigDecimal open,
BigDecimal close,
BigDecimal low,
BigDecimal high,
BigDecimal volume) {
}
}
KLinkCandlestickRenderer
package com.xyy.analyse.util.jfree.klink;
import org.jfree.chart.axis.ValueAxis;
import org.jfree.chart.entity.EntityCollection;
import org.jfree.chart.event.RendererChangeEvent;
import org.jfree.chart.labels.HighLowItemLabelGenerator;
import org.jfree.chart.labels.XYToolTipGenerator;
import org.jfree.chart.plot.CrosshairState;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.plot.PlotRenderingInfo;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.renderer.xy.AbstractXYItemRenderer;
import org.jfree.chart.renderer.xy.XYItemRenderer;
import org.jfree.chart.renderer.xy.XYItemRendererState;
import org.jfree.chart.ui.RectangleEdge;
import org.jfree.chart.util.Args;
import org.jfree.chart.util.PaintUtils;
import org.jfree.chart.util.PublicCloneable;
import org.jfree.chart.util.SerialUtils;
import org.jfree.data.Range;
import org.jfree.data.xy.IntervalXYDataset;
import org.jfree.data.xy.OHLCDataset;
import org.jfree.data.xy.XYDataset;
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Composite;
import java.awt.Graphics2D;
import java.awt.Paint;
import java.awt.Stroke;
import java.awt.geom.Line2D;
import java.awt.geom.Rectangle2D;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serial;
import java.io.Serializable;
/**
* A renderer that draws candlesticks on an {@link XYPlot} (requires a
* {@link OHLCDataset}). The example shown here is generated
* by the {@code CandlestickChartDemo1.java} program included in the
* JFreeChart demo collection:
* <br><br>
* <img src="doc-files/CandlestickRendererSample.png"
* alt="CandlestickRendererSample.png">
* <p>
* This renderer does not include code to calculate the crosshair point for the
* plot.
*/
public class KLinkCandlestickRenderer extends AbstractXYItemRenderer
implements XYItemRenderer, Cloneable, PublicCloneable, Serializable {
/**
* For serialization.
*/
@Serial
private static final long serialVersionUID = 50390395841817122L;
/**
* The average width method.
*/
public static final int WIDTHMETHOD_AVERAGE = 0;
/**
* The smallest width method.
*/
public static final int WIDTHMETHOD_SMALLEST = 1;
/**
* The interval data method.
*/
public static final int WIDTHMETHOD_INTERVALDATA = 2;
/**
* The method of automatically calculating the candle width.
*/
private int autoWidthMethod = WIDTHMETHOD_AVERAGE;
/**
* The number (generally between 0.0 and 1.0) by which the available space
* automatically calculated for the candles will be multiplied to determine
* the actual width to use.
*/
private double autoWidthFactor = 4.5 / 7;
/**
* The minimum gap between one candle and the next
*/
private double autoWidthGap = 0.0;
/**
* The candle width.
*/
private double candleWidth;
/**
* The maximum candlewidth in milliseconds.
*/
private double maxCandleWidthInMilliseconds = 1000.0 * 60.0 * 60.0 * 20.0;
/**
* Temporary storage for the maximum candle width.
*/
private double maxCandleWidth;
/**
* The paint used to fill the candle when the price moved up from open to
* close.
*/
private transient Paint upPaint;
/**
* The paint used to fill the candle when the price moved down from open
* to close.
*/
private transient Paint downPaint;
/**
* A flag controlling whether or not volume bars are drawn on the chart.
*/
private boolean drawVolume;
/**
* The paint used to fill the volume bars (if they are visible). Once
* initialised, this field should never be set to {@code null}.
*/
private transient Paint volumePaint;
/**
* Temporary storage for the maximum volume.
*/
private transient double maxVolume;
/**
* A flag that controls whether or not the renderer's outline paint is
* used to draw the outline of the candlestick. The default value is
* {@code false} to avoid a change of behaviour for existing code.
*/
private boolean useOutlinePaint;
/**
* Creates a new renderer for candlestick charts.
*/
public KLinkCandlestickRenderer() {
this(-1.0);
}
/**
* Creates a new renderer for candlestick charts.
* <p>
* Use -1 for the candle width if you prefer the width to be calculated
* automatically.
*
* @param candleWidth The candle width.
*/
public KLinkCandlestickRenderer(double candleWidth) {
this(candleWidth, true, new HighLowItemLabelGenerator());
}
/**
* Creates a new renderer for candlestick charts.
* <p>
* Use -1 for the candle width if you prefer the width to be calculated
* automatically.
*
* @param candleWidth the candle width.
* @param drawVolume a flag indicating whether or not volume bars should
* be drawn.
* @param toolTipGenerator the tool tip generator. {@code null} is
* none.
*/
public KLinkCandlestickRenderer(double candleWidth, boolean drawVolume,
XYToolTipGenerator toolTipGenerator) {
super();
setDefaultToolTipGenerator(toolTipGenerator);
this.candleWidth = candleWidth;
this.drawVolume = drawVolume;
this.volumePaint = Color.GRAY;
this.upPaint = Color.GREEN;
this.downPaint = Color.RED;
this.useOutlinePaint = false; // false preserves the old behaviour
// prior to introducing this flag
}
/**
* Returns the width of each candle.
*
* @return The candle width.
* @see #setCandleWidth(double)
*/
public double getCandleWidth() {
return this.candleWidth;
}
/**
* Sets the candle width and sends a {@link RendererChangeEvent} to all
* registered listeners.
* <p>
* If you set the width to a negative value, the renderer will calculate
* the candle width automatically based on the space available on the chart.
*
* @param width The width.
* @see #setAutoWidthMethod(int)
* @see #setAutoWidthGap(double)
* @see #setAutoWidthFactor(double)
* @see #setMaxCandleWidthInMilliseconds(double)
*/
public void setCandleWidth(double width) {
if (width != this.candleWidth) {
this.candleWidth = width;
fireChangeEvent();
}
}
/**
* Returns the maximum width (in milliseconds) of each candle.
*
* @return The maximum candle width in milliseconds.
* @see #setMaxCandleWidthInMilliseconds(double)
*/
public double getMaxCandleWidthInMilliseconds() {
return this.maxCandleWidthInMilliseconds;
}
/**
* Sets the maximum candle width (in milliseconds) and sends a
* {@link RendererChangeEvent} to all registered listeners.
*
* @param millis The maximum width.
* @see #getMaxCandleWidthInMilliseconds()
* @see #setCandleWidth(double)
* @see #setAutoWidthMethod(int)
* @see #setAutoWidthGap(double)
* @see #setAutoWidthFactor(double)
*/
public void setMaxCandleWidthInMilliseconds(double millis) {
this.maxCandleWidthInMilliseconds = millis;
fireChangeEvent();
}
/**
* Returns the method of automatically calculating the candle width.
*
* @return The method of automatically calculating the candle width.
* @see #setAutoWidthMethod(int)
*/
public int getAutoWidthMethod() {
return this.autoWidthMethod;
}
/**
* Sets the method of automatically calculating the candle width and
* sends a {@link RendererChangeEvent} to all registered listeners.
* <p>
* {@code WIDTHMETHOD_AVERAGE}: Divides the entire display (ignoring
* scale factor) by the number of items, and uses this as the available
* width.<br>
* {@code WIDTHMETHOD_SMALLEST}: Checks the interval between each
* item, and uses the smallest as the available width.<br>
* {@code WIDTHMETHOD_INTERVALDATA}: Assumes that the dataset supports
* the IntervalXYDataset interface, and uses the startXValue - endXValue as
* the available width.
* <br>
*
* @param autoWidthMethod The method of automatically calculating the
* candle width.
* @see #WIDTHMETHOD_AVERAGE
* @see #WIDTHMETHOD_SMALLEST
* @see #WIDTHMETHOD_INTERVALDATA
* @see #getAutoWidthMethod()
* @see #setCandleWidth(double)
* @see #setAutoWidthGap(double)
* @see #setAutoWidthFactor(double)
* @see #setMaxCandleWidthInMilliseconds(double)
*/
public void setAutoWidthMethod(int autoWidthMethod) {
if (this.autoWidthMethod != autoWidthMethod) {
this.autoWidthMethod = autoWidthMethod;
fireChangeEvent();
}
}
/**
* Returns the factor by which the available space automatically
* calculated for the candles will be multiplied to determine the actual
* width to use.
*
* @return The width factor (generally between 0.0 and 1.0).
* @see #setAutoWidthFactor(double)
*/
public double getAutoWidthFactor() {
return this.autoWidthFactor;
}
/**
* Sets the factor by which the available space automatically calculated
* for the candles will be multiplied to determine the actual width to use.
*
* @param autoWidthFactor The width factor (generally between 0.0 and 1.0).
* @see #getAutoWidthFactor()
* @see #setCandleWidth(double)
* @see #setAutoWidthMethod(int)
* @see #setAutoWidthGap(double)
* @see #setMaxCandleWidthInMilliseconds(double)
*/
public void setAutoWidthFactor(double autoWidthFactor) {
if (this.autoWidthFactor != autoWidthFactor) {
this.autoWidthFactor = autoWidthFactor;
fireChangeEvent();
}
}
/**
* Returns the amount of space to leave on the left and right of each
* candle when automatically calculating widths.
*
* @return The gap.
* @see #setAutoWidthGap(double)
*/
public double getAutoWidthGap() {
return this.autoWidthGap;
}
/**
* Sets the amount of space to leave on the left and right of each candle
* when automatically calculating widths and sends a
* {@link RendererChangeEvent} to all registered listeners.
*
* @param autoWidthGap The gap.
* @see #getAutoWidthGap()
* @see #setCandleWidth(double)
* @see #setAutoWidthMethod(int)
* @see #setAutoWidthFactor(double)
* @see #setMaxCandleWidthInMilliseconds(double)
*/
public void setAutoWidthGap(double autoWidthGap) {
if (this.autoWidthGap != autoWidthGap) {
this.autoWidthGap = autoWidthGap;
fireChangeEvent();
}
}
/**
* Returns the paint used to fill candles when the price moves up from open
* to close.
*
* @return The paint (possibly {@code null}).
* @see #setUpPaint(Paint)
*/
public Paint getUpPaint() {
return this.upPaint;
}
/**
* Sets the paint used to fill candles when the price moves up from open
* to close and sends a {@link RendererChangeEvent} to all registered
* listeners.
*
* @param paint the paint ({@code null} permitted).
* @see #getUpPaint()
*/
public void setUpPaint(Paint paint) {
this.upPaint = paint;
fireChangeEvent();
}
/**
* Returns the paint used to fill candles when the price moves down from
* open to close.
*
* @return The paint (possibly {@code null}).
* @see #setDownPaint(Paint)
*/
public Paint getDownPaint() {
return this.downPaint;
}
/**
* Sets the paint used to fill candles when the price moves down from open
* to close and sends a {@link RendererChangeEvent} to all registered
* listeners.
*
* @param paint The paint ({@code null} permitted).
*/
public void setDownPaint(Paint paint) {
this.downPaint = paint;
fireChangeEvent();
}
/**
* Returns a flag indicating whether or not volume bars are drawn on the
* chart.
*
* @return A boolean.
* @see #setDrawVolume(boolean)
*/
public boolean getDrawVolume() {
return this.drawVolume;
}
/**
* Sets a flag that controls whether or not volume bars are drawn in the
* background and sends a {@link RendererChangeEvent} to all registered
* listeners.
*
* @param flag the flag.
* @see #getDrawVolume()
*/
public void setDrawVolume(boolean flag) {
if (this.drawVolume != flag) {
this.drawVolume = flag;
fireChangeEvent();
}
}
/**
* Returns the paint that is used to fill the volume bars if they are
* visible.
*
* @return The paint (never {@code null}).
* @see #setVolumePaint(Paint)
*/
public Paint getVolumePaint() {
return this.volumePaint;
}
/**
* Sets the paint used to fill the volume bars, and sends a
* {@link RendererChangeEvent} to all registered listeners.
*
* @param paint the paint ({@code null} not permitted).
* @see #getVolumePaint()
* @see #getDrawVolume()
*/
public void setVolumePaint(Paint paint) {
Args.nullNotPermitted(paint, "paint");
this.volumePaint = paint;
fireChangeEvent();
}
/**
* Returns the flag that controls whether or not the renderer's outline
* paint is used to draw the candlestick outline. The default value is
* {@code false}.
*
* @return A boolean.
* @see #setUseOutlinePaint(boolean)
*/
public boolean getUseOutlinePaint() {
return this.useOutlinePaint;
}
/**
* Sets the flag that controls whether or not the renderer's outline
* paint is used to draw the candlestick outline, and sends a
* {@link RendererChangeEvent} to all registered listeners.
*
* @param use the new flag value.
* @see #getUseOutlinePaint()
*/
public void setUseOutlinePaint(boolean use) {
if (this.useOutlinePaint != use) {
this.useOutlinePaint = use;
fireChangeEvent();
}
}
/**
* Returns the range of values the renderer requires to display all the
* items from the specified dataset.
*
* @param dataset the dataset ({@code null} permitted).
* @return The range ({@code null} if the dataset is {@code null}
* or empty).
*/
@Override
public Range findRangeBounds(XYDataset dataset) {
return findRangeBounds(dataset, true);
}
/**
* Initialises the renderer then returns the number of 'passes' through the
* data that the renderer will require (usually just one). This method
* will be called before the first item is rendered, giving the renderer
* an opportunity to initialise any state information it wants to maintain.
* The renderer can do nothing if it chooses.
*
* @param g2 the graphics device.
* @param dataArea the area inside the axes.
* @param plot the plot.
* @param dataset the data.
* @param info an optional info collection object to return data back to
* the caller.
* @return The number of passes the renderer requires.
*/
@Override
public XYItemRendererState initialise(Graphics2D g2, Rectangle2D dataArea,
XYPlot plot, XYDataset dataset, PlotRenderingInfo info) {
// calculate the maximum allowed candle width from the axis...
ValueAxis axis = plot.getDomainAxis();
double x1 = axis.getLowerBound();
double x2 = x1 + this.maxCandleWidthInMilliseconds;
RectangleEdge edge = plot.getDomainAxisEdge();
double xx1 = axis.valueToJava2D(x1, dataArea, edge);
double xx2 = axis.valueToJava2D(x2, dataArea, edge);
this.maxCandleWidth = Math.abs(xx2 - xx1);
// Absolute value, since the relative x
// positions are reversed for horizontal orientation
// calculate the highest volume in the dataset...
if (this.drawVolume) {
OHLCDataset highLowDataset = (OHLCDataset) dataset;
this.maxVolume = 0.0;
for (int series = 0; series < highLowDataset.getSeriesCount();
series++) {
for (int item = 0; item < highLowDataset.getItemCount(series);
item++) {
double volume = highLowDataset.getVolumeValue(series, item);
if (volume > this.maxVolume) {
this.maxVolume = volume;
}
}
}
}
return new XYItemRendererState(info);
}
/**
* Draws the visual representation of a single data item.
*
* @param g2 the graphics device.
* @param state the renderer state.
* @param dataArea the area within which the plot is being drawn.
* @param info collects info about the drawing.
* @param plot the plot (can be used to obtain standard color
* information etc).
* @param domainAxis the domain axis.
* @param rangeAxis the range axis.
* @param dataset the dataset.
* @param series the series index (zero-based).
* @param item the item index (zero-based).
* @param crosshairState crosshair information for the plot
* ({@code null} permitted).
* @param pass the pass index.
*/
@Override
public void drawItem(Graphics2D g2, XYItemRendererState state,
Rectangle2D dataArea, PlotRenderingInfo info, XYPlot plot,
ValueAxis domainAxis, ValueAxis rangeAxis, XYDataset dataset,
int series, int item, CrosshairState crosshairState, int pass) {
boolean horiz;
PlotOrientation orientation = plot.getOrientation();
if (orientation == PlotOrientation.HORIZONTAL) {
horiz = true;
} else if (orientation == PlotOrientation.VERTICAL) {
horiz = false;
} else {
return;
}
// setup for collecting optional entity info...
EntityCollection entities = null;
if (info != null) {
entities = info.getOwner().getEntityCollection();
}
OHLCDataset highLowData = (OHLCDataset) dataset;
double x = highLowData.getXValue(series, item);
double yHigh = highLowData.getHighValue(series, item);
double yLow = highLowData.getLowValue(series, item);
double yOpen = highLowData.getOpenValue(series, item);
double yClose = highLowData.getCloseValue(series, item);
RectangleEdge domainEdge = plot.getDomainAxisEdge();
double xx = domainAxis.valueToJava2D(x, dataArea, domainEdge);
RectangleEdge edge = plot.getRangeAxisEdge();
double yyHigh = rangeAxis.valueToJava2D(yHigh, dataArea, edge);
double yyLow = rangeAxis.valueToJava2D(yLow, dataArea, edge);
double yyOpen = rangeAxis.valueToJava2D(yOpen, dataArea, edge);
double yyClose = rangeAxis.valueToJava2D(yClose, dataArea, edge);
double volumeWidth;
double stickWidth;
if (this.candleWidth > 0) {
// These are deliberately not bounded to minimums/maxCandleWidth to
// retain old behaviour.
volumeWidth = this.candleWidth;
stickWidth = this.candleWidth;
} else {
double xxWidth = 0;
int itemCount;
switch (this.autoWidthMethod) {
case WIDTHMETHOD_AVERAGE:
itemCount = highLowData.getItemCount(series);
if (horiz) {
xxWidth = dataArea.getHeight() / itemCount;
} else {
xxWidth = dataArea.getWidth() / itemCount;
}
break;
case WIDTHMETHOD_SMALLEST:
// Note: It would be nice to pre-calculate this per series
itemCount = highLowData.getItemCount(series);
double lastPos = -1;
xxWidth = dataArea.getWidth();
for (int i = 0; i < itemCount; i++) {
double pos = domainAxis.valueToJava2D(
highLowData.getXValue(series, i), dataArea,
domainEdge);
if (lastPos != -1) {
xxWidth = Math.min(xxWidth,
Math.abs(pos - lastPos));
}
lastPos = pos;
}
break;
case WIDTHMETHOD_INTERVALDATA:
IntervalXYDataset intervalXYData
= (IntervalXYDataset) dataset;
double startPos = domainAxis.valueToJava2D(
intervalXYData.getStartXValue(series, item),
dataArea, plot.getDomainAxisEdge());
double endPos = domainAxis.valueToJava2D(
intervalXYData.getEndXValue(series, item),
dataArea, plot.getDomainAxisEdge());
xxWidth = Math.abs(endPos - startPos);
break;
}
xxWidth -= 2 * this.autoWidthGap;
xxWidth *= this.autoWidthFactor;
xxWidth = Math.min(xxWidth, this.maxCandleWidth);
volumeWidth = Math.max(Math.min(1, this.maxCandleWidth), xxWidth);
stickWidth = Math.max(Math.min(3, this.maxCandleWidth), xxWidth);
}
Paint p = getItemPaint(series, item);
Paint outlinePaint = null;
if (this.useOutlinePaint) {
if (yClose > yOpen) {
if (this.upPaint != null) {
outlinePaint = this.upPaint;
} else {
outlinePaint = p;
}
} else {
if (this.downPaint != null) {
outlinePaint = this.downPaint;
} else {
outlinePaint = p;
}
}
// outlinePaint = getItemOutlinePaint(series, item);
}
Stroke s = getItemStroke(series, item);
g2.setStroke(s);
if (this.drawVolume) {
int volume = (int) highLowData.getVolumeValue(series, item);
double volumeHeight = volume / this.maxVolume;
double min, max;
if (horiz) {
min = dataArea.getMinX();
max = dataArea.getMaxX();
} else {
min = dataArea.getMinY();
max = dataArea.getMaxY();
}
double zzVolume = volumeHeight * (max - min);
g2.setPaint(getVolumePaint());
Composite originalComposite = g2.getComposite();
g2.setComposite(AlphaComposite.getInstance(
AlphaComposite.SRC_OVER, 0.3f));
if (horiz) {
g2.fill(new Rectangle2D.Double(min, xx - volumeWidth / 2,
zzVolume, volumeWidth));
} else {
g2.fill(new Rectangle2D.Double(xx - volumeWidth / 2,
max - zzVolume, volumeWidth, zzVolume));
}
g2.setComposite(originalComposite);
}
if (this.useOutlinePaint) {
g2.setPaint(outlinePaint);
} else {
g2.setPaint(p);
}
double yyMaxOpenClose = Math.max(yyOpen, yyClose);
double yyMinOpenClose = Math.min(yyOpen, yyClose);
double maxOpenClose = Math.max(yOpen, yClose);
double minOpenClose = Math.min(yOpen, yClose);
// draw the upper shadow
if (yHigh > maxOpenClose) {
if (horiz) {
g2.draw(new Line2D.Double(yyHigh, xx, yyMaxOpenClose, xx));
} else {
g2.draw(new Line2D.Double(xx, yyHigh, xx, yyMaxOpenClose));
}
}
// draw the lower shadow
if (yLow < minOpenClose) {
if (horiz) {
g2.draw(new Line2D.Double(yyLow, xx, yyMinOpenClose, xx));
} else {
g2.draw(new Line2D.Double(xx, yyLow, xx, yyMinOpenClose));
}
}
// draw the body
Rectangle2D body;
Rectangle2D hotspot;
double length = Math.abs(yyHigh - yyLow);
double base = Math.min(yyHigh, yyLow);
if (horiz) {
body = new Rectangle2D.Double(yyMinOpenClose, xx - stickWidth / 2,
yyMaxOpenClose - yyMinOpenClose, stickWidth);
hotspot = new Rectangle2D.Double(base, xx - stickWidth / 2,
length, stickWidth);
} else {
body = new Rectangle2D.Double(xx - stickWidth / 2, yyMinOpenClose,
stickWidth, yyMaxOpenClose - yyMinOpenClose);
hotspot = new Rectangle2D.Double(xx - stickWidth / 2,
base, stickWidth, length);
}
if (yClose > yOpen) {
if (this.upPaint != null) {
g2.setPaint(this.upPaint);
} else {
g2.setPaint(p);
}
g2.fill(body);
} else {
if (this.downPaint != null) {
g2.setPaint(this.downPaint);
} else {
g2.setPaint(p);
}
g2.fill(body);
}
if (this.useOutlinePaint) {
g2.setPaint(outlinePaint);
} else {
g2.setPaint(p);
}
g2.draw(body);
// add an entity for the item...
if (entities != null) {
addEntity(entities, hotspot, dataset, series, item, 0.0, 0.0);
}
}
/**
* Tests this renderer for equality with another object.
*
* @param obj the object ({@code null} permitted).
* @return {@code true} or {@code false}.
*/
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (!(obj instanceof KLinkCandlestickRenderer that)) {
return false;
}
if (this.candleWidth != that.candleWidth) {
return false;
}
if (!PaintUtils.equal(this.upPaint, that.upPaint)) {
return false;
}
if (!PaintUtils.equal(this.downPaint, that.downPaint)) {
return false;
}
if (this.drawVolume != that.drawVolume) {
return false;
}
if (this.maxCandleWidthInMilliseconds
!= that.maxCandleWidthInMilliseconds) {
return false;
}
if (this.autoWidthMethod != that.autoWidthMethod) {
return false;
}
if (this.autoWidthFactor != that.autoWidthFactor) {
return false;
}
if (this.autoWidthGap != that.autoWidthGap) {
return false;
}
if (this.useOutlinePaint != that.useOutlinePaint) {
return false;
}
if (!PaintUtils.equal(this.volumePaint, that.volumePaint)) {
return false;
}
return super.equals(obj);
}
/**
* Provides serialization support.
*
* @param stream the output stream.
* @throws IOException if there is an I/O error.
*/
@Serial
private void writeObject(ObjectOutputStream stream) throws IOException {
stream.defaultWriteObject();
SerialUtils.writePaint(this.upPaint, stream);
SerialUtils.writePaint(this.downPaint, stream);
SerialUtils.writePaint(this.volumePaint, stream);
}
/**
* Provides serialization support.
*
* @param stream the input stream.
* @throws IOException if there is an I/O error.
* @throws ClassNotFoundException if there is a classpath problem.
*/
@Serial
private void readObject(ObjectInputStream stream)
throws IOException, ClassNotFoundException {
stream.defaultReadObject();
this.upPaint = SerialUtils.readPaint(stream);
this.downPaint = SerialUtils.readPaint(stream);
this.volumePaint = SerialUtils.readPaint(stream);
}
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
SegmentedTimeline
/* ===========================================================
* JFreeChart : a free chart library for the Java(tm) platform
* ===========================================================
*
* (C) Copyright 2000-2016, 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-2016, 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 com.xyy.analyse.util.jfree.klink;
import org.jfree.chart.axis.Timeline;
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>
* <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>
* <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>
* <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>
* <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>
* <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>
* <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:</p>
* <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>
* <p>Additionally the {@code 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 {@code 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.</p>
* <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>
* <p>Two factory methods newMondayThroughFridayTimeline() and
* newFifteenMinuteTimeline() are provided as examples to create special
* SegmentedTimelines.</p>
*
* @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()}.
*/
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} 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} 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} 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.
*/
@Override
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).
*/
@Override
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).
*/
@Override
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} if a value is contained in the timeline.
*
* @param millisecond the value to verify.
*
* @return {@code true} if value is contained in the timeline.
*/
@Override
public boolean containsDomainValue(long millisecond) {
Segment segment = getSegment(millisecond);
return segment.inIncludeSegments();
}
/**
* Returns {@code true} if a value is contained in the timeline.
*
* @param date date to verify
*
* @return {@code true} if value is contained in the timeline
*/
@Override
public boolean containsDomainValue(Date date) {
return containsDomainValue(getTime(date));
}
/**
* Returns {@code 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 {@code true} if the range is contained in the timeline
*/
@Override
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} 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} if the range is contained in the timeline
*/
@Override
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} will be returned for the smallest
* {@code 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
* {@code date + segmentSize*m} will be returned for the smallest
* {@code 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 objects, taking into account
* nulls.
*
* @param o first object to compare
* @param p second object to compare
*
* @return {@code true} if both objects are equal or both
* {@code null}, {@code 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 {@code true} or {@code false}
*/
@Override
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.
*/
@Override
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 ??.
*/
@Override
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} if the millisecond is contained in the
* segment.
*/
public boolean contains(long millis) {
return (this.segmentStart <= millis && millis <= this.segmentEnd);
}
/**
* Returns {@code true} 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} 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} if a segment is contained in this segment.
*
* @param segment the segment to test for inclusion
*
* @return {@code true} if the segment is contained in this
* segment.
*/
public boolean contains(Segment segment) {
return contains(segment.getSegmentStart(), segment.getSegmentEnd());
}
/**
* Returns {@code true} 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} 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} 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} 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}) for equality
* with this segment.
*
* @param object The other segment to compare with us
*
* @return {@code true} if we are the same segment
*/
@Override
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} 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
*/
@Override
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} or {@code false}.
*/
public boolean inIncludeSegments() {
if (getSegmentNumberRelativeToGroup()
< SegmentedTimeline.this.segmentsIncluded) {
return !inExceptionSegments();
}
else {
return false;
}
}
/**
* Returns true if we are an excluded segment.
*
* @return {@code true} or {@code false}.
*/
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} 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.
*/
@Override
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.
*/
@Override
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} or {@code false}.
*/
@Override
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} or {@code false}.
*/
@Override
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.
*/
@Override
public void inc(long n) {
throw new IllegalArgumentException(
"Not implemented in SegmentRange");
}
}
/**
* Special {@code 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);
}
}
}