org.rdv.ui.TimeSlider.java Source code

Java tutorial

Introduction

Here is the source code for org.rdv.ui.TimeSlider.java

Source

/*
 * RDV
 * Real-time Data Viewer
 * http://rdv.googlecode.com/
 * 
 * Copyright (c) 2005-2007 University at Buffalo
 * Copyright (c) 2005-2007 NEES Cyberinfrastructure Center
 * Copyright (c) 2008 Palta Software
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 * 
 * $URL$
 * $Revision$
 * $Date$
 * $Author$
 */

package org.rdv.ui;

import java.awt.Color;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Insets;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.util.ArrayList;
import java.util.List;

import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JToolTip;
import javax.swing.SwingUtilities;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.rdv.DataViewer;
import org.rdv.rbnb.EventMarker;
import org.rdv.rbnb.TimeRange;

/**
 * A component that lets the user select a specific time or time range between a
 * bounded time interval by sliding knobs along a timeline. It also allows one
 * to display an event indicator at a specific time in the time line.
 *  
 * @author Jason P. Hanley
 */
public class TimeSlider extends JComponent implements MouseListener, MouseMotionListener {

    /** serialization version identifier */
    private static final long serialVersionUID = 2429052022677466231L;

    /** The logger for this class */
    protected static final Log log = LogFactory.getLog(TimeSlider.class.getName());

    /** The minimum time. */
    protected double minimum;

    /** The start of the selected time range. */
    protected double start;

    /** The selected time. */
    protected double value;

    /** The end of the selected time range. */
    protected double end;

    /** The maximum time. */
    protected double maximum;

    /** Indicate if the time value may be changed from the UI. */
    private boolean valueChangeable;

    /** Indicates if a time range may be changed from the UI. */
    private boolean rangeChangeable;

    /** Indicates if the time value is adjusting. */
    private boolean isAdjusting;

    /** The starting offset of a click (for buttons). */
    private int clickStart;

    /** List of event markers. */
    protected final List<EventMarker> markers;

    /** List of valid time ranges. */
    protected final List<TimeRange> timeRanges;

    /** List of valid times ranges, taking into account the minimum and maximum */
    protected final List<TimeRange> actualTimeRanges;

    /** List of time adjustment listeners. */
    private final List<TimeAdjustmentListener> adjustmentListeners;

    /** The button used to indicate the start time. */
    protected final JButton startButton;

    /** The button used to indicate the time. */
    protected final JButton valueButton;

    /** The button used to indicate the end time. */
    protected final JButton endButton;

    /** The default image used to show a marker. */
    protected final Image defaultMarkerImage;

    /** The image used to show an annotation. */
    protected final Image annotationMarkerImage;

    /** The image used to show a start marker. */
    protected final Image startMarkerImage;

    /** The image used to show a stop marker. */
    protected final Image stopMarkerImage;

    /**
     * Creates a time slider with the maximum available range.
     */
    public TimeSlider() {
        super();

        minimum = 0;
        start = 0;
        value = 0;
        end = Double.MAX_VALUE;
        maximum = Double.MAX_VALUE;

        valueChangeable = true;
        rangeChangeable = true;

        isAdjusting = false;

        markers = new ArrayList<EventMarker>();

        timeRanges = new ArrayList<TimeRange>();

        actualTimeRanges = new ArrayList<TimeRange>();
        calculateActualTimeRanges();

        adjustmentListeners = new ArrayList<TimeAdjustmentListener>();

        setBorder(null);
        setLayout(null);

        setToolTipText("");

        addMouseListener(this);

        valueButton = new JButton(DataViewer.getIcon("icons/time.gif")) {
            /** serialization version identifier */
            private static final long serialVersionUID = 8729851598067678522L;

            public JToolTip createToolTip() {
                return TimeSlider.this.createToolTip();
            }

            public String getToolTipText(MouseEvent me) {
                return TimeSlider.this.getToolTipText(me);
            }
        };
        valueButton.setToolTipText("");
        valueButton.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
        valueButton.setOpaque(false);
        valueButton.setBorder(null);
        valueButton.addMouseMotionListener(this);
        valueButton.addMouseListener(this);
        add(valueButton);

        startButton = new JButton(DataViewer.getIcon("icons/left_bound.gif"));
        startButton.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
        startButton.setOpaque(false);
        startButton.setBorder(null);
        startButton.addMouseListener(this);
        startButton.addMouseMotionListener(this);
        add(startButton);

        endButton = new JButton(DataViewer.getIcon("icons/right_bound.gif"));
        endButton.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
        endButton.setOpaque(false);
        endButton.setBorder(null);
        endButton.addMouseListener(this);
        endButton.addMouseMotionListener(this);
        add(endButton);

        defaultMarkerImage = DataViewer.getImage("icons/marker.gif");
        annotationMarkerImage = DataViewer.getImage("icons/marker-annotation.gif");
        startMarkerImage = DataViewer.getImage("icons/marker-start.gif");
        stopMarkerImage = DataViewer.getImage("icons/marker-stop.gif");
    }

