net.schweerelos.timeline.model.Timeline.java Source code

Java tutorial

Introduction

Here is the source code for net.schweerelos.timeline.model.Timeline.java

Source

/*
 * Copyright (C) 2011 Andrea Schweer
 *
 * This file is part of the Digital Parrot. 
 *
 * The Digital Parrot is free software; you can redistribute it and/or modify
 * it under the terms of the Eclipse Public License as published by the Eclipse
 * Foundation or its Agreement Steward, either version 1.0 of the License, or
 * (at your option) any later version.
 *
 * The Digital Parrot 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 Eclipse Public License for
 * more details.
 *
 * You should have received a copy of the Eclipse Public License along with the
 * Digital Parrot. If not, see http://www.eclipse.org/legal/epl-v10.html. 
 *
 */

package net.schweerelos.timeline.model;

import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import net.schweerelos.parrot.timeline.IntervalListener;

import org.apache.log4j.Logger;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.Days;
import org.joda.time.Duration;
import org.joda.time.Interval;
import org.joda.time.Minutes;
import org.joda.time.Months;
import org.joda.time.Period;
import org.joda.time.Weeks;
import org.joda.time.Years;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;

public class Timeline<T> {

    private static final String INTERVAL_PROPERTY_KEY = "interval";

    public enum Mode {
        Years, Months, Weeks, Days
    }

    private IntervalChain<T> allIntervals;
    private Set<PayloadInterval<T>> currentlyVisibleIntervals = new HashSet<PayloadInterval<T>>();
    private Set<PayloadInterval<T>> intervalsWithinRange = new HashSet<PayloadInterval<T>>();

    private DateTime start;
    private DateTime end;

    private int numSlices;

    private Period increment;
    private Mode incrementMode;

    private PropertyChangeSupport changeSupport;

    private SliceLabelExtractor sliceLabelExtractor;

    private Logger logger;

    public Timeline(IntervalChain<T> intervals) {
        logger = Logger.getLogger(Timeline.class);

        allIntervals = intervals;
        changeSupport = new PropertyChangeSupport(this);
        if (intervals != null) {
            setInterval(intervals.getFirstStart(), intervals.getLastEnd());
        }
    }

    public void setInterval(DateTime start, DateTime end) {
        DateTimeZone defaultTimeZone = DateTimeZone.getDefault();

        // make sure we don't start before the start of the intervals (in years)
        DateTime firstStart = allIntervals.getFirstStart();
        firstStart = firstStart.withDayOfYear(firstStart.dayOfYear().getMinimumValue());
        if (start != null && start.toDateTime(defaultTimeZone).isBefore(firstStart)) {
            this.start = firstStart;
        } else {
            this.start = start;
        }

        // make sure we don't end after the end of the intervals (in years)
        DateTime lastEnd = allIntervals.getLastEnd();
        lastEnd = lastEnd.withDayOfYear(lastEnd.dayOfYear().getMaximumValue());
        if (end != null && end.toDateTime(defaultTimeZone).isAfter(lastEnd)) {
            this.end = lastEnd;
        } else {
            this.end = end;
        }

        recalculate();
    }

