com.spaceapplications.vaadin.addon.eventtimeline.EventTimeline.java Source code

Java tutorial

Introduction

Here is the source code for com.spaceapplications.vaadin.addon.eventtimeline.EventTimeline.java

Source

/*
@VaadinAddonLicenseForJavaFiles@
 */
package com.spaceapplications.vaadin.addon.eventtimeline;

import java.io.Serializable;
import java.lang.reflect.Method;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import com.spaceapplications.vaadin.addon.eventtimeline.event.TimelineEvent;
import com.spaceapplications.vaadin.addon.eventtimeline.event.TimelineEventProvider;
import com.spaceapplications.vaadin.addon.eventtimeline.event.TimelineEventProvider.EventSetChange;
import com.spaceapplications.vaadin.addon.eventtimeline.event.TimelineEventProvider.EventSetChangeListener;
import com.spaceapplications.vaadin.addon.eventtimeline.gwt.client.VEventTimelineWidget;
import com.vaadin.terminal.PaintException;
import com.vaadin.terminal.PaintTarget;
import com.vaadin.ui.AbstractComponent;
import com.vaadin.ui.ClientWidget;
import com.vaadin.ui.ClientWidget.LoadStyle;
import com.vaadin.ui.Component;

/**
 * EventTimeline implementation, based on original version from vaadin-timeline.
 * 
 * @author Thomas Neidhart / Space Applications Services NV/SA
 * @author Florian Pirchner (add / remove bands)
 * @author John Ahlroos / IT Mill Oy Ltd 2010
 */
@ClientWidget(value = VEventTimelineWidget.class, loadStyle = LoadStyle.EAGER)
public class EventTimeline extends AbstractComponent implements EventSetChangeListener {

    private static final long serialVersionUID = 6595058445231789530L;

    // The style name
    private static final String STYLENAME = "v-eventtimeline";

    // Event band information
    private final List<BandInfo> bandInfos = new ArrayList<BandInfo>();
    private final List<BandInfo> bandsToBeRemoved = new ArrayList<EventTimeline.BandInfo>();
    private final List<BandInfo> bandsToBeAdded = new ArrayList<EventTimeline.BandInfo>();
    private int lastBandId = -1;

    // The number of event bands visible in the event page area
    private int eventBandPageSize = -1;

    // Initialization is done
    private boolean initDone = false;

    // Should the time limits be sent
    private boolean sendTimeLimits = false;

    // Should the captions for the zoom etc. be sent
    private boolean sendUICaptions = false;

    // Should the bands information be sent
    private boolean sendBands = false;

    // Should a refresh command be sent
    private boolean sendRefreshRequest = false;

    // Should the date range be sent. (Maximum and Minimum dates be sent)
    private boolean sendDateRange = false;

    // Activate change events
    private boolean sendChangeEventAvailable = false;

    // Should the component visibilities be sent
    private boolean sendComponentVisibilities = false;

    // Should the zoom levels be sent
    private boolean sendZoomLevels = false;

    // Should the graph grid color be sent
    private boolean sendGridColor = false;

    // Should the date format info be sent
    private boolean sendDateFormatInfo = false;

    // Is the browser bar locked to the selection
    protected boolean selectionBarLocked = true;

    // Should a the page size be sent
    private boolean sendEventBandPageSize = false;

    // Should the band height be sent
    private boolean sendBandHeight = false;

    // The events to send in the next refresh
    private Date eventsStartTime = null;
    private Date eventsEndTime = null;
    private final Map<Integer, List<TimelineEvent>> eventsToSend = new HashMap<Integer, List<TimelineEvent>>();

    /**
     * The zoom levels to send in the next refresh.<br/>
     * key = Caption of the zoom level<br/>
     * value = The time in milliseconds of the zoom level<br/>
     */
    private final Map<String, Long> zoomLevels = new LinkedHashMap<String, Long>();

    /*
     * We need to keep count of the date range listeners so we can turn off the feature if not needed
     */
    private int dateRangeListenerCounter = 0;

    // The minimum date of all the graphs
    private Date minDate = null;

    // The maximum data of all the graphs
    private Date maxDate = null;

    // Selection minimum date
    private Date selectedStartDate = null;

    // Selection maximum date
    private Date selectedEndDate = null;

    // Is the browser visible
    protected boolean browserIsVisible = true;

    // Is the zoom levels visible
    protected boolean zoomIsVisible = true;

    // The caption of the zoom levels
    protected String zoomLevelCaption = "Zoom";

    // The captions of the band paging navigation
    private PageNavigationCaptions pagingCaption = new PageNavigationCaptions("Pages", "next", "previous");

    // Is the date select visible
    protected boolean dateSelectVisible = true;

    // Is the date select enabled
    protected boolean dateSelectEnabled = true;

    // Is the legend visible
    protected boolean legendVisible = false;

    // Is the page navigation visible
    protected boolean pageNavigationVisible;

    // Is the band selection enabled
    protected boolean bandSelectionEnabled;

    // The graph grid color (as CSS3 style rgba string)
    protected String gridColor = "rgba(192,192,192,1)";

    // The height of a band, -1 indicates automatic sizing
    protected int bandHeight = -1;

    // Date formats
    protected final DateFormatInfo dateFormatInfo = new DateFormatInfo();

    /** Date format that will be used in the UIDL for dates. */
    protected DateFormat df_date = new SimpleDateFormat("yyyy-MM-dd");

    /** Time format that will be used in the UIDL for time. */
    protected DateFormat df_time = new SimpleDateFormat("HH:mm:ss");

    /** Date format that will be used in the UIDL for both date and time. */
    protected DateFormat df_date_time = new SimpleDateFormat("yyyy-MM-dd-HH-mm");

    /**
     * Date range changed event.<br/>
     * The date range changed event represents a change in the time span.
     */
    public class DateRangeChangedEvent extends Component.Event {

        private static final long serialVersionUID = -5424380516338748718L;

        private Date startDate;

        private Date endDate;

        /**
         * Default constructor.<br/>
         * See {@link Component.Event} for more details.
         * 
         * @param source
         *          The source of the event
         */
        public DateRangeChangedEvent(Component source) {
            super(source);
        }

        /**
         * See {@link Component.Event} for more details.
         * 
         * @param source
         *          The source of the event
         * @param start
         *          The start date of the range
         * @param end
         *          The end date of the range
         */
        public DateRangeChangedEvent(Component source, Date start, Date end) {
            super(source);
            startDate = start;
            endDate = end;
        }