    /**
     * Sets if the time value of the slider can be changed by the UI element.
     * 
     * @param changeable  if true, the value can be changed, false otherwise.
     */
    public void setValueChangeable(boolean changeable) {
        if (valueChangeable == changeable) {
            return;
        }

        valueChangeable = changeable;

        valueButton.setEnabled(valueChangeable);

        if (valueChangeable) {
            addMouseListener(this);
            valueButton.addMouseListener(this);
            valueButton.addMouseMotionListener(this);
        } else {
            removeMouseListener(this);
            valueButton.removeMouseListener(this);
            valueButton.removeMouseMotionListener(this);
        }
    }

    /**
     * Set if the value is visible in the slider UI.
     * 
     * @param visible  if true the value is shown, if false the value is not shown
     */
    public void setValueVisible(boolean visible) {
        if (valueButton.isVisible() == visible) {
            return;
        }

        valueButton.setVisible(visible);
    }

    /**
     * Set if a time range can be changed via the UI elements
     * 
     * @param changeable  if true, the time range can be changed, false otherwsie
     */
    public void setRangeChangeable(boolean changeable) {
        if (rangeChangeable == changeable) {
            return;
        }

        rangeChangeable = changeable;

        startButton.setEnabled(rangeChangeable);
        endButton.setEnabled(rangeChangeable);

        if (rangeChangeable) {
            startButton.addMouseMotionListener(this);
            endButton.addMouseMotionListener(this);
        } else {
            startButton.removeMouseMotionListener(this);
            endButton.removeMouseMotionListener(this);
        }
    }

    /**
     * Gets the time value set by the slider.
     * 
     * @return  the time set by the slider
     */
    public double getValue() {
        return value;
    }

    /**
     * Sets the time value of the slider.
     * 
     * @param value  the time value to set
     */
    public void setValue(double value) {
        if (this.value != value) {
            this.value = value;
            doLayout();
            fireValueChanged();
        }
    }

    /**
     * Sets the maximum and minimum values.
     * 
     * @param minimum  the minimum allowed time value
     * @param maximum  the maximum allowed time value
     */
    public void setValues(double minimum, double maximum) {
        setMinimum(minimum);
        setMaximum(maximum);
    }

    /**
     * Sets the time value, and the maximum and minimum time values.
     * 
     * @param value    the time value
     * @param minimum  the minimum allowed time value
     * @param maximum  the maximum allowed time value
     */
    public void setValues(double value, double minimum, double maximum) {
        setMinimum(minimum);
        setMaximum(maximum);
        setValue(value);
    }

    /**
     * Sets the maximum and minimum time value, and the time range.
     *  
     * @param minimum  the minimum allowed time value
     * @param maximum  the maximum allowed time value
     * @param start    the start of the time range
     * @param end      the end of the time range
     */
    public void setValues(double minimum, double maximum, double start, double end) {
        setStart(start);
        setEnd(end);
        setMinimum(minimum);
        setMaximum(maximum);
    }

    /**
     * Sets the maximum and minimum time value, the time range, and the time
     * value.
     *  
     * @param value    the time value
     * @param minimum  the minimum allowed time value
     * @param maximum  the maximum allowed time value
     * @param start    the start of the time range
     * @param end      the end of the time range
     */
    public void setValues(double value, double minimum, double maximum, double start, double end) {
        setStart(start);
        setEnd(end);
        setMinimum(minimum);
        setMaximum(maximum);
        setValue(value);
    }