    private void recalculate() {
        if (start == null || end == null) {
            logger.warn("recalculating aborted, start and/or end is null");
            numSlices = 0;
            return;
        }
        Interval interval = new Interval(start, end);

        if (Years.yearsIn(interval).isGreaterThan(Years.ZERO)) {
            // make it start at the start of the current increment mode
            start = start.withDayOfYear(start.dayOfYear().getMinimumValue());
            end = end.withDayOfYear(end.dayOfYear().getMaximumValue());
            interval = new Interval(start, end);

            // figure out number of slices
            numSlices = Years.yearsIn(interval).getYears();
            if (start.plusYears(numSlices).isBefore(end)) {
                numSlices += 1;
            }

            // update label extractor
            sliceLabelExtractor = new SliceLabelExtractor() {
                @Override
                public String extractLabel(DateTime from) {
                    return from.year().getAsShortText();
                }
            };

            // update increment
            increment = Years.ONE.toPeriod();
            incrementMode = Mode.Years;
        } else if (Months.monthsIn(interval).isGreaterThan(Months.ZERO)) {
            // make it start at the start of the current increment mode
            start = start.withDayOfMonth(start.dayOfMonth().getMinimumValue());
            end = end.withDayOfMonth(end.dayOfMonth().getMaximumValue());
            interval = new Interval(start, end);

            numSlices = Months.monthsIn(interval).getMonths();
            if (start.plusMonths(numSlices).isBefore(end)) {
                numSlices += 1;
            }

            sliceLabelExtractor = new SliceLabelExtractor() {
                @Override
                public String extractLabel(DateTime from) {
                    return from.monthOfYear().getAsShortText();
                }
            };

            increment = Months.ONE.toPeriod();
            incrementMode = Mode.Months;
        } else if (Weeks.weeksIn(interval).isGreaterThan(Weeks.ZERO)) {
            start = start.withDayOfWeek(start.dayOfWeek().getMinimumValue());
            end = end.withDayOfWeek(end.dayOfWeek().getMaximumValue());
            interval = new Interval(start, end);

            numSlices = Weeks.weeksIn(interval).getWeeks();
            if (start.plusWeeks(numSlices).isBefore(end)) {
                numSlices += 1;
            }

            sliceLabelExtractor = new SliceLabelExtractor() {
                @Override
                public String extractLabel(DateTime from) {
                    return "W" + from.weekOfWeekyear().getAsShortText();
                }
            };

            increment = Weeks.ONE.toPeriod();
            incrementMode = Mode.Weeks;
        } else {
            numSlices = Days.daysIn(interval).getDays();
            if (start.plusDays(numSlices).isBefore(end)) {
                numSlices += 1;
            }
            if (numSlices == 0) {
                // force at least one day to be drawn
                numSlices = 1;
            }

            sliceLabelExtractor = new SliceLabelExtractor() {
                @Override
                public String extractLabel(DateTime from) {
                    return from.dayOfMonth().getAsShortText();
                }
            };

            increment = Days.ONE.toPeriod();
            incrementMode = Mode.Days;
        }

        // reset time of day too
        start = start.withMillisOfDay(start.millisOfDay().getMinimumValue());
        end = end.withMillisOfDay(end.millisOfDay().getMaximumValue());

        // recalculate which intervals are within range
        intervalsWithinRange.clear();
        intervalsWithinRange.addAll(calculateIntervalsWithinRange(start, end));

        // notify listeners
        changeSupport.firePropertyChange(INTERVAL_PROPERTY_KEY, interval, new Interval(start, end));
    }

    private Set<PayloadInterval<T>> calculateIntervalsWithinRange(DateTime rangeStart, DateTime rangeEnd) {
        Set<PayloadInterval<T>> result = new HashSet<PayloadInterval<T>>();
        Interval range = new Interval(rangeStart, rangeEnd);
        for (PayloadInterval<T> interval : allIntervals) {
            if (range.contains(interval.getStart()) && range.contains(interval.getEnd())) {
                result.add(interval);
            }
        }
        return result;
    }

    public Set<PayloadInterval<T>> getVisibleIntervals(Duration minLength) {
        Set<PayloadInterval<T>> result = new HashSet<PayloadInterval<T>>();
        List<PayloadInterval<T>> intervals = allIntervals.getIntervals();
        for (PayloadInterval<T> interval : intervals) {
            DateTime intervalStart = interval.getStart();
            DateTime intervalEnd = interval.getEnd();

            // if interval is completely outside of time shown in timeline
            // -> skip this interval
            if (intervalEnd.isBefore(start) || intervalStart.isAfter(end)) {
                continue;
            }

            // if interval is too short
            // -> skip this interval
            Duration length = interval.toInterval().toDuration();
            if (length.isShorterThan(minLength)) {
                continue;
            }

            if (isWithinRange(intervalStart)) {
                if (isWithinRange(intervalEnd)) {
                    // interval is completely inside time shown in timeline
                    result.add(interval);
                } else {
                    // interval starts during timeline but ends later
                    result.add(interval);
                }
            } else if (isWithinRange(intervalEnd)) {
                // interval starts before timeline but ends within
                result.add(interval);
            } else {
                // interval start before timeline and ends later
                result.add(interval);
            }
        }
        return result;
    }