        /**
         * Returns the start date of the range
         * 
         * @return The start date
         */
        public Date getStartDate() {
            return startDate;
        }

        /**
         * Returns the end date of the range
         * 
         * @return The end date
         */
        public Date getEndDate() {
            return endDate;
        }
    }

    /**
     * Event button click event. This event is sent when a user clicks an event button in the graph.
     * 
     */
    public class EventButtonClickEvent extends Component.Event {

        private static final long serialVersionUID = 1215106616175652769L;

        private Object id;

        /**
         * See {@link Component.Event} for details.
         * 
         * @param source
         *          The source of the event
         */
        public EventButtonClickEvent(Component source) {
            super(source);
        }

        /**
         * See {@link Component.Event} for more details.
         * 
         * @param source
         *          The source of the event
         * @param itemIds
         *          The item id:s in the event data source which are related to the event
         */
        public EventButtonClickEvent(Component source, Object itemId) {
            super(source);
            id = itemId;
        }

        /**
         * Gets the item id:s in the event data source which are related to the event
         * 
         * @return The item id:s related to the event
         */
        public Object getItemId() {
            return id;
        }
    }

    /**
     * Band selection event. This event is sent when a user clicks a band in the graph.
     */
    public class BandSelectionEvent extends Component.Event {

        private static final long serialVersionUID = -621449416684203285L;

        private int id;

        /**
         * See {@link Component.Event} for details.
         * 
         * @param source
         *          The source of the event
         */
        public BandSelectionEvent(Component source) {
            super(source);
        }

        /**
         * See {@link Component.Event} for more details.
         * 
         * @param source
         *          The source of the event
         * @param id
         *          The band id.
         */
        public BandSelectionEvent(Component source, int id) {
            super(source);
            this.id = id;
        }

        /**
         * Returns the id of the selected band.
         * 
         * @return The band id.
         */
        public int getBandId() {
            return id;
        }
    }

    /**
     * Page navigation event. This event is sent when a user selected the next / previous page button.
     */
    public class PageNavigationEvent extends Component.Event {

        private static final long serialVersionUID = -5391363403702750953L;

        private final int page;

        /**
         * 
         * @param source
         *          The source of the event
         * @param page
         *          If true, then the next band is requested. False otherwise.
         */
        public PageNavigationEvent(Component source, int page) {
            super(source);

            this.page = page;
        }

        /**
         * Returns the page number of the bands area.
         * 
         * @return the page
         */
        public int getPage() {
            return page;
        }

    }

    /**
     * Describes the date formats used by the EventTimeline.
     */
    public class DateFormatInfo implements Serializable {

        private static final long serialVersionUID = -3103432458378549206L;

        private String dateSelectDisplaySimpleDateFormat = "MMM d, y";
        private String dateSelectEditSimpleDateFormat = "dd/MM/yyyy";
        private String shortYearFormat = "''yy";
        private String longYearFormat = "yyyy";
        private String shortMonthFormat = "MMM yyyy";
        private String longMonthFormat = "MMMM yyyy";
        private String shortDayFormat = "MMM d, yyyy";
        private String longDayFormat = "MMMM d, yyyy";
        private String shortTimeFormat = "HH:mm";
        private String longTimeFormat = "HH:mm:ss";

        private static final char DELIM = '|';

        /**
         * Get the date format which is used to display the selected date range in the top right corner.
         * 
         * See {@link SimpleDateFormat} for format details.
         * 
         * @return A format describing how the date should be formatted
         */
        public String getDateSelectDisplaySimpleDateFormat() {
            return dateSelectDisplaySimpleDateFormat;
        }

        /**
         * Set the date format used to display the selected date range in the top right corner.
         * 
         * See {@link SimpleDateFormat} for format details.
         * 
         * @param format
         *          A format describing how the date should be formatted
         */
        public void setDateSelectDisplaySimpleDateFormat(String format) {
            if (dateSelectDisplaySimpleDateFormat != null) {
                this.dateSelectDisplaySimpleDateFormat = format;
                sendDateFormatInfo = true;
                if (initDone) {
                    requestRepaint();
                }
            }
        }

        /**
         * Get the date format which is used to edit the selected date range in the top right corner
         * 
         * See {@link SimpleDateFormat} for format details.
         * 
         * @return A format describing how the date should be formatted
         */
        public String getDateSelectEditSimpleDateFormat() {
            return dateSelectEditSimpleDateFormat;
        }

        /**
         * Set the date format used to display the selected date range in the top right corner.
         * 
         * See {@link SimpleDateFormat} for format details.
         * 
         * @param format
         *          A format describing how the date should be formatted
         */
        public void setDateSelectEditSimpleDateFormat(String format) {
            if (dateSelectEditSimpleDateFormat != null) {
                this.dateSelectEditSimpleDateFormat = format;
                sendDateFormatInfo = true;
                if (initDone) {
                    requestRepaint();
                }
            }
        }

        /**
         * Get the date format which is displayed in the horizontal scale when the scale has a
         * year-resolution and only a little amount of space is available.
         * 
         * See {@link SimpleDateFormat} for format details.
         * 
         * @return A format describing how the date should be formatted
         */
        public String getShortYearFormat() {
            return shortYearFormat;
        }

        /**
         * Set the date format which is displayed in the horizontal scale when the scale has a
         * year-resolution and only a little amount of space is available.
         * 
         * See {@link SimpleDateFormat} for format details.
         * 
         * @return A format describing how the date should be formatted
         */
        public void setShortYearFormat(String shortYearFormat) {
            if (shortYearFormat != null) {
                this.shortYearFormat = shortYearFormat;
                sendDateFormatInfo = true;
                if (initDone) {
                    requestRepaint();
                }
            }
        }

        /**
         * Get the date format which is displayed in the horizontal scale when the scale has a
         * year-resolution.
         * 
         * See {@link SimpleDateFormat} for format details.
         * 
         * @return A format describing how the date should be formatted
         */
        public String getLongYearFormat() {
            return longYearFormat;
        }