    /**
     * Gets the start of the selected time range.
     * 
     * @return  the start of the time range
     */
    public double getStart() {
        return start;
    }

    /**
     * Sets the start of the time range.
     * 
     * @param start  the start of the time range
     */
    public void setStart(double start) {
        if (this.start != start) {
            this.start = start;
            doLayout();
            fireRangeChanged();
        }
    }

    /**
     * Gets the end of the selected time range.
     * 
     * @return  the end of the time range
     */
    public double getEnd() {
        return end;
    }

    /**
     * Sets the end of the time range.
     * 
     * @param end  the end of the time range
     */
    public void setEnd(double end) {
        if (this.end != end) {
            this.end = end;
            doLayout();
            fireRangeChanged();
        }
    }

    /**
     * Gets the minimum allowed time value.
     * 
     * @return  the minimum allowed time value
     */
    public double getMinimum() {
        return minimum;
    }

    /**
     * Sets the minimum allowed time value. If this is greater than the selected
     * time range, it will be set to be equal to this value.
     * 
     * @param minimum  the minimum allowed time value
     */
    public void setMinimum(double minimum) {
        if (this.minimum != minimum) {
            this.minimum = minimum;

            calculateActualTimeRanges();

            if (rangeChangeable) {
                if (minimum > start) {
                    start = minimum;
                    fireRangeChanged();
                }
                if (minimum > end) {
                    end = minimum;
                    fireRangeChanged();
                }
            } else {
                start = minimum;
            }

            fireBoundsChanged();
            doLayout();
        }
    }

    /**
     * Gets the maximum allowed time value.
     * 
     * @return  the maximum allowed time value.
     */
    public double getMaximum() {
        return maximum;
    }

    /**
     * Sets the maximum allowed time value. If this is less than the selected time
     * range, it will be set to be equal to this value.
     * 
     * @param maximum  the maximym allowed time value
     */
    public void setMaximum(double maximum) {
        if (this.maximum != maximum) {
            this.maximum = maximum;

            calculateActualTimeRanges();

            if (rangeChangeable) {
                if (maximum < end) {
                    end = maximum;
                    fireRangeChanged();
                }
                if (maximum < start) {
                    start = maximum;
                    fireRangeChanged();
                }
            } else {
                end = maximum;
            }

            fireBoundsChanged();
            doLayout();
        }
    }