    public boolean isWithinRange(DateTime date) {
        boolean notBeforeStart = date.isAfter(start) || date.isEqual(start);
        boolean notAfterEnd = date.isBefore(end) || date.isEqual(end);
        return notBeforeStart && notAfterEnd;
    }

    public DateTime getStart() {
        return start;
    }

    public DateTime getEnd() {
        return end;
    }

    public Duration getDuration() {
        return new Duration(start, end);
    }

    public boolean isBeforeStart(DateTime date) {
        return date.isBefore(start);
    }

    public void addIntervalListener(IntervalListener listener) {
        changeSupport.addPropertyChangeListener(INTERVAL_PROPERTY_KEY, listener);
    }

    public void removeIntervalListener(IntervalListener listener) {
        changeSupport.removePropertyChangeListener(INTERVAL_PROPERTY_KEY, listener);
    }

    public Set<PayloadInterval<T>> getIntervalsWithinRange() {
        return intervalsWithinRange;
    }

    public void clear() {
        PropertyChangeListener[] listeners = changeSupport.getPropertyChangeListeners();
        for (int i = 0; i < listeners.length; i++) {
            changeSupport.removePropertyChangeListener(listeners[i]);
        }

        allIntervals = null;
        currentlyVisibleIntervals.clear();
        intervalsWithinRange.clear();

        start = null;
        end = null;

        numSlices = 0;
        increment = null;

        sliceLabelExtractor = null;
    }

    public int getNumSlices() {
        return numSlices;
    }

    public Period getIncrement() {
        return increment;
    }

    public Mode getIncrementMode() {
        return incrementMode;
    }

    public boolean canZoomInFurther() {
        if (allIntervals == null || incrementMode == null) {
            return false;
        }
        return numSlices > 1 || incrementMode != Mode.Days;
    }

    public boolean canZoomOutFurther() {
        if (allIntervals == null || start == null || end == null) {
            return false;
        }
        return start.isAfter(allIntervals.getFirstStart()) || end.isBefore(allIntervals.getLastEnd());
    }

    public Interval convertSliceToInterval(int row) {
        if (row > -1) {
            DateTime periodStart = start;
            for (int i = 0; i < row; i++) {
                Duration addDuration = increment.toDurationFrom(periodStart);
                periodStart = periodStart.plus(addDuration);
            }
            Duration addDuration = increment.toDurationFrom(periodStart);
            DateTime periodEnd = periodStart.plus(addDuration);
            if (periodEnd.isAfter(end)) {
                periodEnd = end;
            }
            periodEnd = periodEnd.minus(Minutes.ONE);
            return new Interval(periodStart, periodEnd);
        } else {
            return null;
        }
    }

    private interface SliceLabelExtractor {
        String extractLabel(DateTime from);
    }

    public String extractLabel(DateTime sliceStart) {
        return sliceLabelExtractor.extractLabel(sliceStart);
    }

    public String extractLabel(int slice) {
        Interval interval;
        try {
            interval = convertSliceToInterval(slice);
        } catch (IllegalArgumentException iae) {
            iae.printStackTrace();
            return "";
        }
        DateTimeFormatter format = DateTimeFormat.shortDate();
        if (incrementMode == Mode.Days) {
            return interval.getStart().toString(format);
        } else {
            String incrementString = "";
            switch (incrementMode) {
            case Years:
                incrementString = "Year " + sliceLabelExtractor.extractLabel(interval.getStart()) + " (";
                break;
            case Months:
                incrementString = "Month " + sliceLabelExtractor.extractLabel(interval.getStart()) + " (";
                break;
            case Weeks:
                incrementString = "Week " + sliceLabelExtractor.extractLabel(interval.getStart()) + " (";
                break;
            }
            return incrementString + interval.getStart().toString(format) + " to "
                    + interval.getEnd().toString(format) + ")";
        }
    }

    public int countIntervalsWithinRange(Interval sliceInterval) {
        Set<PayloadInterval<T>> intervals = calculateIntervalsWithinRange(sliceInterval.getStart(),
                sliceInterval.getEnd());
        return intervals.size();
    }

}