JFreeChart K线图

发布于:2024-10-11 ⋅ 阅读:(108) ⋅ 点赞:(0)

效果图:
在这里插入图片描述

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>
 * &lt;space&gt; = 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 &lt;= domainValue &lt;= 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
     * &lt;= domainValue and segmentEnd &lt;= 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 &lt;= domainValue &lt;= 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 &gt;= 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 &lt; object, 0: this.equal(object) and
         *         +1: this &gt; 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);
        }

    }

}

网站公告

今日签到

点亮在社区的每一天
去签到