    /**
     * Add a marker to the slider.
     * 
     * @param marker  the marker
     */
    public void addMarker(final EventMarker marker) {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                markers.add(marker);
                repaint();
            }
        });
    }

    /**
     * Get the event marker that is closest to the given time.
     * 
     * @param time  the time to search around
     * @return      the event marker closest to the time, or null if there are
     *              none
     */
    private EventMarker getMarkerClosestToTime(double time) {
        EventMarker eventMarker = null;
        double distance = Double.MAX_VALUE;

        for (EventMarker marker : markers) {
            double markerTime = Double.parseDouble(marker.getProperty("timestamp"));
            double thisDistance = Math.abs(time - markerTime);
            if (eventMarker == null || thisDistance < distance) {
                eventMarker = marker;
                distance = thisDistance;
            }
        }

        return eventMarker;
    }

    /**
     * Get markers around the specified time. This will look before and after
     * the time according to the offset.
     * 
     * @param time    the time to look for markers at
     * @param offset  the offset from the time to look
     * @return        a list of markers within this time range
     */
    private List<EventMarker> getMarkersAroundTime(double time, double offset) {
        double lowerBound = time - offset;
        double upperBound = time + offset;

        List<EventMarker> markersAround = new ArrayList<EventMarker>();

        for (EventMarker marker : markers) {
            double markerTime = Double.parseDouble(marker.getProperty("timestamp"));
            if (markerTime >= lowerBound && markerTime <= upperBound) {
                markersAround.add(marker);
            }
        }

        return markersAround;
    }

    /**
     * Remove all marker.
     */
    public void cleareMarkers() {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                markers.clear();
                repaint();
            }
        });
    }

    /**
     * Replace the current set of time ranges with this one.
     * 
     * @param newTimeRanges  a list of time ranges
     */
    public void setTimeRanges(List<TimeRange> newTimeRanges) {
        timeRanges.clear();
        timeRanges.addAll(newTimeRanges);

        calculateActualTimeRanges();

        double newValue = getClosestValidTime(value);
        if (value != newValue) {
            value = newValue;
            fireValueChanged();
        }

        double newStart = getClosestValidTime(start);
        double newEnd = getClosestValidTime(end);
        if (start != newStart || end != newEnd) {
            start = newStart;
            end = newEnd;
            fireRangeChanged();
        }

        doLayout();
    }

    /**
     * Tell if the time is valid with respect to the current time bounds and
     * ranges.
     * 
     * @param time  the time to check
     * @return      true if the time is valid, false if it is not
     */
    public boolean isTimeValid(double time) {
        for (TimeRange timeRange : actualTimeRanges) {
            if (timeRange.contains(time)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Get a time that is valid and greater than or equal to the current one.
     * 
     * @return  the next valid time, or -1 if there is none
     */
    public double getNextValidTime(double time) {
        for (TimeRange tr : actualTimeRanges) {
            if (tr.contains(time)) {
                return time;
            } else if (tr.start > time) {
                return tr.start;
            }
        }

        return -1;
    }

    /**
     * Return a time that is valid and the closest possible to the given time. If
     * the provided time is valid, then the same time will be returned. If the
     * time is invalid, the nearest valid time will be returned.
     * 
     * @param time  the time to use
     * @return      the closest valid time to the given time
     */
    private double getClosestValidTime(double time) {
        double possibleTime = 0;

        for (int i = 0; i < actualTimeRanges.size(); i++) {
            TimeRange tr = actualTimeRanges.get(i);

            if (i == 0 && time < tr.start) {
                return tr.start;
            } else if (i == actualTimeRanges.size() - 1 && time > tr.end) {
                return tr.end;
            } else if (tr.contains(time)) {
                return time;
            } else if (time > tr.end) {
                possibleTime = tr.end;
            } else if (time - possibleTime < tr.start - time) {
                return possibleTime;
            } else {
                return tr.start;
            }
        }

        // this shouldn't be possible
        return time;
    }

    /**
     * Remove all time ranges.
     */
    public void clearTimeRanges() {
        timeRanges.clear();
        calculateActualTimeRanges();
        doLayout();
    }

    /**
     * Indicates if the value is being continuously changed in the UI.
     * 
     * @return  true if the value is changing, false otherwise
     */
    public boolean isValueAdjusting() {
        return isAdjusting;
    }

    /**
     * Updates the list of time ranges represented by the time slider. This
     * enforces the maximum and minimum values on the list of time ranges.
     * 
     * @see #actualTimeRanges
     */
    private void calculateActualTimeRanges() {
        actualTimeRanges.clear();

        if (timeRanges.size() == 0) {
            actualTimeRanges.add(new TimeRange(minimum, maximum));
        } else {
            for (TimeRange t : timeRanges) {
                if (t.start > maximum) {
                    break;
                }

                if (t.end < minimum) {
                    continue;
                }

                double rangeStart;
                if (t.start >= minimum) {
                    rangeStart = t.start;
                } else {
                    rangeStart = minimum;
                }

                double rangeEnd;
                if (t.end <= maximum) {
                    rangeEnd = t.end;
                } else {
                    rangeEnd = maximum;
                }

                actualTimeRanges.add(new TimeRange(rangeStart, rangeEnd));
            }
        }
    }

    /**
     * Get the minimum value allowed on the time slider. This takes into account
     * the time ranges.
     * 
     * @return  the minimum value allowed on the time slider
     */
    public double getActualMinimum() {
        if (actualTimeRanges.size() == 0) {
            return minimum;
        } else {
            return actualTimeRanges.get(0).start;
        }
    }

    /**
     * Get the maximum value allowed on the time slider. This takes into account
     * the time ranges.
     * 
     * @return  the maximum value allowed on the time slider
     */
    public double getActualMaximum() {
        if (actualTimeRanges.size() == 0) {
            return maximum;
        } else {
            return actualTimeRanges.get(actualTimeRanges.size() - 1).end;
        }
    }

    /**
     * Get the total length of time represented by the time slider. This removes
     * time contained in the gaps from time ranges.
     * 
     * @return  the total length of time represented by the time slider
     */
    private double getTimeLength() {
        double length = 0;

        for (TimeRange t : actualTimeRanges) {
            length += t.length();
        }

        return length;
    }

    /**
     * Get the width (in pixels) of the time portion of the slider.
     * 
     * @return  the width of the time portion of the slider
     */
    private int getTimeWidth() {
        Insets insets = getInsets();
        return getWidth() - 13 - insets.left - insets.right;
    }

    /**
     * Return the time corresponding to the x coordinate of the time slider.
     * 
     * @param x  the horizontal point on the slider component
     * @return   the time
     */
    private double getTimeFromX(int x) {
        // remove left inset and button width
        x = x - 6 - getInsets().left;

        int width = getTimeWidth();

        if (x < 0) {
            return getActualMinimum();
        } else if (x > width) {
            return getActualMaximum();
        }

        double factor = ((double) (x)) / width;
        double length = getTimeLength();
        double value = factor * length;

        double position = 0;
        for (TimeRange t : actualTimeRanges) {
            double startPosition = position;
            position += t.length();

            if (value <= position) {
                double startFactor = startPosition / length;
                double endFactor = position / length;
                factor = (factor - startFactor) / (endFactor - startFactor);
                return (t.length()) * factor + t.start;
            }
        }

        return 0;
    }

    /**
     * Get the horizontal point on the slider component corresponding to the given
     * time. If the time is not visible on the slider, -1 will be returned.
     * 
     * @param time  the time to get the point from
     * @return      the horizontal (x) point of the time, or -1 if the point is
     *              not visible
     */
    private int getXFromTime(double time) {
        boolean timeVisible = false;
        double position = 0;
        for (TimeRange t : actualTimeRanges) {
            if (t.contains(time)) {
                timeVisible = true;
                position += (time - t.start);
                break;
            }
            position += t.length();
        }

        if (!timeVisible) {
            return -1;
        }

        double factor = position / getTimeLength();
        int width = getTimeWidth();

        return (int) Math.round(factor * width) + getInsets().left + 6;
    }

    /**
     * Gets the amount of time one pixel on the time slider represends.
     * 
     * @return  the amount of time represented by one pixel, in seconds
     */
    private double getPixelTime() {
        return getTimeLength() / getTimeWidth();
    }

    /**
     * Layout the buttons.
     */
    public void doLayout() {
        Insets insets = getInsets();

        if (maximum == minimum) {
            startButton.setVisible(false);
            endButton.setVisible(false);
        } else {
            startButton.setVisible(true);
            endButton.setVisible(true);
        }

        startButton.setBounds(getXFromTime(start) - 6, insets.top, 6, 11);
        endButton.setBounds(getXFromTime(end) + 1, insets.top, 6, 11);

        valueButton.setBounds(getXFromTime(value) - 3, insets.top + 2, 7, 7);

        repaint();
    }

    /**
     * Paint the components. Also paint the slider and the markers.
     */
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);

        Insets insets = getInsets();

        g.setColor(Color.lightGray);
        g.fillRect(insets.left + 6, insets.top + 4, getWidth() - insets.left - 12 - insets.right, 3);

        if (isEnabled()) {
            g.setColor(Color.gray);
            int startX = getXFromTime(start);
            int endX = getXFromTime(end);
            g.fillRect(startX, insets.top + 4, endX - startX, 3);
        }

        for (EventMarker marker : markers) {
            double markerTime = Double.parseDouble(marker.getProperty("timestamp"));
            if (markerTime >= minimum && markerTime <= maximum) {
                int x = getXFromTime(markerTime);
                if (x == -1) {
                    continue;
                }

                Image markerImage;

                String markerType = marker.getProperty("type");
                if (markerType.compareToIgnoreCase("annotation") == 0) {
                    markerImage = annotationMarkerImage;
                } else if (markerType.compareToIgnoreCase("start") == 0) {
                    markerImage = startMarkerImage;
                } else if (markerType.compareToIgnoreCase("stop") == 0) {
                    markerImage = stopMarkerImage;
                } else {
                    markerImage = defaultMarkerImage;
                }

                g.drawImage(markerImage, x - 1, insets.top, null);
            }
        }
    }

    /**
     * Get the minimum dimensions for this component.
     * 
     * @return  the minimum dimensions
     */
    public Dimension getMinimumSize() {
        Insets insets = getInsets();
        return new Dimension(insets.left + 6 + 6 + 1 + insets.right, insets.top + 11 + insets.bottom);
    }

    /**
     * Get the preferred size for this component.
     * 
     * @return  the preferred dimensions
     */
    public Dimension getPreferredSize() {
        return getMinimumSize();
    }

    /**
     * Creates the tooltip for the component. This changes the default tooltip by
     * setting a different border.
     * 
     * @return  the tooltip created
     */
    public JToolTip createToolTip() {
        JToolTip toolTip = super.createToolTip();
        toolTip.setBackground(Color.decode("#FFFFFC"));
        toolTip.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createEtchedBorder(),
                BorderFactory.createEmptyBorder(5, 5, 5, 5)));
        return toolTip;
    }

    /**
     * Gets the text for the tooltip. This will return a description of the
     * markers the mouse pointer is around.
     * 
     * @param me  the mouse event that triggered this
     * @return    text describing the markers around the mouse pointer
     */
    public String getToolTipText(MouseEvent me) {
        int x = me.getX();
        if (me.getSource() == valueButton) {
            x += valueButton.getX();
        }

        double time = getTimeFromX(x);
        double timeOffset = time - value;

        double markerOffset = 2 * getPixelTime();
        List<EventMarker> markersOver = getMarkersAroundTime(time, markerOffset);

        String text = new String("<html><font size=\"5\">");

        text += DataViewer.formatDateSmart(time);
        if (timeOffset != 0) {
            text += " (" + (timeOffset < 0 ? "-" : "+") + DataViewer.formatSeconds(Math.abs(timeOffset)) + ")";
        }

        int numberOfMarkers = markersOver.size();
        if (numberOfMarkers > 0) {
            text += "<br>Found " + numberOfMarkers + " event" + (numberOfMarkers == 1 ? "" : "s") + " (&plusmn;"
                    + DataViewer.formatSeconds(markerOffset) + ")<br><br>";
        }

        for (EventMarker marker : markersOver) {
            String date = DataViewer.formatDate(Double.parseDouble(marker.getProperty("timestamp")));
            String source = marker.getProperty("source");
            String type = marker.getProperty("type");
            String label = marker.getProperty("label");
            String content = marker.getProperty("content");

            boolean showType = false;
            String dateColor;
            if (type == null) {
                dateColor = "black";
            } else if (type.compareToIgnoreCase("annotation") == 0) {
                dateColor = "blue";
            } else if (type.compareToIgnoreCase("start") == 0) {
                dateColor = "green";
            } else if (type.compareToIgnoreCase("stop") == 0) {
                dateColor = "red";
            } else {
                showType = true;
                dateColor = "#FF9900";
            }

            text += "<font color=" + dateColor + ">" + date + "</font> ";

            if (showType) {
                text += "<i>" + type + "</i> ";
            }

            if (source != null && source.length() > 0) {
                text += "by " + source + ": ";
            }

            if (label != null && label.length() > 0) {
                text += "<u>" + label + "</u> ";
            }

            if (content != null && content.length() > 0) {
                int maxLineLength = 75;
                int maxLines = 10;

                String[] words = content.split(" ");
                int lineLength = 0;
                int lines = 0;
                for (String word : words) {
                    if (lineLength + word.length() < (lines == 0 ? (maxLineLength - 32) : maxLineLength)) {
                        text += " " + word;
                        lineLength += word.length() + 1;
                    } else {
                        if (++lines == maxLines) {
                            text += "...";
                            break;
                        }
                        text += "<br>" + word;
                        lineLength = word.length();
                    }
                }
            }
            text += "<br>";
        }
        text += "</font></html>";

        return text;
    }

    /**
     * Called when the mouse is dragged. This deals dragging the time value and
     * range controls.
     * 
     * @param me  the mosue event that triggered this 
     */
    public void mouseDragged(MouseEvent me) {
        if (!isEnabled()) {
            return;
        }

        JButton button = (JButton) me.getSource();
        int x = me.getX();
        if (button == startButton) {
            x += endButton.getWidth() - clickStart;
        } else if (button == valueButton) {
            x += Math.round(valueButton.getWidth() / 2d) - clickStart;
        } else if (button == endButton) {
            x -= clickStart;
        }
        double time = getTimeFromX(button.getX() + x);

        if (button == startButton) {
            if (time < minimum) {
                time = minimum;
            } else if (time >= end) {
                time = end;
            }

            setStart(time);
        } else if (button == valueButton) {
            if (rangeChangeable) {
                if (time < start) {
                    time = start;
                } else if (time > end) {
                    time = end;
                }
            } else {
                if (time < minimum) {
                    time = minimum;
                } else if (time > maximum) {
                    time = maximum;
                }
            }

            setValue(time);
        } else if (button == endButton) {
            if (time < start) {
                time = start;
            } else if (time > maximum) {
                time = maximum;
            }

            setEnd(time);
        }
    }

    /**
     * Called when the mouse is pressed. Used to tell when the value control is
     * being adjusted.
     * 
     * @param me  the mouse event that triggered this
     */
    public void mousePressed(MouseEvent me) {
        clickStart = me.getX();

        if (me.getSource() == valueButton) {
            isAdjusting = true;
        }
    }

    /**
     * Called whn the mouse is released. Used to tell when the value control is
     * being adjusted.
     * 
     * @param me  the mouse event that triggered this
     */
    public void mouseReleased(MouseEvent me) {
        if (me.getSource() == valueButton) {
            isAdjusting = false;
        }
    }

    public void mouseMoved(MouseEvent me) {
    }

    /**
     * Called when the mouse is clicked. Used to set the time value when there is
     * a click on the time slider.
     * 
     * @param me  the mouse event that triggered this
     */
    public void mouseClicked(MouseEvent me) {
        double time;
        if (me.getSource() == this) {
            time = getTimeFromX(me.getX());
        } else if (me.getSource() == valueButton) {
            time = getTimeFromX(valueButton.getX() + me.getX());
        } else {
            return;
        }

        if (me.getButton() == MouseEvent.BUTTON1) {
            setValue(time);
        } else if (me.getButton() == MouseEvent.BUTTON3) {
            EventMarker eventMarker = getMarkerClosestToTime(time);
            if (eventMarker != null) {
                double markerTime = Double.parseDouble(eventMarker.getProperty("timestamp"));
                setValue(markerTime);
            }
        }
    }

    public void mouseEntered(MouseEvent me) {
    }

    public void mouseExited(MouseEvent me) {
    }

    /**
     * Add a listener for time adjustments.
     * 
     * @param l  the listener to add
     */
    public void addTimeAdjustmentListener(TimeAdjustmentListener l) {
        adjustmentListeners.add(l);
    }

    /**
     * Remove a listener for time adjustments.
     * 
     * @param l   the listener to remove
     */
    public void removeTimeAdjustmentListener(TimeAdjustmentListener l) {
        adjustmentListeners.remove(l);
    }

    /**
     * Fires a value changed event to all adjustment listeners.
     */
    protected void fireValueChanged() {
        TimeEvent event = new TimeEvent(this);
        for (TimeAdjustmentListener l : adjustmentListeners) {
            l.timeChanged(event);
        }
    }

    /**
     * Fires a range changed event to all adjustment listeners.
     */
    protected void fireRangeChanged() {
        TimeEvent event = new TimeEvent(this);
        for (TimeAdjustmentListener l : adjustmentListeners) {
            l.rangeChanged(event);
        }
    }

    /**
     * Fires a bounds changed event to all adjustment listeners.
     */
    protected void fireBoundsChanged() {
        TimeEvent event = new TimeEvent(this);
        for (TimeAdjustmentListener l : adjustmentListeners) {
            l.boundsChanged(event);
        }
    }

    /**
     * Sets if this component is enabled. If the component is not enabled, the
     * time value and range controls will not respond to user input.
     * 
     * @param enabled true if this component should be enabled, false otherwise
     */
    public void setEnabled(boolean enabled) {
        if (isEnabled() == enabled) {
            return;
        }

        super.setEnabled(enabled);

        valueButton.setEnabled(enabled && valueChangeable);

        startButton.setEnabled(enabled && rangeChangeable);
        endButton.setEnabled(enabled && rangeChangeable);

        repaint();
    }
}