        /**
         * Get the date format which is displayed in the horizontal scale when the scale has a
         * year-resolution.
         * 
         * See {@link SimpleDateFormat} for format details.
         * 
         * @return A format describing how the date should be formatted
         */
        public void setLongYearFormat(String longYearFormat) {
            if (longYearFormat != null) {
                this.longYearFormat = longYearFormat;
                sendDateFormatInfo = true;
                if (initDone) {
                    requestRepaint();
                }
            }
        }

        /**
         * Get the date format which is displayed in the horizontal scale when the scale has a
         * month-resolution and only a little amount of space is available.
         * 
         * See {@link SimpleDateFormat} for format details.
         * 
         * @return A format describing how the date should be formatted
         */
        public String getShortMonthFormat() {
            return shortMonthFormat;
        }

        /**
         * Set the date format which is displayed in the horizontal scale when the scale has a
         * month-resolution and only a little amount of space is available.
         * 
         * See {@link SimpleDateFormat} for format details.
         * 
         * @return A format describing how the date should be formatted
         */
        public void setShortMonthFormat(String shortMonthFormat) {
            if (shortMonthFormat != null) {
                this.shortMonthFormat = shortMonthFormat;
                sendDateFormatInfo = true;
                if (initDone) {
                    requestRepaint();
                }
            }
        }

        /**
         * Get the date format which is displayed in the horizontal scale when the scale has a
         * month-resolution.
         * 
         * See {@link SimpleDateFormat} for format details.
         * 
         * @return A format describing how the date should be formatted
         */
        public String getLongMonthFormat() {
            return longMonthFormat;
        }

        /**
         * Set the date format which is displayed in the horizontal scale when the scale has a
         * month-resolution.
         * 
         * See {@link SimpleDateFormat} for format details.
         * 
         * @return A format describing how the date should be formatted
         */
        public void setLongMonthFormat(String longMonthFormat) {
            if (longMonthFormat != null) {
                this.longMonthFormat = longMonthFormat;
                sendDateFormatInfo = true;
                if (initDone) {
                    requestRepaint();
                }
            }
        }

        /**
         * Get the date format which is displayed in the horizontal scale when the scale has a
         * day-resolution and only a little amount of space is available.
         * 
         * See {@link SimpleDateFormat} for format details.
         * 
         * @return A format describing how the date should be formatted
         */
        public String getShortDayFormat() {
            return shortDayFormat;
        }

        /**
         * Set the date format which is displayed in the horizontal scale when the scale has a
         * day-resolution and only a little amount of space is available.
         * 
         * See {@link SimpleDateFormat} for format details.
         * 
         * @return A format describing how the date should be formatted
         */
        public void setShortDayFormat(String shortDayFormat) {
            if (shortDayFormat != null) {
                this.shortDayFormat = shortDayFormat;
                sendDateFormatInfo = true;
                if (initDone) {
                    requestRepaint();
                }
            }
        }

        /**
         * Get the date format which is displayed in the horizontal scale when the scale has a
         * day-resolution.
         * 
         * See {@link SimpleDateFormat} for format details.
         * 
         * @return A format describing how the date should be formatted
         */
        public String getLongDayFormat() {
            return longDayFormat;
        }

        /**
         * Set the date format which is displayed in the horizontal scale when the scale has a
         * day-resolution.
         * 
         * See {@link SimpleDateFormat} for format details.
         * 
         * @return A format describing how the date should be formatted
         */
        public void setLongDayFormat(String longDayFormat) {
            if (longDayFormat != null) {
                this.longDayFormat = longDayFormat;
                sendDateFormatInfo = true;
                if (initDone) {
                    requestRepaint();
                }
            }
        }

        /**
         * Get the date format which is displayed in the horizontal scale when the scale is displaying
         * time and only a little amount of space is available.
         * 
         * See {@link SimpleDateFormat} for format details.
         * 
         * @return A format describing how the date should be formatted
         */
        public String getShortTimeFormat() {
            return shortTimeFormat;
        }

        /**
         * Set the date format which is displayed in the horizontal scale when the scale is displaying
         * time and only a little amount of space is available.
         * 
         * See {@link SimpleDateFormat} for format details.
         * 
         * @return A format describing how the date should be formatted
         */
        public void setShortTimeFormat(String shortTimeFormat) {
            if (shortTimeFormat != null) {
                this.shortTimeFormat = shortTimeFormat;
                sendDateFormatInfo = true;
                if (initDone) {
                    requestRepaint();
                }
            }
        }

        /**
         * Get the date format which is displayed in the horizontal scale when the scale is displaying
         * time.
         * 
         * See {@link SimpleDateFormat} for format details.
         * 
         * @return A format describing how the date should be formatted
         */
        public String getLongTimeFormat() {
            return longTimeFormat;
        }

        /**
         * Set the date format which is displayed in the horizontal scale when the scale is displaying
         * time.
         * 
         * See {@link SimpleDateFormat} for format details.
         * 
         * @return A format describing how the date should be formatted
         */
        public void setLongTimeFormat(String longTimeFormat) {
            if (longTimeFormat != null) {
                this.longTimeFormat = longTimeFormat;
                sendDateFormatInfo = true;
                if (initDone) {
                    requestRepaint();
                }
            }
        }

        /**
         * Serializes the date formats into a string which can be sent to the client.
         * 
         * @return
         */
        public String serialize() {
            return dateSelectDisplaySimpleDateFormat + DELIM + dateSelectEditSimpleDateFormat + DELIM
                    + shortYearFormat + DELIM + longYearFormat + DELIM + shortMonthFormat + DELIM + longMonthFormat
                    + DELIM + shortDayFormat + DELIM + longDayFormat + DELIM + shortTimeFormat + DELIM
                    + longTimeFormat;
        }

        @Override
        public String toString() {
            return serialize();
        }
    }

    /**
     * The date range listener interface
     */
    public interface DateRangeListener {
        public void dateRangeChanged(DateRangeChangedEvent event);
    }

    /**
     * The event click listener interface
     */
    public interface EventClickListener {
        public void eventClick(EventButtonClickEvent event);
    }

    /**
     * The band selected listener interface
     */
    public interface BandSelectionListener {
        public void bandSelected(BandSelectionEvent event);
    }

    /**
     * The band paging listener interface
     */
    public interface PageNavigationListener {
        public void requestNavigation(PageNavigationEvent event);
    }

    /*
     * Event methods
     */
    private static final Method DATERANGE_CHANGED_METHOD;
    private static final Method EVENT_CLICK_METHOD;
    private static final Method PAGE_NAVIGATION_METHOD;
    private static final Method BAND_SELECTION_METHOD;

    static {
        try {
            DATERANGE_CHANGED_METHOD = DateRangeListener.class.getDeclaredMethod("dateRangeChanged",
                    new Class[] { DateRangeChangedEvent.class });

            EVENT_CLICK_METHOD = EventClickListener.class.getDeclaredMethod("eventClick",
                    new Class[] { EventButtonClickEvent.class });

            PAGE_NAVIGATION_METHOD = PageNavigationListener.class.getDeclaredMethod("requestNavigation",
                    new Class[] { PageNavigationEvent.class });

            BAND_SELECTION_METHOD = BandSelectionListener.class.getDeclaredMethod("bandSelected",
                    new Class[] { BandSelectionEvent.class });

        } catch (final java.lang.NoSuchMethodException e) {
            // This should never happen
            throw new java.lang.RuntimeException("Internal error finding methods in EventTimeline");
        }
    }

    /**
     * Default constructor
     */
    public EventTimeline() {
        setStyleName(STYLENAME);

        // Default size
        setWidth("400px");
        setHeight("300px");
    }

    /**
     * Constructor
     * 
     * @param caption
     *          The caption of the graph
     */
    public EventTimeline(String caption) {
        this();
        setCaption(caption);
    }

    /*
     * (non-Javadoc)
     * 
     * @see com.vaadin.ui.AbstractComponent#changeVariables(java.lang.Object, java.util.Map)
     */
    @Override
    public void changeVariables(Object source, @SuppressWarnings("rawtypes") Map variables) {

        // Initialization data requested from the client side (refresh occurred)
        if (variables.containsKey("init")) {

            // also re-send all bands
            if (!sendBands) {
                sendBands = true;
                for (BandInfo info : bandInfos) {
                    if (!bandsToBeAdded.contains(info)) {
                        bandsToBeAdded.add(info);
                    }
                }
            }

            initDataFlags();
        }

        // The client need some events to display
        if (variables.containsKey(VEventTimelineWidget.ATTR_EVENTS)) {
            Object[] indexes = (Object[]) variables.get(VEventTimelineWidget.ATTR_EVENTS);
            Date start = new Date(Long.parseLong(indexes[0].toString()));
            Date end = new Date(Long.parseLong(indexes[1].toString()));

            if (eventsStartTime == null) {
                eventsStartTime = start;
                eventsEndTime = end;
            } else {
                if (start.before(eventsStartTime)) {
                    eventsStartTime = start;
                }

                if (end.after(eventsEndTime)) {
                    eventsEndTime = end;
                }

                eventsToSend.clear();
            }

            eventsToSend.putAll(getAllEventsMap(eventsStartTime, eventsEndTime));
        }

        // Send the data to the client
        if (variables.containsKey("send") || variables.containsKey("init")) {
            requestRepaint();
        }

        // Date range changed event
        if (variables.containsKey("drce")) {
            Object[] values = (Object[]) variables.get("drce");
            selectedStartDate = new Date(Long.parseLong(values[0].toString()));
            selectedEndDate = new Date(Long.parseLong(values[1].toString()));

            fireDateRangeChangedEvent(selectedStartDate, selectedEndDate);
        }

        // Event button click event received
        if (variables.containsKey("ebce")) {
            String itemId = (String) variables.get("ebce");
            fireEventButtonClickEvent(itemId);
        }

        // The user requests the page of the band area
        if (variables.containsKey("bandPage")) {
            int page = (Integer) variables.get("bandPage");
            fireBandPagingEvent(page);
        }

        // The user selected a band
        if (variables.containsKey("bandSel")) {
            int bandId = (Integer) variables.get("bandSel");
            fireBandSelectionEvent(bandId);
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see com.vaadin.ui.AbstractComponent#paintContent(com.vaadin.terminal.PaintTarget )
     */
    @Override
    public void paintContent(PaintTarget target) throws PaintException {

        // Superclass writes any common attributes in the paint target.
        super.paintContent(target);

        // Send the selection lock
        target.addAttribute("lock", selectionBarLocked);

        // Always send change event available flag
        target.addAttribute("e1", sendChangeEventAvailable);

        // Always sending locale
        final Locale l = getLocale();
        if (l != null) {
            target.addAttribute("locale", l.toString());
        }

        // Add the events
        if (eventsToSend.size() > 0) {
            target.startTag(VEventTimelineWidget.ATTR_EVENTS);
            target.addAttribute(VEventTimelineWidget.ATTR_START, eventsStartTime.getTime());
            target.addAttribute(VEventTimelineWidget.ATTR_END, eventsEndTime.getTime());

            for (Map.Entry<Integer, List<TimelineEvent>> entry : eventsToSend.entrySet()) {
                target.startTag(VEventTimelineWidget.ATTR_BAND);
                target.addAttribute(VEventTimelineWidget.ATTR_BANDID, entry.getKey());
                for (TimelineEvent event : entry.getValue()) {
                    target.startTag(VEventTimelineWidget.ATTR_EVENT);
                    paintEvent(event, target);
                    target.endTag(VEventTimelineWidget.ATTR_EVENT);
                }
                target.endTag(VEventTimelineWidget.ATTR_BAND);
            }
            target.endTag(VEventTimelineWidget.ATTR_EVENTS);
            eventsToSend.clear();
            eventsStartTime = null;
            eventsEndTime = null;
        }

        // Send time limits
        if (sendTimeLimits && minDate != null && maxDate != null) {
            target.addAttribute("startDate", minDate.getTime());
            target.addAttribute("endDate", maxDate.getTime());
            sendTimeLimits = false;
        }

        // Send refresh request
        if (sendRefreshRequest) {
            target.addAttribute("refresh", true);
            sendRefreshRequest = false;
        }

        // Send new selected date range
        if (sendDateRange && selectedStartDate != null && selectedEndDate != null) {
            target.addVariable(this, "selectStart", selectedStartDate.getTime());
            target.addVariable(this, "selectEnd", selectedEndDate.getTime());

            sendDateRange = false;
        }

        // Send the component visibilities
        if (sendComponentVisibilities) {
            target.addAttribute("browserVisibility", browserIsVisible);
            target.addAttribute("zoomVisibility", zoomIsVisible);
            target.addAttribute("dateSelectVisibility", dateSelectVisible);
            target.addAttribute("dateSelectEnabled", dateSelectEnabled);
            target.addAttribute("legendVisibility", legendVisible);
            target.addAttribute("bandPagingVisible", pageNavigationVisible);
            target.addAttribute("bandSelectionEnabled", bandSelectionEnabled);
            sendComponentVisibilities = false;
        }

        // Send zoom levels
        if (sendZoomLevels) {
            // put them into an array of objects to keep the order
            String[] levels = new String[zoomLevels.size()];
            int idx = 0;
            for (Map.Entry<String, Long> entry : zoomLevels.entrySet()) {
                levels[idx++] = entry.getKey() + "," + entry.getValue();
            }
            target.addAttribute("zoomLevels", levels);
            sendZoomLevels = false;
        }

        // Send the graph grid color if it has changed
        if (sendGridColor) {
            // target
            target.addAttribute("gridColor", gridColor == null ? "" : gridColor);
            sendGridColor = false;
        }

        // Send the UI captions if they have changed
        if (sendUICaptions) {
            target.addAttribute("zlvlcaption", zoomLevelCaption);
            target.addAttribute("bpgingCaption", pagingCaption.getCaption());
            target.addAttribute("bpgingCptPrevious", pagingCaption.getCaption_previous());
            target.addAttribute("bpgingCptNext", pagingCaption.getCaption_next());

            sendUICaptions = false;
        }

        // Send the number of event bands visible
        if (sendEventBandPageSize) {
            target.addAttribute(VEventTimelineWidget.ATTR_BAND_PAGE_SIZE, eventBandPageSize);
            sendEventBandPageSize = false;
        }

        if (sendBandHeight) {
            target.addAttribute("bandheight", bandHeight);
            sendBandHeight = false;
        }

        if (sendBands) {
            if (bandsToBeRemoved.size() > 0 || bandsToBeAdded.size() > 0) {
                BandsPainter.start(target);
                for (BandInfo band : bandsToBeRemoved) {
                    BandsPainter.paintRemove(target, band.getBandId());
                }
                bandsToBeRemoved.clear();
                for (BandInfo band : bandsToBeAdded) {
                    BandsPainter.paintAdd(target, band.getBandId(), band.getCaption());
                }
                bandsToBeAdded.clear();
                BandsPainter.end(target);
            }
            sendBands = false;
        }

        // Send date formats if set
        if (sendDateFormatInfo) {
            target.addAttribute("dateformats", dateFormatInfo.serialize());
            sendDateFormatInfo = false;
        }
    }

    /**
     * Paints single timeline event to UIDL. Override this method to add custom attributes for events.
     * 
     * @param i
     *          Index of target Timeline.Event
     * @param target
     *          PaintTarget
     */
    protected void paintEvent(TimelineEvent e, PaintTarget target) throws PaintException {
        target.addAttribute(VEventTimelineWidget.ATTR_ID, e.getEventId());
        target.addAttribute(VEventTimelineWidget.ATTR_CAPTION, (e.getCaption() == null ? "" : e.getCaption()));
        target.addAttribute(VEventTimelineWidget.ATTR_DATEFROM, df_date.format(e.getStart()));
        target.addAttribute(VEventTimelineWidget.ATTR_DATETO, df_date.format(e.getEnd()));
        target.addAttribute(VEventTimelineWidget.ATTR_TIMEFROM, e.getStart().getTime());
        target.addAttribute(VEventTimelineWidget.ATTR_TIMETO, e.getEnd().getTime());
        target.addAttribute(VEventTimelineWidget.ATTR_DESCRIPTION,
                e.getDescription() == null ? "" : e.getDescription());
        target.addAttribute(VEventTimelineWidget.ATTR_STYLE, e.getStyleName() == null ? "" : e.getStyleName());
    }

    protected String[] getEventsAsStringArray(final List<TimelineEvent> events) throws PaintException {
        String[] result = new String[events.size()];

        int idx = 0;
        for (TimelineEvent e : events) {
            StringBuilder sb = new StringBuilder();
            sb.append(e.getEventId());
            sb.append(";");
            sb.append(e.getCaption());
            sb.append(";");
            sb.append(df_date.format(e.getStart()));
            sb.append(";");
            sb.append(df_date.format(e.getEnd()));
            sb.append(";");
            sb.append(e.getStart().getTime());
            sb.append(";");
            sb.append(e.getEnd().getTime());
            sb.append(";");
            sb.append(e.getDescription());
            sb.append(";");
            sb.append(e.getStyleName());
            result[idx++] = sb.toString();
        }

        return result;
    }

    /**
     * Add another event band.
     * 
     * @param caption
     *          The caption for this band
     * @param provider
     *          The provider for events displayed in this band
     * @return the created band
     */
    public BandInfo addEventBand(final String caption, final TimelineEventProvider provider) {
        lastBandId++;

        provider.addListener(this);
        BandInfo result = new BandInfo(lastBandId, provider, caption);
        bandInfos.add(result);
        bandsToBeAdded.add(result);

        if (initDone) {
            sendBands = true;
            List<TimelineEvent> events = provider.getEvents(minDate, maxDate);
            eventsToSend.put(result.getBandId(), events);
            eventsStartTime = minDate;
            eventsEndTime = maxDate;
            requestRepaint();
        }

        return result;
    }

    /**
     * Removes the event band.
     * 
     * @param provider
     *          The provider for events displayed in this band
     */
    public void removeEventBand(final TimelineEventProvider provider) {
        provider.removeListener(this);

        BandInfo result = null;
        for (BandInfo info : bandInfos) {
            if (info.getProvider() == provider) {
                result = info;
                break;
            }
        }

        if (result != null) {
            result.getProvider().removeListener(this);
            bandInfos.remove(result);
            // if the band was added previously before sending the band
            // to the client, than remove the band again
            if (bandsToBeAdded.contains(result)) {
                bandsToBeAdded.remove(result);
                eventsToSend.remove(result.getBandId());
            } else {
                bandsToBeRemoved.add(result);
            }
        }

        if (initDone) {
            sendBands = true;
            requestRepaint();
        }
    }

    /**
     * Removes all event bands from the timeline.
     */
    public void removeAllEventBands() {
        for (BandInfo info : getBandInfos()) {
            removeEventBand(info.getProvider());
        }
    }

    @Override
    public void eventSetChange(EventSetChange changeEvent) {
        TimelineEventProvider provider = changeEvent.getProvider();
        List<TimelineEvent> events = provider.getEvents(minDate, maxDate);

        int bandId = -1;
        for (BandInfo info : bandInfos) {
            if (info.getProvider().equals(provider)) {
                bandId = info.getBandId();
                break;
            }
        }

        if (bandId >= 0) {
            eventsToSend.put(bandId, events);
            eventsStartTime = minDate;
            eventsEndTime = maxDate;
        }
        requestRepaint();
    }

    /**
     * Sets the displayed time range.<br/>
     * The displayed time is the time range selected in the browser and displayed in the main area of
     * the component. The displayed time range cannot be set until some data source has been added to
     * the component.
     * 
     * @param start
     *          The start date
     * @param end
     *          The end data
     */
    public void setVisibleDateRange(Date start, Date end) {

        // Do consistency check
        if (start.equals(end) || end.after(start)) {
            // new time limits
            minDate = start;
            maxDate = end;
            sendTimeLimits = true;

            selectedStartDate = start;
            selectedEndDate = end;
            sendDateRange = true;
            if (initDone) {
                requestRepaint();
            }

        } else {
            throw new IllegalArgumentException("End date must come after the start date.");
        }
    }

    /**
     * Makes the whole graph visible and selected in the browser.
     */
    public void selectFullRange() {
        setVisibleDateRange(minDate, maxDate);
    }

    /**
     * Adds a date range listener.<br/>
     * This is triggered when the date range is changed.
     * 
     * @param listener
     *          The listener to be added
     */
    public void addListener(DateRangeListener listener) {
        addListener(DateRangeChangedEvent.class, listener, DATERANGE_CHANGED_METHOD);
        sendChangeEventAvailable = true;
        dateRangeListenerCounter++;
    }

    /**
     * Add a button click listener
     * 
     * @param listener
     *          The listener to be added
     */
    public void addListener(EventClickListener listener) {
        addListener(EventButtonClickEvent.class, listener, EVENT_CLICK_METHOD);
    }

    /**
     * Add a band paging listener
     * 
     * @param listener
     *          The listener to be added
     */
    public void addListener(PageNavigationListener listener) {
        addListener(PageNavigationEvent.class, listener, PAGE_NAVIGATION_METHOD);
    }

    /**
     * Add a band selection listener
     * 
     * @param listener
     *          The listener to be added
     */
    public void addListener(BandSelectionListener listener) {
        addListener(BandSelectionEvent.class, listener, BAND_SELECTION_METHOD);
    }

    /**
     * Remove a date range listener
     * 
     * @param listener
     *          The listener to be removed
     */
    public void removeListener(DateRangeListener listener) {
        removeListener(DateRangeChangedEvent.class, listener);
        dateRangeListenerCounter--;
        if (dateRangeListenerCounter == 0) {
            sendChangeEventAvailable = false;
        }
    }

    /**
     * Remove a event button click listener
     * 
     * @param listener
     *          The listener to be removed
     */
    public void removeListener(EventClickListener listener) {
        removeListener(EventButtonClickEvent.class, listener);
    }

    /**
     * Remove a page navigation listener
     * 
     * @param listener
     *          The listener to be removed
     */
    public void removeListener(PageNavigationListener listener) {
        removeListener(PageNavigationEvent.class, listener);
    }

    /**
     * Remove a band selection listener
     * 
     * @param listener
     *          The listener to be removed
     */
    public void removeListener(BandSelectionListener listener) {
        removeListener(BandSelectionEvent.class, listener);
    }

    /**
     * Fires a date range changed event
     * 
     * @param start
     *          The start date of the range
     * @param end
     *          The end date of the range
     */
    protected void fireDateRangeChangedEvent(Date start, Date end) {
        fireEvent(new EventTimeline.DateRangeChangedEvent(this, start, end));
    }

    /**
     * Fires a event button click event which occurs when the user presses an event button in the
     * graph
     * 
     * @param itemId
     *          The item id in the Event container which represents the event
     */
    protected void fireEventButtonClickEvent(String itemId) {
        fireEvent(new EventTimeline.EventButtonClickEvent(this, itemId));
    }

    /**
     * Fires a band paging event if the user clicks on next / previous band.
     * 
     * @param page
     *          The visible page of the band area.
     */
    protected void fireBandPagingEvent(int page) {
        fireEvent(new EventTimeline.PageNavigationEvent(this, page));
    }

    /**
     * Fires a band selection event if the user selected a band.
     * 
     * @param page
     *          The id of the selected band.
     */
    protected void fireBandSelectionEvent(int bandId) {
        fireEvent(new EventTimeline.BandSelectionEvent(this, bandId));
    }

    /**
     * Shows or hides the browser.<br/>
     * The browser is the timeline browser in the bottom of the component. With the browser you can
     * move or zoom in time.
     * 
     * @param visible
     *          If true then the browser is visible
     */
    public void setBrowserVisible(boolean visible) {
        browserIsVisible = visible;
        sendComponentVisibilities = true;
        if (initDone) {
            requestRepaint();
        }
    }

    /**
     * Is the browser visible
     */
    public boolean isBrowserVisible() {
        return browserIsVisible;
    }

    /**
     * Show the zoom levels.<br/>
     * Zoom levels are predefined time ranges which are displayed in the top left corner of the
     * Timeline component.
     * 
     * @param visible
     *          If true then the zoom levels are visible
     */
    public void setZoomLevelsVisible(boolean visible) {
        zoomIsVisible = visible;
        sendComponentVisibilities = true;
        if (initDone) {
            requestRepaint();
        }
    }

    /**
     * Are the zoom levels visible.<br/>
     * Zoom levels are predefined time ranges which are displayed in the top left corner of the
     * Timeline component.
     */
    public boolean isZoomLevelsVisible() {
        return zoomIsVisible;
    }

    /**
     * Sets the caption of the zoom levels
     * 
     * @param caption
     * 
     */
    public void setZoomLevelsCaption(String caption) {
        if (caption == null) {
            caption = "";
        }

        this.zoomLevelCaption = caption;
        sendUICaptions = true;
        if (initDone) {
            requestRepaint();
        }
    }

    /**
     * Get the caption of the zoom levels
     * 
     * @return
     */
    public String getZoomLevelsCaption() {
        return this.zoomLevelCaption;
    }

    /**
     * Sets the band height. A value of -1 indicates that the bands shall use automatic resizing to
     * available space.
     * 
     * @param height
     *          the band heigth to be used
     * 
     */
    public void setBandHeight(int height) {
        this.bandHeight = height;
        sendBandHeight = true;
        if (initDone) {
            requestRepaint();
        }
    }

    /**
     * Get the configured band height
     * 
     * @return the band height
     */
    public int getBandHeight() {
        return this.bandHeight;
    }

    /**
     * Returns the captions of the band paging element.
     * 
     * @return the pagingCaption
     */
    public PageNavigationCaptions getPageNavigationCaptions() {
        return pagingCaption;
    }

    /**
     * Sets the pagingCaptions of the band paging element.
     * 
     * @param pagingCaption
     *          the pagingCaption to set
     */
    public void setPageNavigationCaptions(PageNavigationCaptions pagingCaption) {
        if (pagingCaption == null) {
            pagingCaption = new PageNavigationCaptions("", "", "");
        }

        this.pagingCaption = pagingCaption;
        sendUICaptions = true;
        if (initDone) {
            requestRepaint();
        }
    }

    /**
     * Returns a collection with informations about each added band.
     * 
     * @return the bandInfos
     */
    public List<BandInfo> getBandInfos() {
        return Collections.unmodifiableList(new ArrayList<BandInfo>(bandInfos));
    }

    /**
     * Returns the band info for the given bandId or <code>null</code> if no band could be found.
     * 
     * @param bandId
     * @return
     */
    public BandInfo getBand(int bandId) {
        for (BandInfo info : bandInfos) {
            if (info.getBandId() == bandId) {
                return info;
            }
        }
        return null;
    }

    /**
     * Show the date select.<br/>
     * The date select is the text fields in the top right corner of the component which shows the
     * currently selected date range.
     * 
     * @param visible
     *          Should the data select be visible
     */
    public void setDateSelectVisible(boolean visible) {
        dateSelectVisible = visible;
        sendComponentVisibilities = true;
        if (initDone) {
            requestRepaint();
        }
    }

    /**
     * Is the date select visible.<br/>
     * The date select is the text fields in the top right corner of the component which shows the
     * currently selected date range.
     */
    public boolean isDateSelectVisible() {
        return dateSelectVisible;
    }

    /**
     * Enable manual editing of selected date range.<br/>
     * The date select is the text fields in the top right corner of the component which shows the
     * currently selected date range.<br/>
     * The date range can be used to either just display the currently selected date range or one can
     * allow the used to manually edit the selected date range by clicking on the dates by setting
     * enabled to true.
     * 
     * @param enabled
     *          Is the date selected manually editable
     */
    public void setDateSelectEnabled(boolean enabled) {
        dateSelectEnabled = enabled;
        sendComponentVisibilities = true;
        if (initDone) {
            requestRepaint();
        }
    }

    /**
     * Shows the page navigation.<br/>
     * Using the page navigation allows to observe the requested band page.
     * 
     * @param visible
     */
    public void setPageNavigationVisible(boolean visible) {
        pageNavigationVisible = visible;
        sendComponentVisibilities = true;
        if (initDone) {
            requestRepaint();
        }
    }

    /**
     * Returns true, if the page navigation is visible.
     * 
     * @return the pageNavigationVisible
     */
    public boolean isPageNavigationVisible() {
        return pageNavigationVisible;
    }

    /**
     * Returns true, if the band selection is enabled. See {@link #setBandSelectionEnabled(boolean)}
     * for more details.
     * 
     * @return
     */
    public boolean isBandSelectionEnabled() {
        return bandSelectionEnabled;
    }

    /**
     * If the band selection is enabled the selected band will be marked at the UI. Additionally band
     * selection events are sent, if the selected band changes.
     * 
     * @param bandSelectionEnabled
     */
    public void setBandSelectionEnabled(boolean bandSelectionEnabled) {
        this.bandSelectionEnabled = bandSelectionEnabled;
        sendComponentVisibilities = true;
        if (initDone) {
            requestRepaint();
        }
    }

    /**
     * Returns the numbers of event bands shown in the event bands area. A value lower equal 0 means
     * that an unlimited number of event bands can be shown simultaneously at the band area.
     * 
     * @return the eventBandPageSize
     */
    public int getEventBandPageSize() {
        return eventBandPageSize;
    }

    /**
     * Specifies the numbers of event bands shown in the event bands area.<br/>
     * Setting a value lower equal 0 means that an unlimited number of event bands can be shown
     * simultaneously at the band area.
     * 
     * @param eventBandPageSize
     *          the eventBandPageSize to set
     */
    public void setEventBandPageSize(int eventBandPageSize) {
        this.eventBandPageSize = eventBandPageSize;
        sendEventBandPageSize = true;
        if (initDone) {
            requestRepaint();
        }
    }

    /**
     * Is the date select enabled.<br/>
     * The date select is the text fields in the top right corner of the component which shows the
     * currently selected date range.<br/>
     * The date range can be used to either just display the currently selected date range or one can
     * allow the used to manually edit the selected date range by clicking on the dates by setting
     * enabled to true.
     */
    public boolean isDateSelectEnabled() {
        return dateSelectEnabled;
    }

    /**
     * Set legend visibility.<br/>
     * The legend displays labels for each graph
     * 
     * @param visible
     *          The legend visibility
     */
    public void setGraphLegendVisible(boolean visible) {
        legendVisible = visible;
        sendComponentVisibilities = true;
        if (initDone) {
            requestRepaint();
        }
    }

    /**
     * Is the legend visible
     */
    public boolean isGraphLegendVisible() {
        return legendVisible;
    }

    /**
     * Add a zoom level.<br/>
     * Adds a custom zoom level. Zoom levels are defined as milliseconds and are added to the top left
     * of the Timeline component.
     * 
     * @param caption
     *          The title of the zoom level
     * @param time
     *          The timespan of the zoom level
     */
    public void addZoomLevel(String caption, Long time) {
        zoomLevels.put(caption, time);
        sendZoomLevels = true;
        if (initDone) {
            requestRepaint();
        }
    }

    /**
     * Remove a zoom level.<br/>
     * Zoom levels are defined as milliseconds and are added to the top left of the Timeline
     * component.
     * 
     * @param caption
     *          The title of the zoom level
     */
    public void removeZoomLevel(String caption) {
        zoomLevels.remove(caption);
        sendZoomLevels = true;
        if (initDone) {
            requestRepaint();
        }
    }

    /**
     * Sets the color of the grid.<br/>
     * Setting the color to NULL will remove the grid.
     * 
     * @param color
     *          The color (as CSS3-style rgba string) of the grid or Null to remove the grid
     */
    public void setGridColor(String color) {
        gridColor = color;
        sendGridColor = true;
        if (initDone) {
            requestRepaint();
        }
    }

    /**
     * Gets the grid color used to draw the vertical and horizontal scale grids.<br/>
     * Returns null if no grid is drawn in the graph
     * 
     * @return The color of the graph
     */
    public String getGridColor() {
        return gridColor;
    }

    /**
     * When using dynamically updating graphs the updates may cause the selection bar to move when new
     * items are added to the data source and the graph changes. To lock the browser bar so it stays
     * still and instead the selection changes when new items are added set this to false. By default
     * the selection bar is locked to the selection. <br/>
     * Please note that when the selection lock is unlocked then the selection range will change with
     * each update.
     * 
     * @param locked
     *          Should the selection range be locked to the selected range or should the selection
     *          change when new items are added to the data source
     */
    public void setBrowserSelectionLock(boolean locked) {
        selectionBarLocked = locked;
        if (initDone) {
            requestRepaint();
        }
    }

    /**
     * Returns a map with all events grouped by the bandId.
     * 
     * @param start
     * @param end
     * @return
     */
    private Map<Integer, List<TimelineEvent>> getAllEventsMap(Date start, Date end) {
        Map<Integer, List<TimelineEvent>> events = new HashMap<Integer, List<TimelineEvent>>();

        for (BandInfo info : bandInfos) {
            TimelineEventProvider provider = info.getProvider();
            events.put(info.getBandId(), provider.getEvents(start, end));
        }

        return events;
    }

    private void initDataFlags() {
        initDone = true;
        sendTimeLimits = true;
        sendDateRange = true;
        sendComponentVisibilities = true;
        sendGridColor = true;
        sendZoomLevels = true;
        sendUICaptions = true;
        sendBands = true;
        sendBandHeight = true;
        sendEventBandPageSize = true;
    }

    /**
     * Gets an object describing the date formats.
     * 
     * To use your own date formats retrieve the formats using this method and use the setters to
     * customize the date formats.
     * 
     * @return
     */
    public DateFormatInfo getDateFormats() {
        return this.dateFormatInfo;
    }

    /**
     * A helper class to paint band commands.
     */
    private static class BandsPainter {

        /**
         * Start tag {@link VEventTimelineWidget#ATTR_BANDS}
         * 
         * @param target
         * @throws PaintException
         */
        public static void start(PaintTarget target) throws PaintException {
            target.startTag(VEventTimelineWidget.ATTR_BANDS);
        }

        /**
         * End tag {@link VEventTimelineWidget#ATTR_BANDS}
         * 
         * @param target
         * @throws PaintException
         */
        public static void end(PaintTarget target) throws PaintException {
            target.endTag(VEventTimelineWidget.ATTR_BANDS);
        }

        /**
         * Paints the add band command.
         * 
         * @param target
         * @param bandId
         * @param caption
         * @throws PaintException
         */
        public static void paintAdd(PaintTarget target, int bandId, String caption) throws PaintException {
            target.startTag(VEventTimelineWidget.ATTR_BAND);
            target.addAttribute(VEventTimelineWidget.ATTR_BANDID, bandId);
            target.addAttribute(VEventTimelineWidget.ATTR_OPERATION, VEventTimelineWidget.OPERATION_ADD);
            target.addAttribute(VEventTimelineWidget.ATTR_BAND_CAPTION, caption);
            target.endTag(VEventTimelineWidget.ATTR_BAND);
        }

        /**
         * Paints the remove band command.
         * 
         * @param target
         * @param bandId
         * @throws PaintException
         */
        public static void paintRemove(PaintTarget target, int bandId) throws PaintException {
            target.startTag(VEventTimelineWidget.ATTR_BAND);
            target.addAttribute(VEventTimelineWidget.ATTR_BANDID, bandId);
            target.addAttribute(VEventTimelineWidget.ATTR_OPERATION, VEventTimelineWidget.OPERATION_REMOVE);
            target.endTag(VEventTimelineWidget.ATTR_BAND);
        }

    }

    /**
     * Can be used to specify the captions of the band paging navigation element.
     */
    public static class PageNavigationCaptions implements Serializable {

        private static final long serialVersionUID = 7731112570587996160L;

        private final String caption;
        private final String caption_next;
        private final String caption_previous;

        /**
         * 
         * @param caption
         *          main caption
         * @param caption_next
         *          caption of the next button
         * @param caption_previous
         *          caption of the previous button
         */
        public PageNavigationCaptions(String caption, String caption_next, String caption_previous) {
            this.caption = caption;
            this.caption_next = caption_next;
            this.caption_previous = caption_previous;
        }

        /**
         * Main caption.
         * 
         * @return the caption
         */
        public String getCaption() {
            return caption;
        }

        /**
         * Caption of the next button.
         * 
         * @return the caption_next
         */
        public String getCaption_next() {
            return caption_next;
        }

        /**
         * Caption of the previous button.
         * 
         * @return the caption_previous
         */
        public String getCaption_previous() {
            return caption_previous;
        }

    }

    /**
     * Class providing band information.
     */
    public static class BandInfo implements Serializable {

        private static final long serialVersionUID = 6388755571992132307L;

        private final int bandId;
        private final TimelineEventProvider provider;
        private String caption;

        public BandInfo(int bandId, TimelineEventProvider provider, String caption) {
            this.bandId = bandId;
            this.provider = provider;
            this.caption = caption;
        }

        /**
         * @return the caption
         */
        public String getCaption() {
            return caption;
        }

        /**
         * @param caption the caption to set
         */
        protected void setCaption(String caption) {
            this.caption = caption;
        }

        /**
         * @return the bandId
         */
        public int getBandId() {
            return bandId;
        }

        /**
         * @return the provider
         */
        public TimelineEventProvider getProvider() {
            return provider;
        }

    }
}