org.mwc.cmap.TimeController.views.TimeController.java Source code

Java tutorial

Introduction

Here is the source code for org.mwc.cmap.TimeController.views.TimeController.java

Source

/*
 *    Debrief - the Open Source Maritime Analysis Application
 *    http://debrief.info
 *
 *    (C) 2000-2014, PlanetMayo Ltd
 *
 *    This library is free software; you can redistribute it and/or
 *    modify it under the terms of the Eclipse Public License v1.0
 *    (http://www.eclipse.org/legal/epl-v10.html)
 *
 *    This library is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 
 */
package org.mwc.cmap.TimeController.views;

import java.awt.event.ActionEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.TimeZone;
import java.util.TimerTask;
import java.util.Vector;

import junit.framework.TestCase;

import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.IContributionItem;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.action.IToolBarManager;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.action.Separator;
import org.eclipse.jface.dialogs.InputDialog;
import org.eclipse.jface.layout.GridDataFactory;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.ISelectionProvider;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Scale;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IFileEditorInput;
import org.eclipse.ui.IMemento;
import org.eclipse.ui.IViewSite;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.part.ViewPart;
import org.mwc.cmap.TimeController.TimeControllerPlugin;
import org.mwc.cmap.TimeController.controls.DTGBiSlider;
import org.mwc.cmap.TimeController.controls.DTGBiSlider.DoFineControl;
import org.mwc.cmap.TimeController.properties.FineTuneStepperProps;
import org.mwc.cmap.core.CorePlugin;
import org.mwc.cmap.core.DataTypes.Temporal.ControllablePeriod;
import org.mwc.cmap.core.DataTypes.Temporal.ControllableTime;
import org.mwc.cmap.core.DataTypes.Temporal.SteppableTime;
import org.mwc.cmap.core.DataTypes.Temporal.TimeControlPreferences;
import org.mwc.cmap.core.DataTypes.Temporal.TimeControlProperties;
import org.mwc.cmap.core.DataTypes.Temporal.TimeProvider;
import org.mwc.cmap.core.DataTypes.TrackData.TrackDataProvider;
import org.mwc.cmap.core.interfaces.TimeControllerOperation;
import org.mwc.cmap.core.interfaces.TimeControllerOperation.TimeControllerOperationStore;
import org.mwc.cmap.core.property_support.EditableWrapper;
import org.mwc.cmap.core.ui_support.PartMonitor;
import org.mwc.cmap.plotViewer.editors.CorePlotEditor;
import org.mwc.debrief.core.editors.PlotEditor;
import org.mwc.debrief.core.editors.painters.LayerPainterManager;
import org.mwc.debrief.core.editors.painters.TemporalLayerPainter;
import org.mwc.debrief.core.editors.painters.highlighters.SWTPlotHighlighter;

import MWC.Algorithms.PlainProjection;
import MWC.Algorithms.PlainProjection.RelativeProjectionParent;
import MWC.GUI.Layers;
import MWC.GUI.Properties.DateFormatPropertyEditor;
import MWC.GenericData.Duration;
import MWC.GenericData.HiResDate;
import MWC.GenericData.TimePeriod;
import MWC.GenericData.WatchableList;
import MWC.GenericData.WorldLocation;
import MWC.Utilities.Timer.TimerListener;

/**
 * View performing time management: show current time, allow control of time,
 * allow selection of time periods
 */

public class TimeController extends ViewPart
        implements ISelectionProvider, TimerListener, RelativeProjectionParent {
    private static final String ICON_BKMRK_NAV = "icons/bkmrk_nav.gif";

    private static final String ICON_FILTER_TO_PERIOD = "icons/16/filter.png";

    private static final String ICON_LOCK_VIEW2 = "icons/lock_view2.png";

    private static final String ICON_LOCK_VIEW1 = "icons/lock_view1.png";

    private static final String ICON_LOCK_VIEW = "icons/lock_view.png";

    private static final String ICON_PROPERTIES = "icons/16/properties.png";

    private static final String ICON_MEDIA_END = "icons/24/media_end.png";

    private static final String ICON_MEDIA_FAST_FORWARD = "icons/24/media_fast_forward.png";

    private static final String ICON_MEDIA_FORWARD = "icons/24/media_forward.png";

    private static final String ICON_MEDIA_BACK = "icons/24/media_back.png";

    private static final String ICON_MEDIA_REWIND = "icons/24/media_rewind.png";

    private static final String ICON_MEDIA_BEGINNING = "icons/24/media_beginning.png";

    private static final String ICON_MEDIA_PAUSE = "icons/24/media_pause.png";

    private static final String ICON_MEDIA_PLAY = "icons/24/media_play.png";

    private static final String DUFF_TIME_TEXT = "--------------------------";

    private static final String PAUSE_TEXT = "Pause automatically moving forward";

    private static final String PLAY_TEXT = "Start automatically moving forward";

    private static final String OP_LIST_MARKER_ID = "OPERATION_LIST_MARKER";

    private PartMonitor _myPartMonitor;

    /**
     * the automatic timer we are using
     */
    MWC.Utilities.Timer.Timer _myTimer;

    /**
     * the editor the user is currently working with (assigned alongside the
     * time-provider object)
     */
    protected transient IEditorPart _currentEditor;

    /**
     * listen out for new times
     */
    final PropertyChangeListener _temporalListener = new NewTimeListener();

    /**
     * the temporal dataset controlling the narrative entry currently displayed
     */
    TimeProvider _myTemporalDataset;

    /**
     * the "write" interface for the plot which tracks the narrative, where
     * avaialable
     */
    ControllableTime _controllableTime;

    /**
     * an object that gets stepped, not one that we can slide backwards & forwards
     * through
     * 
     */
    SteppableTime _steppableTime;

    /**
     * the "write" interface for indicating a selected time period
     */
    ControllablePeriod _controllablePeriod;

    /**
     * label showing the current time
     */
    Label _timeLabel;

    /**
     * the set of layers we control through the range selector
     */
    Layers _myLayers;

    /**
     * the parent object for the time controller. It is at this level that we
     * enable/disable the controls
     */
    Composite _wholePanel;

    /**
     * the holder for the VCR controls
     * 
     */
    private Composite _btnPanel;

    /**
     * the people listening to us
     */
    Vector<ISelectionChangedListener> _selectionListeners;

    /**
     * and the preferences for time control
     */
    TimeControlProperties _myStepperProperties;

    /**
     * module to look after the limits of the slider
     */
    SliderRangeManagement _slideManager = null;

    /**
     * when the user clicks on us, we set our properties as a selection. Remember
     * the set of properties
     */
    private StructuredSelection _propsAsSelection = null;

    /**
     * our fancy time range selector
     */
    DTGBiSlider _dtgRangeSlider;

    /**
     * whether the user wants to trim to time period after bislider change
     */
    Action _filterToSelectionAction;

    /**
     * the slider control - remember it because we're always changing the limits,
     * etc
     */
    Scale _tNowSlider;

    /**
     * the play button, obviously.
     */
    Button _playButton;

    PropertyChangeListener _myDateFormatListener = null;

    /**
     * name of property storing slider step size, used for saving state
     */
    private final String SLIDER_STEP_SIZE = "SLIDER_STEP_SIZE";

    /**
     * make the forward button visible at a class level so that we can fire it in
     * testing
     */
    private Button _forwardButton;

    /**
     * utility class to help us plot relative plots
     */
    RelativeProjectionParent _relativeProjector;

    /**
     * the projection we're going to set to relative mode, as we wish
     */
    PlainProjection _targetProjection;

    TrackDataProvider.TrackDataListener _theTrackDataListener;

    /**
     * keep track of the list of play buttons, since on occasion we may want to
     * hide some of them
     * 
     */
    private HashMap<String, Button> _buttonList;

    /**
     * listener for the play button
     * 
     */
    private SelectionAdapter _playListener;

    // private static final Color bColor = new Color(Display.getDefault(), 0, 0,
    // 0);
    private static final Color bColor = Display.getCurrent().getSystemColor(SWT.COLOR_BLACK);

    private static final Color fColor = new Color(Display.getDefault(), 33, 255, 22);

    private static final Font arialFont = new Font(Display.getDefault(), "Arial", 16, SWT.NONE);

    /**
     * This is a callback that will allow us to create the viewer and initialize
     * it.
     */
    public void createPartControl(final Composite parent) {

        // and declare our context sensitive help
        CorePlugin.declareContextHelp(parent, "org.mwc.debrief.help.TimeController");

        // also sort out the slider conversion bits. We do it at the start,
        // because
        // callbacks
        // created during initialisation may need to use/reset it
        _slideManager = new SliderRangeManagement() {
            public void setMinVal(final int min) {
                if (!_tNowSlider.isDisposed()) {
                    Display.getDefault().asyncExec(new Runnable() {
                        public void run() {
                            _tNowSlider.setMinimum(min);
                        }
                    });
                }
            }

            public void setMaxVal(final int max) {
                if (!_tNowSlider.isDisposed())
                    Display.getDefault().asyncExec(new Runnable() {
                        public void run() {
                            _tNowSlider.setMaximum(max);
                        }
                    });
            }

            public void setTickSize(final int small, final int large, final int drag) {
                if (!_tNowSlider.isDisposed()) {
                    Display.getDefault().asyncExec(new Runnable() {
                        public void run() {
                            _tNowSlider.setIncrement(small);
                            _tNowSlider.setPageIncrement(large);
                        }
                    });
                }
            }

            public void setEnabled(final boolean val) {
                if (!_tNowSlider.isDisposed())
                    Display.getDefault().asyncExec(new Runnable() {
                        public void run() {
                            _tNowSlider.setEnabled(val);
                        }
                    });
            }
        };

        // and fill in the interface
        buildInterface(parent);

        // of course though, we start off with the buttons not enabled
        _wholePanel.setEnabled(false);

        // and start listing for any part action
        setupListeners();

        // ok we're all ready now. just try and see if the current part is valid
        _myPartMonitor.fireActivePart(getSite().getWorkbenchWindow().getActivePage());

        // say that we're a selection provider
        getSite().setSelectionProvider(this);
    }

    private MWC.Utilities.Timer.Timer getTimer() {
        // have we created it yet?
        if (_myTimer == null) {
            // nope, better go for it.
            /**
             * the timer-related settings
             */
            _myTimer = new MWC.Utilities.Timer.Timer();
            _myTimer.stop();
            _myTimer.setDelay(1000);
            _myTimer.addTimerListener(this);
        }

        return _myTimer;
    }

    /**
     * ok - put in our bits
     * 
     * @param parent
     */
    private void buildInterface(final Composite parent) {
        // ok, draw our wonderful GUI.
        _wholePanel = new Composite(parent, SWT.BORDER);

        final GridLayout onTop = new GridLayout();
        onTop.horizontalSpacing = 0;
        onTop.verticalSpacing = 0;
        onTop.marginHeight = 0;
        onTop.marginWidth = 0;
        _wholePanel.setLayout(onTop);

        // stick in the long list of VCR buttons
        createVCRbuttons();

        _timeLabel = new Label(_wholePanel, SWT.NONE);
        final GridData labelGrid = new GridData(GridData.FILL_HORIZONTAL);
        _timeLabel.setLayoutData(labelGrid);
        _timeLabel.setAlignment(SWT.CENTER);
        _timeLabel.setText(DUFF_TIME_TEXT);
        // _timeLabel.setFont(new Font(Display.getDefault(), "OCR A Extended",
        // 16,
        // SWT.NONE));
        _timeLabel.setFont(arialFont);
        _timeLabel.setForeground(fColor);
        _timeLabel.setBackground(bColor);

        // next create the time slider holder
        _tNowSlider = new Scale(_wholePanel, SWT.NONE);
        final GridData sliderGrid = new GridData(GridData.FILL_HORIZONTAL);
        _tNowSlider.setLayoutData(sliderGrid);
        _tNowSlider.setMinimum(0);
        _tNowSlider.setMaximum(100);
        _tNowSlider.addSelectionListener(new SelectionListener() {
            public void widgetSelected(final SelectionEvent e) {
                _alreadyProcessingChange = true;
                try {
                    final int index = _tNowSlider.getSelection();
                    final HiResDate newDTG = _slideManager.fromSliderUnits(index, _dtgRangeSlider.getStepSize());
                    fireNewTime(newDTG);
                } catch (final Exception ex) {
                    System.err.println("Tripped in step forward:" + ex);
                } finally {
                    _alreadyProcessingChange = false;
                }
            }

            public void widgetDefaultSelected(final SelectionEvent e) {
            }
        });
        _tNowSlider.addListener(SWT.MouseWheel, new WheelMovedEvent());

        /**
         * declare the handler we use for if the user double-clicks on a slider
         * marker
         * 
         */
        final DoFineControl fineControl = new DoFineControl() {
            public void adjust(final boolean isMax) {
                doFineControl(isMax);
            }
        };

        _dtgRangeSlider = new DTGBiSlider(_wholePanel, fineControl) {
            public void rangeChanged(final TimePeriod period) {
                super.rangeChanged(period);

                selectPeriod(period);
            }

        };
        if (_defaultSliderResolution != null)
            _dtgRangeSlider.setStepSize(_defaultSliderResolution.intValue());

        // hmm, do we have a default step size for the slider?
        final GridData biGrid = new GridData(GridData.FILL_BOTH);
        _dtgRangeSlider.getControl().setLayoutData(biGrid);
    }

    /**
     * user has double-clicked on one of the slider markers, allow detailed edit
     * 
     * @param doMinVal
     *          whether it was the min or max value
     */
    public void doFineControl(final boolean doMinVal) {
        final FineTuneStepperProps fineTunerProperties = new FineTuneStepperProps(_dtgRangeSlider, doMinVal);
        final EditableWrapper wrappedEditable = new EditableWrapper(fineTunerProperties);
        final StructuredSelection _propsAsSelection1 = new StructuredSelection(wrappedEditable);
        CorePlugin.editThisInProperties(_selectionListeners, _propsAsSelection1, this, this);
    }

    /**
     * 
     */
    private void createVCRbuttons() {
        // first create the button holder
        _btnPanel = new Composite(_wholePanel, SWT.BORDER);
        _btnPanel.setLayout(new GridLayout(7, true));

        final Button eBwd = new Button(_btnPanel, SWT.NONE);
        addTimeButtonListener(eBwd, new RepeatingTimeButtonListener(false, STEP_SIZE.END, false));
        eBwd.setImage(TimeControllerPlugin.getImage(ICON_MEDIA_BEGINNING));
        eBwd.setToolTipText("Move to start of dataset");

        final Button lBwd = new Button(_btnPanel, SWT.NONE);
        lBwd.setToolTipText("Move backward large step (hold to repeat)");
        lBwd.setImage(TimeControllerPlugin.getImage(ICON_MEDIA_REWIND));
        RepeatingTimeButtonListener listener = new RepeatingTimeButtonListener(false, STEP_SIZE.LARGE, true);
        addTimeButtonListener(lBwd, listener);

        final Button sBwd = new Button(_btnPanel, SWT.NONE);
        sBwd.setToolTipText("Move backward small step (hold to repeat)");
        sBwd.setImage(TimeControllerPlugin.getImage(ICON_MEDIA_BACK));
        listener = new RepeatingTimeButtonListener(false, STEP_SIZE.NORMAL, true);
        addTimeButtonListener(sBwd, listener);

        _playButton = new Button(_btnPanel, SWT.TOGGLE | SWT.NONE);
        _playButton.setImage(TimeControllerPlugin.getImage(ICON_MEDIA_PLAY));
        _playButton.setToolTipText(PLAY_TEXT);
        _playListener = new SelectionAdapter() {
            public void widgetSelected(final SelectionEvent e) {
                final boolean playing = _playButton.getSelection();
                final String tipTxt;
                final String imageTxt;
                // ImageDescriptor thisD;
                if (playing) {
                    startPlaying();
                    tipTxt = PAUSE_TEXT;
                    imageTxt = ICON_MEDIA_PAUSE;
                } else {
                    stopPlaying();
                    tipTxt = PLAY_TEXT;
                    imageTxt = ICON_MEDIA_PLAY;

                }

                // ok, set the tooltip & image
                _playButton.setToolTipText(tipTxt);
                _playButton.setImage(TimeControllerPlugin.getImage(imageTxt));

            }
        };
        _playButton.addSelectionListener(_playListener);

        _forwardButton = new Button(_btnPanel, SWT.NONE);
        _forwardButton.setImage(TimeControllerPlugin.getImage(ICON_MEDIA_FORWARD));
        listener = new RepeatingTimeButtonListener(true, STEP_SIZE.NORMAL, true);
        addTimeButtonListener(_forwardButton, listener);

        _forwardButton.setToolTipText("Move forward small step (hold to repeat)");

        final Button lFwd = new Button(_btnPanel, SWT.NONE);
        lFwd.setImage(TimeControllerPlugin.getImage(ICON_MEDIA_FAST_FORWARD));
        lFwd.setToolTipText("Move forward large step (hold to repeat)");
        listener = new RepeatingTimeButtonListener(true, STEP_SIZE.LARGE, true);
        addTimeButtonListener(lFwd, listener);

        final Button eFwd = new Button(_btnPanel, SWT.NONE);
        addTimeButtonListener(eFwd, new RepeatingTimeButtonListener(true, STEP_SIZE.END, false));
        eFwd.setImage(TimeControllerPlugin.getImage(ICON_MEDIA_END));
        eFwd.setToolTipText("Move to end of dataset");

        final GridDataFactory btnGd = GridDataFactory.fillDefaults().grab(true, false);
        btnGd.applyTo(eBwd);
        btnGd.applyTo(lBwd);
        btnGd.applyTo(sBwd);
        btnGd.applyTo(_playButton);
        btnGd.applyTo(_forwardButton);
        btnGd.applyTo(lFwd);
        btnGd.applyTo(eFwd);

        // and apply it to the whole panel
        btnGd.applyTo(_btnPanel);

        _buttonList = new HashMap<String, Button>();
        _buttonList.put("eBwd", eBwd);
        _buttonList.put("lBwd", lBwd);
        _buttonList.put("sBwd", sBwd);
        _buttonList.put("play", _playButton);
        _buttonList.put("sFwd", _forwardButton);
        _buttonList.put("lFwd", lFwd);
        _buttonList.put("eFwd", eFwd);
    }

    private void addTimeButtonListener(final Button button, final RepeatingTimeButtonListener listener) {
        button.addListener(SWT.MouseDown, listener);
        button.addListener(SWT.MouseUp, listener);
        button.addDisposeListener(new DisposeListener() {

            @Override
            public void widgetDisposed(DisposeEvent e) {
                listener.purge();
                button.removeListener(SWT.MouseDown, listener);
                button.removeListener(SWT.MouseUp, listener);
            }
        });
    }

    boolean _alreadyProcessingChange = false;

    /**
     * user has selected a time period, indicate it to the controllable
     * 
     * @param period
     */
    protected void selectPeriod(final TimePeriod period) {
        if (_controllablePeriod != null) {

            // updating the text items has to be done in the UI thread. make it
            // so
            Display.getDefault().asyncExec(new Runnable() {
                public void run() {
                    // just do a double-check that we have a controllable
                    // period,
                    // - after this process is being run as async, we may have
                    // lost the controllable
                    // period since starting the manoeuvre.
                    if (_controllablePeriod == null) {
                        CorePlugin.logError(Status.ERROR,
                                "Maintainer problem: In TimeController, we have lost our controllable period in async call",
                                null);
                        return;
                    }

                    _controllablePeriod.setPeriod(period);

                    // are we set to filter?
                    if (_filterToSelectionAction.isChecked()) {
                        _controllablePeriod.performOperation(ControllablePeriod.FILTER_TO_TIME_PERIOD);

                        // and trim down the range of our slider manager

                        // hey, what's the current dtg?
                        final HiResDate currentDTG = _slideManager.fromSliderUnits(_tNowSlider.getSelection(),
                                _dtgRangeSlider.getStepSize());

                        // update the range of the slider
                        _slideManager.resetRange(period.getStartDTG(), period.getEndDTG());

                        // hey - remember the updated time range (largely so
                        // that we can
                        // restore from file later on)
                        _myStepperProperties.setSliderStartTime(period.getStartDTG());
                        _myStepperProperties.setSliderEndTime(period.getEndDTG());

                        // do we need to move the slider back into a valid
                        // point?
                        // hmm, was it too late?
                        HiResDate trimmedDTG = null;
                        if (currentDTG.greaterThan(period.getEndDTG())) {
                            trimmedDTG = period.getEndDTG();
                        } else if (currentDTG.lessThan(period.getStartDTG())) {
                            trimmedDTG = period.getStartDTG();
                        } else {
                            if (!_alreadyProcessingChange)
                                if (!_tNowSlider.isDisposed()) {
                                    _tNowSlider.setSelection(_slideManager.toSliderUnits(currentDTG));
                                }
                        }

                        // did we have to move them?
                        if (trimmedDTG != null) {
                            fireNewTime(trimmedDTG);
                        }
                    }
                }
            });
        }

    }

    void stopPlaying() {
        /**
         * give the event to our child class, in case it's a scenario
         * 
         */
        if (_steppableTime != null)
            _steppableTime.pause(this, true);

        getTimer().stop();
    }

    /**
     * ok, start auto-stepping forward through the serial
     */
    void startPlaying() {
        // hey - set a practical minimum step size, 1/4 second is a fair start
        // point
        final long delayToUse = Math.max(_myStepperProperties.getAutoInterval().getMillis(), 250);

        // ok - make sure the time has the right time
        getTimer().setDelay(delayToUse);

        getTimer().start();
    }

    public void onTime(final ActionEvent event) {

        // temporarily remove ourselves, to prevent being called twice
        getTimer().removeTimerListener(this);

        // catch any exceptions raised here, it doesn't really
        // matter if we miss a time step
        try {
            // pass the step operation on to our parent
            processClick(STEP_SIZE.NORMAL, true);
        } catch (final Exception e) {
            CorePlugin.logError(Status.ERROR, "Error on auto-time stepping", e);
        }

        // register ourselves as a time again
        getTimer().addTimerListener(this);

    }

    /** declare a range of sizes for different types of time step
     * 
     * @author ian
     *
     */
    protected enum STEP_SIZE {
        END, SMALL, NORMAL, LARGE
    }

    protected final class NewTimeListener implements PropertyChangeListener {
        public void propertyChange(final PropertyChangeEvent event) {
            // see if it's the time or the period which
            // has changed
            if (event.getPropertyName().equals(TimeProvider.TIME_CHANGED_PROPERTY_NAME)) {
                // ok, use the new time
                final HiResDate newDTG = (HiResDate) event.getNewValue();
                timeUpdated(newDTG);
            } else if (event.getPropertyName().equals(TimeProvider.PERIOD_CHANGED_PROPERTY_NAME)) {
                final TimePeriod newPeriod = (TimePeriod) event.getNewValue();
                if (newPeriod == null) {
                    Display.getDefault().asyncExec(new Runnable() {
                        public void run() {

                            // hey, we haven't got a time period- disable
                            _wholePanel.setEnabled(false);

                            // hey - remember the updated time range (largely so
                            // that we can
                            // restore from file later on)
                            _myStepperProperties.setSliderStartTime(null);
                            _myStepperProperties.setSliderEndTime(null);

                        }
                    });
                } else {

                    // extend the slider
                    _slideManager.resetRange(newPeriod.getStartDTG(), newPeriod.getEndDTG());

                    // now the slider selector bar thingy
                    Display.getDefault().asyncExec(new Runnable() {
                        public void run() {
                            // ok, double-check we're enabled
                            _wholePanel.setEnabled(true);

                            // and our range selector - first the outer
                            // ranges
                            _dtgRangeSlider.updateOuterRanges(newPeriod);

                            // ok - we no longer reset the range limits on a data change,
                            // since it's proving inconvenient to have to reset the sliders
                            // after some data is dropped in.

                            // ok, now the user ranges...
                            // _dtgRangeSlider.updateSelectedRanges(newPeriod.getStartDTG(),
                            // newPeriod.getEndDTG());
                        }
                    });
                }
            }

            // also double-check if it's time to enable our interface
            checkTimeEnabled();
        }
    }

    void processClick(final STEP_SIZE step, final boolean fwd) {

        // CorePlugin.logError(Status.INFO, "Starting step", null);

        // check that we have a current time (on initialisation some plots may
        // not
        // contain data)
        final HiResDate tNow = _myTemporalDataset.getTime();
        if (tNow != null) {
            // just check if we've got a simulation running, in which case we just
            // fire a step
            if (_steppableTime != null)
                // see if the user has pressed 'back to start', in which case we will
                // rewind
                if ((step == STEP_SIZE.END) && (fwd == false))
                    _steppableTime.restart(this, true);
                else
                    _steppableTime.step(this, true);
            else {

                // yup, time is there. work with it baby
                long micros = tNow.getMicros();

                // right, special case for when user wants to go straight to the end
                // - in
                // which
                // case there is a zero in the scale
                if (step == STEP_SIZE.END) {
                    // right, fwd or bwd
                    if (fwd)
                        micros = _myTemporalDataset.getPeriod().getEndDTG().getMicros();
                    else
                        micros = _myTemporalDataset.getPeriod().getStartDTG().getMicros();
                } else {
                    final long size;

                    // normal processing..
                    if (step == STEP_SIZE.LARGE) {
                        // do large step
                        size = (long) _myStepperProperties.getLargeStep().getValueIn(Duration.MICROSECONDS);
                    } else if (step == STEP_SIZE.NORMAL) {
                        // and the small size step
                        size = (long) _myStepperProperties.getSmallStep().getValueIn(Duration.MICROSECONDS);
                    } else {
                        // and the small size step
                        size = (long) _myStepperProperties.getSmallStep().getValueIn(Duration.MICROSECONDS) / 10;
                    }

                    // right, either move forwards or backwards.
                    if (fwd)
                        micros += size;
                    else
                        micros -= size;
                }

                final HiResDate newDTG = new HiResDate(0, micros);

                // find the extent of the current dataset

                /*
                 * RIGHT, until JAN 2007 this next line had been commented out -
                 * replaced by the line immediately after it. We've switched back to
                 * this implementation. This implementation lets the time-slider select
                 * a time for which there aren't any points visible. This makes sense
                 * because in it's successor implementation when the DTG slipped outside
                 * the visible time period, the event was rejected, and the time-
                 * controller buttons appeared to break. It remains responsive this
                 * way...
                 */
                TimePeriod timeP = _myTemporalDataset.getPeriod();
                if (_filterToSelectionAction != null && _filterToSelectionAction.isChecked()) {
                    TimePeriod filteredPeriod = _controllablePeriod.getPeriod();
                    if (filteredPeriod != null) {
                        timeP = filteredPeriod;
                    }
                }

                if (timeP != null) {
                    // do we represent a valid time?
                    if (timeP.contains(newDTG)) {
                        // yes, fire the new DTG
                        fireNewTime(newDTG);
                    } else {
                        if (newDTG.greaterThan(timeP.getEndDTG())) {
                            fireNewTime(timeP.getEndDTG());
                        } else {
                            fireNewTime(timeP.getStartDTG());
                        }
                        stopPlayingTimer();
                    }
                }
            }
        }

        // CorePlugin.logError(Status.INFO, "Step complete", null);

    }

    private void stopPlayingTimer() {
        if (getTimer().isRunning()) {
            stopPlaying();
            Display.getDefault().asyncExec(new Runnable() {

                @Override
                public void run() {
                    _playButton.setToolTipText(PLAY_TEXT);
                    _playButton.setImage(TimeControllerPlugin.getImage(ICON_MEDIA_PLAY));
                }
            });
        }
    }

    private boolean _firingNewTime = false;

    /**
     * any default size to use for the slider threshold (read in as part of the
     * 'init' operation before we actually create the slider)
     */
    private Integer _defaultSliderResolution;

    /**
     * keep track of what tracks are open - we may want to use them for our
     * exporting calc data to clipboard
     */
    protected TrackDataProvider _myTrackProvider;

    /**
     * the list of operations that the current plot wants us to display
     * 
     */
    protected TimeControllerOperation.TimeControllerOperationStore _timeOperations;

    private Vector<Action> _legacyTimeOperations;

    /**
     * the first of the relative plotting modes - absolute north oriented plt
     */
    private Action _normalPlottingMode;

    /**
     * first custom plotting mode: - always centre the plot on ownship, and orient
     * the plot along ownship heading
     */
    private Action _primaryCentredPrimaryOrientedPlottingMode;

    /**
     * second custom plotting mode: always centre the plot on ownship, but keep
     * north-oriented.
     */
    private Action _primaryCentredNorthOrientedPlottingMode;

    protected LayerPainterManager _layerPainterManager;

    public void fireNewTime(final HiResDate dtg) {
        if (!_firingNewTime) {
            _firingNewTime = true;
            try {
                _controllableTime.setTime(this, dtg, true);
            } finally {
                _firingNewTime = false;
            }
        }
    }

    private void setupListeners() {

        // try to add ourselves to listen out for page changes
        // getSite().getWorkbenchWindow().getPartService().addPartListener(this);

        _myPartMonitor = new PartMonitor(getSite().getWorkbenchWindow().getPartService());

        _myPartMonitor.addPartListener(TimeProvider.class, PartMonitor.ACTIVATED, new PartMonitor.ICallback() {
            public void eventTriggered(final String type, final Object part, final IWorkbenchPart parentPart) {
                if (_myTemporalDataset != part) {
                    // ok, stop listening to the old one
                    if (_myTemporalDataset != null) {
                        // right, we were looking at something, and now
                        // we're not.

                        // stop playing (if we were)
                        if (getTimer().isRunning()) {
                            // un-depress the play button
                            _playButton.setSelection(false);

                            // and tell the button's listeners (which
                            // will stop the timer
                            // and update the image)
                            _playButton.notifyListeners(SWT.Selection, new Event());
                        }

                        // stop listening to that dataset
                        _myTemporalDataset.removeListener(_temporalListener,
                                TimeProvider.TIME_CHANGED_PROPERTY_NAME);
                        _myTemporalDataset.removeListener(_temporalListener,
                                TimeProvider.PERIOD_CHANGED_PROPERTY_NAME);
                    }

                    // implementation here.
                    _myTemporalDataset = (TimeProvider) part;

                    // and start listening to the new one
                    _myTemporalDataset.addListener(_temporalListener, TimeProvider.TIME_CHANGED_PROPERTY_NAME);
                    _myTemporalDataset.addListener(_temporalListener, TimeProvider.PERIOD_CHANGED_PROPERTY_NAME);

                    // also configure for the current time
                    final HiResDate newDTG = _myTemporalDataset.getTime();

                    timeUpdated(newDTG);

                    // and initialise the current time
                    final TimePeriod timeRange = _myTemporalDataset.getPeriod();
                    if (timeRange != null) {
                        // and our range selector - first the outer
                        // ranges
                        _dtgRangeSlider.updateOuterRanges(timeRange);

                        // ok, now the user ranges...
                        _dtgRangeSlider.updateSelectedRanges(timeRange.getStartDTG(), timeRange.getEndDTG());

                        // and the time slider range
                        _slideManager.resetRange(timeRange.getStartDTG(), timeRange.getEndDTG());

                    }

                    checkTimeEnabled();

                    // hmm, do we want to store this part?
                    if (parentPart instanceof IEditorPart) {
                        _currentEditor = (IEditorPart) parentPart;
                    }
                }

            }
        });
        _myPartMonitor.addPartListener(TimeProvider.class, PartMonitor.CLOSED, new PartMonitor.ICallback() {
            public void eventTriggered(final String type, final Object part, final IWorkbenchPart parentPart) {
                // was it our one?
                if (_myTemporalDataset == part) {
                    // ok, stop listening to this object (just in case
                    // we were,
                    // anyway).
                    _myTemporalDataset.removeListener(_temporalListener, TimeProvider.TIME_CHANGED_PROPERTY_NAME);
                    _myTemporalDataset.removeListener(_temporalListener, TimeProvider.PERIOD_CHANGED_PROPERTY_NAME);

                    _myTemporalDataset = null;
                }

                // and sort out whether we should be active or not.
                checkTimeEnabled();
            }
        });

        _myPartMonitor.addPartListener(TimeControllerOperationStore.class, PartMonitor.ACTIVATED,
                new PartMonitor.ICallback() {
                    public void eventTriggered(final String type, final Object part,
                            final IWorkbenchPart parentPart) {
                        if (part != _timeOperations) {
                            _timeOperations = (TimeControllerOperationStore) part;

                            // and refresh the dropdown menu
                            refreshTimeOperations();
                        }
                    }

                });

        _myPartMonitor.addPartListener(SteppableTime.class, PartMonitor.ACTIVATED, new PartMonitor.ICallback() {
            public void eventTriggered(final String type, final Object part, final IWorkbenchPart parentPart) {
                if (_steppableTime != part) {
                    _steppableTime = (SteppableTime) part;

                    // enable the ui, if we have to.
                    checkTimeEnabled();
                }
            }

        });
        _myPartMonitor.addPartListener(SteppableTime.class, PartMonitor.CLOSED, new PartMonitor.ICallback() {
            public void eventTriggered(final String type, final Object part, final IWorkbenchPart parentPart) {
                if (_steppableTime != part) {
                    _steppableTime = null;
                    // disable the ui, if we have to.
                    checkTimeEnabled();
                }
            }
        });

        _myPartMonitor.addPartListener(Layers.class, PartMonitor.ACTIVATED, new PartMonitor.ICallback() {
            public void eventTriggered(final String type, final Object part, final IWorkbenchPart parentPart) {
                // implementation here.
                final Layers newLayers = (Layers) part;
                if (newLayers != _myLayers) {
                    _myLayers = newLayers;
                }
            }

        });
        _myPartMonitor.addPartListener(Layers.class, PartMonitor.CLOSED, new PartMonitor.ICallback() {
            public void eventTriggered(final String type, final Object part, final IWorkbenchPart parentPart) {
                if (part == _myLayers)
                    _myLayers = null;
            }
        });

        _myPartMonitor.addPartListener(RelativeProjectionParent.class, PartMonitor.ACTIVATED,
                new PartMonitor.ICallback() {
                    public void eventTriggered(final String type, final Object part,
                            final IWorkbenchPart parentPart) {
                        // implementation here.
                        final RelativeProjectionParent relProjector = (RelativeProjectionParent) part;
                        if (relProjector != _relativeProjector) {
                            // ok, better store it
                            storeProjectionParent(relProjector);
                        }
                    }
                });
        _myPartMonitor.addPartListener(RelativeProjectionParent.class, PartMonitor.CLOSED,
                new PartMonitor.ICallback() {
                    public void eventTriggered(final String type, final Object part,
                            final IWorkbenchPart parentPart) {
                        if (part == _relativeProjector)
                            _relativeProjector = null;
                    }
                });

        _myPartMonitor.addPartListener(PlainProjection.class, PartMonitor.ACTIVATED, new PartMonitor.ICallback() {
            public void eventTriggered(final String type, final Object part, final IWorkbenchPart parentPart) {
                // implementation here.
                final PlainProjection newProjection = (PlainProjection) part;
                if (newProjection != _targetProjection) {
                    storeNewProjection(newProjection);
                }
            }
        });
        _myPartMonitor.addPartListener(PlainProjection.class, PartMonitor.CLOSED, new PartMonitor.ICallback() {
            public void eventTriggered(final String type, final Object part, final IWorkbenchPart parentPart) {
                if (part == _targetProjection)
                    _targetProjection = null;
            }
        });

        _myPartMonitor.addPartListener(ControllableTime.class, PartMonitor.ACTIVATED, new PartMonitor.ICallback() {
            public void eventTriggered(final String type, final Object part, final IWorkbenchPart parentPart) {

                if (_controllableTime != part) {
                    // implementation here.
                    final ControllableTime ct = (ControllableTime) part;
                    _controllableTime = ct;
                    checkTimeEnabled();
                }
            }

        });
        _myPartMonitor.addPartListener(ControllableTime.class, PartMonitor.CLOSED, new PartMonitor.ICallback() {
            public void eventTriggered(final String type, final Object part, final IWorkbenchPart parentPart) {
                if (part == _controllableTime) {
                    _controllableTime = null;
                    checkTimeEnabled();
                }
            }
        });
        _myPartMonitor.addPartListener(ControllablePeriod.class, PartMonitor.ACTIVATED,
                new PartMonitor.ICallback() {
                    public void eventTriggered(final String type, final Object part,
                            final IWorkbenchPart parentPart) {

                        if (_controllablePeriod != part) {
                            // right, we're clearly not running a simulation here, clear the
                            // simulation object
                            // that gets uses as a flag
                            _steppableTime = null;

                            // implementation here.
                            final ControllablePeriod ct = (ControllablePeriod) part;
                            _controllablePeriod = ct;
                            checkTimeEnabled();

                            // ok, we've got all the normal controls, make the ui do it's
                            // stuff
                            reformatUI(true);
                        }
                    }

                });
        _myPartMonitor.addPartListener(ControllablePeriod.class, PartMonitor.CLOSED, new PartMonitor.ICallback() {
            public void eventTriggered(final String type, final Object part, final IWorkbenchPart parentPart) {
                if (part == _controllablePeriod) {
                    _controllablePeriod = null;
                    checkTimeEnabled();
                }
            }
        });

        _myPartMonitor.addPartListener(TrackDataProvider.class, PartMonitor.ACTIVATED, new PartMonitor.ICallback() {
            public void eventTriggered(final String type, final Object part, final IWorkbenchPart parentPart) {
                // sort out our listener
                if (_theTrackDataListener == null)
                    _theTrackDataListener = new TrackDataProvider.TrackDataListener() {

                        public void tracksUpdated(final WatchableList primary, final WatchableList[] secondaries) {
                            // ok - make sure we're seeing the full time
                            // period
                            // NO: don't bother, since adding an annotation causes the
                            // filter to reset
                            // expandTimeSliderRangeToFull();

                            // and the controls are enabled (if we know
                            // time data)
                            checkTimeEnabled();
                        }
                    };

                final TrackDataProvider thisTrackProvider = (TrackDataProvider) part;
                if (thisTrackProvider != _myTrackProvider) {
                    // do we have one already?
                    if (_myTrackProvider != null) {
                        // ok, ditch existing provider
                        _myTrackProvider.removeTrackDataListener(_theTrackDataListener);
                    }

                    // remember the new one
                    _myTrackProvider = thisTrackProvider;
                    _myTrackProvider.addTrackDataListener(_theTrackDataListener);
                }
            }

        });
        _myPartMonitor.addPartListener(TrackDataProvider.class, PartMonitor.CLOSED, new PartMonitor.ICallback() {
            public void eventTriggered(final String type, final Object part, final IWorkbenchPart parentPart) {
                if (part == _myTrackProvider) {
                    _myTrackProvider = null;
                }
            }
        });

        _myPartMonitor.addPartListener(LayerPainterManager.class, PartMonitor.ACTIVATED,
                new PartMonitor.ICallback() {
                    public void eventTriggered(final String type, final Object part,
                            final IWorkbenchPart parentPart) {
                        if (_layerPainterManager != part) {
                            // ok, insert the painter mode actions, together with
                            // our standard
                            // ones

                            _layerPainterManager = (LayerPainterManager) part;
                            populateDropDownList(_layerPainterManager);
                        }
                    }

                });
        _myPartMonitor.addPartListener(LayerPainterManager.class, PartMonitor.CLOSED, new PartMonitor.ICallback() {
            public void eventTriggered(final String type, final Object part, final IWorkbenchPart parentPart) {
                if (_layerPainterManager == part) {
                    _layerPainterManager = null;
                }
            }
        });

        _myPartMonitor.addPartListener(TimeControlPreferences.class, PartMonitor.ACTIVATED,
                new PartMonitor.ICallback() {
                    public void eventTriggered(final String type, final Object part,
                            final IWorkbenchPart parentPart) {
                        // just check we're not already managing this plot
                        if (part != _myStepperProperties) {
                            // ok, ignore the old one, if we have one
                            if (_myStepperProperties != null) {
                                _myStepperProperties.removePropertyChangeListener(_myDateFormatListener);
                                _myStepperProperties = null;
                            }

                            _myStepperProperties = (TimeControlProperties) part;

                            if (_myDateFormatListener == null)
                                _myDateFormatListener = new PropertyChangeListener() {
                                    public void propertyChange(final PropertyChangeEvent evt) {
                                        // right, see if the user is changing
                                        // the DTG format
                                        if (evt.getPropertyName().equals(TimeControlProperties.DTG_FORMAT_ID)) {
                                            // ok, refresh the DTG
                                            final String newVal = getFormattedDate(_myTemporalDataset.getTime());
                                            _timeLabel.setText(newVal);

                                            // hmm, also set the bi-slider to
                                            // repaint so we get fresh
                                            // labels
                                            _dtgRangeSlider.update();
                                        } else if (evt.getPropertyName()
                                                .equals(TimeControlProperties.STEP_INTERVAL_ID)) {
                                            // hey, if we're stepping, we'd
                                            // better change the size of
                                            // the time step
                                            if (getTimer().isRunning()) {
                                                final Duration theDelay = (Duration) evt.getNewValue();
                                                getTimer().setDelay(
                                                        (long) theDelay.getValueIn(Duration.MILLISECONDS));
                                            }
                                        }
                                    }
                                };

                            // also, listen out for changes in the DTG formatter
                            _myStepperProperties.addPropertyChangeListener(_myDateFormatListener);

                            // and update the slider ranges
                            // do we have start/stop times?
                            final HiResDate startDTG = _myStepperProperties.getSliderStartTime();
                            if ((startDTG != null) && (_myTemporalDataset != null)) {
                                // cool - update the slider to our data settings

                                HiResDate startTime, endTime;
                                startTime = _myStepperProperties.getSliderStartTime();
                                endTime = _myStepperProperties.getSliderEndTime();

                                _slideManager.resetRange(startTime, endTime);

                                // ok, set the slider ranges...
                                _dtgRangeSlider.updateSelectedRanges(startTime, endTime);

                                // and set the time again - the slider has
                                // probably forgotten
                                // it.
                                timeUpdated(_myTemporalDataset.getTime());

                            }
                        }
                    }
                });
    }

    protected void refreshTimeOperations() {
        // ok, loop through them, deleting them
        final IMenuManager menuManager = getViewSite().getActionBars().getMenuManager();

        // do we have any legacy time operations
        if (_legacyTimeOperations == null) {
            _legacyTimeOperations = new Vector<Action>();
        } else {
            // yup, we do have one - better ditch the old ones
            final Iterator<Action> iter = _legacyTimeOperations.iterator();
            while (iter.hasNext()) {
                final Action action = iter.next();
                menuManager.remove((IContributionItem) action);
            }

            // and clear the list
            _legacyTimeOperations.removeAllElements();
        }

        // ok, now add the new ones
        if (_timeOperations != null) {
            final Iterator<TimeControllerOperation> newOps = _timeOperations.iterator();
            while (newOps.hasNext()) {
                final TimeControllerOperation newOp = newOps.next();
                final Action newAction = new Action(newOp.getName()) {

                    @Override
                    public void run() {
                        newOp.run(_myTrackProvider.getPrimaryTrack(), _myTrackProvider.getSecondaryTracks(),
                                getPeriod());
                    }
                };

                // give it an id, so we can look for it shortly.
                newAction.setId(newAction.getText());

                final ImageDescriptor id = newOp.getDescriptor();
                if (id != null)
                    newAction.setImageDescriptor(id);

                // see if we already contain it
                if (menuManager.find(newAction.getId()) != null) {
                    // ignore, we already know about it
                } else
                    menuManager.insertBefore(OP_LIST_MARKER_ID, newAction);
            }

        }
    }

    /**
     * we may be listening to an object that cannot be rewound, that does not
     * support backward stepping. If so, let us reformat ourselves accordingly
     * 
     * @param canRewind
     *          whether this time-dataset can rewind
     */
    protected void reformatUI(final boolean canRewind) {
        _tNowSlider.setVisible(canRewind);
        _dtgRangeSlider.getControl().setVisible(canRewind);

        // and now the play buttons
        _buttonList.get("lBwd").setVisible(canRewind);
        _buttonList.get("sBwd").setVisible(canRewind);
        _buttonList.get("lFwd").setVisible(canRewind);
        _buttonList.get("eFwd").setVisible(canRewind);

        final GridData gd1 = (GridData) _buttonList.get("lBwd").getLayoutData();
        final GridData gd2 = (GridData) _buttonList.get("sBwd").getLayoutData();
        final GridData gd3 = (GridData) _buttonList.get("lFwd").getLayoutData();
        final GridData gd4 = (GridData) _buttonList.get("eFwd").getLayoutData();
        gd1.exclude = !canRewind;
        gd2.exclude = !canRewind;
        gd3.exclude = !canRewind;
        gd4.exclude = !canRewind;

        // also reduce the number of columns if we have to
        final GridLayout gl = (GridLayout) _btnPanel.getLayout();
        if (canRewind) {
            gl.numColumns = 7;
        } else {
            gl.numColumns = 3;
        }

        // tell the parent that some buttons have changed, and that it probably
        // wants to do a re-layout
        _btnPanel.pack(true);

        // sort out the dropdowns
        populateDropDownList(null);

    }

    /**
     * remember the new relative projection provider
     * 
     * @param relProjector
     */
    protected void storeProjectionParent(final RelativeProjectionParent relProjector) {
        // ok, this isn't us, is it?
        if (relProjector != this) {
            // ok, store it
            _relativeProjector = relProjector;
        }
    }

    protected void storeNewProjection(final PlainProjection newProjection) {
        // ok, remember the projection
        _targetProjection = newProjection;

        // and tell it we're here
        _targetProjection.setRelativeProjectionParent(this);

        // and reflect it's current status
        if (_targetProjection.getNonStandardPlotting()) {
            if (_targetProjection.getPrimaryOriented()) {
                _primaryCentredPrimaryOrientedPlottingMode.setChecked(true);
            } else {
                _primaryCentredNorthOrientedPlottingMode.setChecked(true);
            }

        } else
            _normalPlottingMode.setChecked(true);
    }

    /**
     * convenience method to make the panel enabled if we have a time controller
     * and a valid time
     */
    void checkTimeEnabled() {
        boolean enable = false;

        if (_steppableTime != null) {
            // this is our 'fancy' situation - just enable it
            enable = true;
        } else {
            // normal, Debrief-style situation, check we've got
            // what we're after
            if (_myTemporalDataset != null) {
                if ((_controllableTime != null) && (_myTemporalDataset.getTime() != null))
                    enable = true;
            }
        }

        final boolean finalEnabled = enable;

        Display.getDefault().asyncExec(new Runnable() {
            public void run() {
                if (!_wholePanel.isDisposed()) {
                    // aaah, if we're clearing the panel, set the text to
                    // "pending"
                    if (_myTemporalDataset == null) {
                        _timeLabel.setText("-----");
                        _dtgRangeSlider.setShowLabels(false);
                        _dtgRangeSlider.resetMinMaxPointers();
                    }

                    _wholePanel.setEnabled(finalEnabled);
                    _dtgRangeSlider.setShowLabels(finalEnabled);
                }
            }
        });
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.eclipse.ui.part.WorkbenchPart#dispose()
     */
    @SuppressWarnings("deprecation")
    public void dispose() {
        super.dispose();

        // and ditch the timer
        _myTimer = null;

        // and stop listening for part activity
        _myPartMonitor.dispose(getSite().getWorkbenchWindow().getPartService());

        // also stop listening for time events
        if (_myTemporalDataset != null) {
            _myTemporalDataset.removeListener(_temporalListener, TimeProvider.TIME_CHANGED_PROPERTY_NAME);
            _myTemporalDataset.removeListener(_temporalListener, TimeProvider.PERIOD_CHANGED_PROPERTY_NAME);
        }
    }

    /**
     * Passing the focus request to the viewer's control.
     */
    void editMeInProperties(final PropertyChangeSupport props) {
        // do we have any data?
        if (props != null) {
            // get the editable thingy
            if (_propsAsSelection == null)
                _propsAsSelection = new StructuredSelection(props);

            CorePlugin.editThisInProperties(_selectionListeners, _propsAsSelection, this, this);
            _propsAsSelection = null;
        } else {
            System.out.println("we haven't got any properties yet");
        }
    }

    /**
     * the data we are looking at has updated. If we're set to follow that time,
     * update ourselves
     */
    void timeUpdated(final HiResDate newDTG) {
        if (newDTG != null) {
            // display the correct time.
            if (!_timeLabel.isDisposed()) {
                // updating the text items has to be done in the UI thread. make
                // it so
                // note - we use 'syncExec'. When we were using asyncExec, we
                // would have
                // a back-log
                // of events waiting to fire.

                final Runnable nextEvent = new Runnable() {
                    public void run() {
                        // display the correct time.
                        final String newVal = getFormattedDate(newDTG);

                        if (!_timeLabel.isDisposed())
                            _timeLabel.setText(newVal);

                        if (!_alreadyProcessingChange) {

                            // there's a (slim) chance that the temp dataset has
                            // already been
                            // cleared, or
                            // hasn't been caught yet. just check we still know
                            // about it
                            if (_myTemporalDataset != null) {
                                final TimePeriod dataPeriod = _myTemporalDataset.getPeriod();
                                if (dataPeriod != null) {
                                    final int newIndex = _slideManager.toSliderUnits(newDTG);
                                    // did we find a valid time?
                                    if (newIndex != -1) {
                                        // yes, go for it.
                                        if (!_tNowSlider.isDisposed()) {
                                            _tNowSlider.setSelection(newIndex);
                                        }
                                    }
                                }
                            }
                        }
                    }
                };

                Display.getDefault().syncExec(nextEvent);

            }
        } else {
            System.out.println("null DTG received by time controller");
            // updating the text items has to be done in the UI thread. make it
            // so
            Display.getDefault().asyncExec(new Runnable() {
                public void run() {

                    _timeLabel.setText(DUFF_TIME_TEXT);
                }
            });
        }

    }

    String getFormattedDate(final HiResDate newDTG) {
        String newVal = "n/a";
        // hmm, we may have heard about the new date before hearing about the
        // plot's stepper properties. check they arrived
        if (_myStepperProperties != null) {
            final String dateFormat = _myStepperProperties.getDTGFormat();

            // store.getString(PreferenceConstants.P_STRING);
            try {
                newVal = toStringHiRes(newDTG, dateFormat);
            } catch (final IllegalArgumentException e) {
                System.err.println("Invalid date format in preferences");
            }
        }
        return newVal;
    }

    private static SimpleDateFormat _myFormat = null;

    private static String _myFormatString = null;

    public synchronized static String toStringHiRes(final HiResDate time, final String pattern)
            throws IllegalArgumentException {
        // so, have a look at the data
        final long micros = time.getMicros();
        // long wholeSeconds = micros / 1000000;

        final StringBuffer res = new StringBuffer();

        final java.util.Date theTime = new java.util.Date(micros / 1000);

        // do we already know about a date format?
        if (_myFormatString != null) {
            // right, see if it's what we're after
            if (_myFormatString != pattern) {
                // nope, it's not what we're after. ditch gash
                _myFormatString = null;
                _myFormat = null;
            }
        }

        // so, we either don't have a format yet, or we did have, and now we
        // want to
        // forget it...
        if (_myFormat == null) {
            _myFormatString = pattern;
            _myFormat = new SimpleDateFormat(pattern);
            _myFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
        }

        res.append(_myFormat.format(theTime));

        final DecimalFormat microsFormat = new DecimalFormat("000000");
        // DecimalFormat millisFormat = new DecimalFormat("000");

        // do we have micros?
        if (micros % 1000 > 0) {
            // yes
            res.append(".");
            res.append(microsFormat.format(micros % 1000000));
        } else {
            // do we have millis?
            // if (micros % 1000000 > 0)
            // {
            // // yes, convert the value to millis
            //
            // long millis = micros = (micros % 1000000) / 1000;
            //
            // res.append(".");
            // res.append(millisFormat.format(millis));
            // }
            // else
            // {
            // // just use the normal output
            // }
        }

        return res.toString();
    }

    public static class TestTimeController extends TestCase {
        int _min;

        int _max;

        int _smallTick;

        int _largeTick;

        int _dragSize;

        boolean _enabled;

        public void testSliderScales() {
            final SliderRangeManagement range = new SliderRangeManagement() {
                public void setMinVal(final int min) {
                    _min = min;
                }

                public void setMaxVal(final int max) {
                    _max = max;
                }

                public void setTickSize(final int small, final int large, final int drag) {
                    _smallTick = small;
                    _largeTick = large;
                    _dragSize = drag;
                }

                public void setEnabled(final boolean val) {
                    _enabled = val;
                }
            };

            // initialise our testing values
            _min = _max = _smallTick = _largeTick = -1;
            _enabled = false;

            HiResDate starter = new HiResDate(0, 100);
            HiResDate ender = new HiResDate(0, 200);
            range.resetRange(starter, ender);

            assertEquals("min val set", 0, _min);
            assertEquals("max val set", 100, _max);
            assertEquals("sml tick set", 1, _smallTick);
            assertEquals("drag size set", 0, _dragSize);
            assertEquals("large tick set", 1000, _largeTick);

            assertTrue("slider should be enabled", _enabled);

            // ok, see how the transfer goes
            HiResDate newToSlider = new HiResDate(0, 130);
            final int res = range.toSliderUnits(newToSlider);
            assertEquals("correct to slider units", 30, res);

            // and backwards
            newToSlider = range.fromSliderUnits(res, 1000);
            assertEquals("correct from slider units", 130, newToSlider.getMicros());

            // right, now back to millis
            final Calendar cal = new GregorianCalendar();

            cal.set(2005, 3, 3, 12, 1, 1);
            final Date starterD = cal.getTime();

            cal.set(2005, 3, 12, 12, 1, 1);
            final Date enderD = cal.getTime();

            starter = new HiResDate(starterD.getTime());
            ender = new HiResDate(enderD.getTime());
            range.resetRange(starter, ender);

            final long diff = (enderD.getTime() - starterD.getTime()) / 1000;
            assertEquals("correct range in secs", diff, _max);
            assertEquals("sml tick set", 1, _smallTick);
            assertEquals("large tick set", 60000, _largeTick);

        }

    }

    protected class WheelMovedEvent implements Listener {
        public void handleEvent(final Event event) {
            // find out what keys are pressed
            final int keys = event.stateMask;

            // is is the control button?
            if ((keys & SWT.CTRL) != 0) {
                final double zoomFactor;
                // decide if we're going in or out
                if (event.count > 0)
                    zoomFactor = 0.9;
                else
                    zoomFactor = 1.1;

                // and request the zoom
                doZoom(zoomFactor);
            } else {
                // right, we're not zooming, we must be time-stepping
                final int count = event.count;
                boolean fwd;
                STEP_SIZE size = STEP_SIZE.NORMAL;

                if ((keys & SWT.SHIFT) != 0)
                    size = STEP_SIZE.LARGE;
                else if ((keys & SWT.ALT) != 0)
                    size = STEP_SIZE.SMALL;

                if (count < 0)
                    fwd = true;
                else
                    fwd = false;

                processClick(size, fwd);
            }
        }
    }

    public void addSelectionChangedListener(final ISelectionChangedListener listener) {
        if (_selectionListeners == null)
            _selectionListeners = new Vector<ISelectionChangedListener>(0, 1);

        // see if we don't already contain it..
        if (!_selectionListeners.contains(listener))
            _selectionListeners.add(listener);
    }

    /**
     * zoom the plot (in response to a control-mouse drag)
     * 
     * @param zoomFactor
     */
    public void doZoom(final double zoomFactor) {
        // ok, get the plot, and do some zooming
        if (_currentEditor instanceof PlotEditor) {
            final PlotEditor plot = (PlotEditor) _currentEditor;
            plot.getChart().getCanvas().getProjection().zoom(zoomFactor);
            plot.getChart().update();
        }

    }

    public ISelection getSelection() {
        return null;
    }

    /**
     * accessor to the slider (used for testing the view)
     * 
     * @return the slider control
     */
    public DTGBiSlider getPeriodSlider() {
        return _dtgRangeSlider;
    }

    public Scale getTimeSlider() {
        return _tNowSlider;
    }

    public void removeSelectionChangedListener(final ISelectionChangedListener listener) {
        _selectionListeners.remove(listener);
    }

    public void setSelection(final ISelection selection) {
    }

    /**
     * ok - put in the stepper mode buttons - and any others we think of.
     */
    void populateDropDownList(final LayerPainterManager myLayerPainterManager) {
        // clear the list
        final IMenuManager menuManager = getViewSite().getActionBars().getMenuManager();
        final IToolBarManager toolManager = getViewSite().getActionBars().getToolBarManager();

        // ok, remove the existing items
        menuManager.removeAll();
        toolManager.removeAll();

        // create a host for when we're populating the properties window
        final ISelectionProvider provider = this;

        // right, do we have something with editable layer details?
        if (myLayerPainterManager != null) {

            // ok - add the painter selectors/editors
            createPainterOptions(myLayerPainterManager, menuManager, toolManager, provider);

            // ok, let's have a separator
            toolManager.add(new Separator());

            // now add the highlighter options/editors
            createHighlighterOptions(myLayerPainterManager, menuManager, provider);

            // and another separator
            menuManager.add(new Separator());
        }

        // add the list of DTG formats for the DTG slider
        addDateFormats(menuManager);

        // add the list of DTG formats for the DTG slider
        addBiSliderResolution(menuManager);

        // and another separator
        menuManager.add(new Separator());

        // and another separator
        toolManager.add(new Separator());

        // let user indicate whether we should be filtering to window
        _filterToSelectionAction = new Action("Filter to period", Action.AS_CHECK_BOX) {

            @Override
            public void run() {
                super.run();
                if (isChecked()) {
                    final HiResDate tNow = _myTemporalDataset.getTime();
                    if (tNow != null) {
                        TimePeriod period = _controllablePeriod.getPeriod();
                        if (period != null) {
                            if (!period.contains(tNow)) {
                                if (tNow.greaterThan(period.getEndDTG())) {
                                    fireNewTime(period.getEndDTG());
                                } else {
                                    fireNewTime(period.getStartDTG());
                                }
                            }
                            stopPlayingTimer();
                        }
                    }
                }
            }

        };
        _filterToSelectionAction.setImageDescriptor(CorePlugin.getImageDescriptor(ICON_FILTER_TO_PERIOD));
        _filterToSelectionAction.setToolTipText("Filter plot data to selected time period");
        menuManager.add(_filterToSelectionAction);
        toolManager.add(_filterToSelectionAction);

        // and another separator
        menuManager.add(new Separator());

        // now the add-bookmark item
        final Action _setAsBookmarkAction = new Action("Add DTG as bookmark", Action.AS_PUSH_BUTTON) {
            public void runWithEvent(final Event event) {
                addMarker();
            }
        };
        _setAsBookmarkAction.setImageDescriptor(CorePlugin.getImageDescriptor(ICON_BKMRK_NAV));
        _setAsBookmarkAction.setToolTipText("Add this DTG to the list of bookmarks");
        _setAsBookmarkAction.setId(OP_LIST_MARKER_ID); // give it an id, so we can
        menuManager.add(_setAsBookmarkAction);
        // refer to this later on.

        // and another separator
        menuManager.add(new Separator());

        // now our own menu editor
        final Action toolboxProperties = new Action("Edit Time controller properties", Action.AS_PUSH_BUTTON) {
            public void runWithEvent(final Event event) {
                editMeInProperties(_myStepperProperties);
            }
        };
        toolboxProperties.setToolTipText("Edit Time Controller properties");
        toolboxProperties.setImageDescriptor(CorePlugin.getImageDescriptor(ICON_PROPERTIES));

        menuManager.add(toolboxProperties);
        toolManager.add(toolboxProperties);

        // and sort out our specific items
        refreshTimeOperations();

        // and the help link
        menuManager.add(new Separator());
        menuManager.add(CorePlugin.createOpenHelpAction("org.mwc.debrief.help.TimeController", null, this));

        // ok - get the action bars to re-populate themselves, otherwise we
        // don't
        // see our changes
        getViewSite().getActionBars().updateActionBars();
    }

    /**
     * @param myLayerPainterManager
     * @param menuManager
     * @param provider
     */
    private void createHighlighterOptions(final LayerPainterManager myLayerPainterManager,
            final IMenuManager menuManager, final ISelectionProvider provider) {
        // right, first the drop-down for the display-er
        // ok, second menu for the DTG formats
        final MenuManager highlighterMenu = new MenuManager("Highlight Mode");

        // and store it
        menuManager.add(highlighterMenu);

        // and the range highlighters
        final SWTPlotHighlighter[] highlighterList = myLayerPainterManager.getHighlighterList();
        final String curHighlighterName = myLayerPainterManager.getCurrentHighlighter().getName();

        // add the items
        for (int i = 0; i < highlighterList.length; i++) {
            // ok, next painter
            final SWTPlotHighlighter highlighter = highlighterList[i];

            // create an action for it
            final Action thisOne = new Action(highlighter.toString(), Action.AS_RADIO_BUTTON) {
                public void runWithEvent(final Event event) {
                    myLayerPainterManager.setCurrentHighlighter(highlighter);

                    // and redo this list (deferred until the current processing
                    // is complete...
                    Display.getDefault().asyncExec(new Runnable() {
                        public void run() {
                            populateDropDownList(myLayerPainterManager);
                        }
                    });

                }
            };
            String descPath = "icons/" + highlighter.toString().toLowerCase() + ".png";
            descPath = descPath.replace(" ", "_");
            thisOne.setImageDescriptor(org.mwc.debrief.core.DebriefPlugin.getImageDescriptor(descPath));

            // hmm, and see if this is our current painter
            if (highlighter.getName().equals(curHighlighterName))
                thisOne.setChecked(true);

            // and store it on both menus
            highlighterMenu.add(thisOne);
        }

        // ok, now for the current highlighter
        final SWTPlotHighlighter currentHighlighter = myLayerPainterManager.getCurrentHighlighter();

        // create an action for it
        final IWorkbenchPart myPart = this;
        final Action highlighterProperties = new Action("Edit current highlighter:" + currentHighlighter.getName(),
                Action.AS_PUSH_BUTTON) {
            public void runWithEvent(final Event event) {
                // ok - get the info object for this painter
                if (currentHighlighter.hasEditor()) {
                    final EditableWrapper pw = new EditableWrapper(currentHighlighter, _myLayers);
                    CorePlugin.editThisInProperties(_selectionListeners, new StructuredSelection(pw), provider,
                            myPart);
                }
            }
        };
        highlighterProperties.setImageDescriptor(CorePlugin.getImageDescriptor(ICON_PROPERTIES));

        // and store it on both menus
        highlighterMenu.add(highlighterProperties);
    }

    /**
     * @param myLayerPainterManager
     * @param menuManager
     * @param toolManager
     * @param provider
     */
    private void createPainterOptions(final LayerPainterManager myLayerPainterManager,
            final IMenuManager menuManager, final IToolBarManager toolManager, final ISelectionProvider provider) {
        // right, first the drop-down for the display-er
        // ok, second menu for the DTG formats
        MenuManager displayMenu = new MenuManager("Display Mode");

        // and store it
        menuManager.add(displayMenu);

        // ok, what are the painters we know about
        final TemporalLayerPainter[] painterList = myLayerPainterManager.getPainterList();

        // add the items
        for (int i = 0; i < painterList.length; i++) {
            // ok, next painter
            final TemporalLayerPainter painter = painterList[i];

            // create an action for it
            final Action changePainter = new Action(painter.toString(), Action.AS_RADIO_BUTTON) {
                public void runWithEvent(final Event event) {
                    myLayerPainterManager.setCurrentPainter(painter);

                    // and redo this list (deferred until the current processing
                    // is complete...
                    Display.getDefault().asyncExec(new Runnable() {
                        public void run() {
                            populateDropDownList(myLayerPainterManager);
                        }
                    });
                }
            };
            final String descPath = "icons/16/" + painter.toString().toLowerCase() + ".png";
            changePainter.setImageDescriptor(org.mwc.debrief.core.DebriefPlugin.getImageDescriptor(descPath));

            // hmm, and see if this is our current painter
            if (painter.getName().equals(myLayerPainterManager.getCurrentPainter().getName())) {
                changePainter.setChecked(true);
            }

            // and store it on both menus
            displayMenu.add(changePainter);
            toolManager.add(changePainter);
        }

        // put the display painter property editor into this one
        final TemporalLayerPainter currentPainter = myLayerPainterManager.getCurrentPainter();
        // create an action for it
        final IWorkbenchPart myPart = this;
        final Action currentPainterProperties = new Action("Edit current painter:" + currentPainter.getName(),
                Action.AS_PUSH_BUTTON) {
            public void runWithEvent(final Event event) {
                // ok - get the info object for this painter
                if (currentPainter.hasEditor()) {
                    final EditableWrapper pw = new EditableWrapper(currentPainter, _myLayers);
                    CorePlugin.editThisInProperties(_selectionListeners, new StructuredSelection(pw), provider,
                            myPart);
                }
            }
        };
        currentPainterProperties.setImageDescriptor(CorePlugin.getImageDescriptor(ICON_PROPERTIES));

        // and store it on both menus
        displayMenu.add(currentPainterProperties);

        // lastly, sort out the relative projection mode
        // right, first the drop-down for the display-er
        // ok, second menu for the DTG formats
        displayMenu = new MenuManager("Plotting mode");

        // and store it
        menuManager.add(displayMenu);

        _normalPlottingMode = new Action("Normal", Action.AS_RADIO_BUTTON) {
            @Override
            public void run() {
                setRelativeMode(false, false);
            }
        };
        _normalPlottingMode.setImageDescriptor(TimeControllerPlugin.getImageDescriptor(ICON_LOCK_VIEW));
        displayMenu.add(_normalPlottingMode);

        _primaryCentredNorthOrientedPlottingMode = new Action("Primary centred/North oriented",
                Action.AS_RADIO_BUTTON) {
            @Override
            public void run() {
                // see if we have a primary track...
                if (_myTrackProvider != null) {
                    if (_myTrackProvider.getPrimaryTrack() == null) {
                        CorePlugin.showMessage("Primary Centred Plotting",
                                "A Primary Track must be specified to use this mode");
                        _normalPlottingMode.setChecked(true);
                        _primaryCentredNorthOrientedPlottingMode.setChecked(false);
                    } else
                        setRelativeMode(true, false);
                }
            }
        };
        _primaryCentredNorthOrientedPlottingMode
                .setImageDescriptor(TimeControllerPlugin.getImageDescriptor(ICON_LOCK_VIEW1));
        displayMenu.add(_primaryCentredNorthOrientedPlottingMode);

        _primaryCentredPrimaryOrientedPlottingMode = new Action("Primary centred/Primary oriented",
                Action.AS_RADIO_BUTTON) {

            @Override
            public void run() {
                setRelativeMode(true, true);
            }
        };
        _primaryCentredPrimaryOrientedPlottingMode
                .setImageDescriptor(TimeControllerPlugin.getImageDescriptor(ICON_LOCK_VIEW2));
        // no, let's not offer primary centred, primary oriented view
        // displayMenu.add(_primaryCentredPrimaryOrientedPlottingMode);

    }

    private void setRelativeMode(final boolean primaryCentred, final boolean primaryOriented) {
        _targetProjection.setRelativeMode(primaryCentred, primaryOriented);
        // and trigger redraw
        final IWorkbench wb = PlatformUI.getWorkbench();
        final IWorkbenchWindow win = wb.getActiveWorkbenchWindow();
        final IWorkbenchPage page = win.getActivePage();
        final IEditorPart editor = page.getActiveEditor();
        if (editor instanceof CorePlotEditor) {
            final CorePlotEditor plot = (CorePlotEditor) editor;
            plot.update();
        }

    }

    /**
     * @param menuManager
     */
    private void addDateFormats(final IMenuManager menuManager) {
        // ok, second menu for the DTG formats
        final MenuManager formatMenu = new MenuManager("DTG Format");

        // and store it
        menuManager.add(formatMenu);

        // and now the date formats
        final String[] formats = DateFormatPropertyEditor.getTagList();
        for (int i = 0; i < formats.length; i++) {
            final String thisFormat = formats[i];

            // the properties manager is expecting the integer index of the new
            // format, not the string value.
            // so store it as an integer index
            final Integer thisIndex = new Integer(i);

            // and create a new action to represent the change
            final Action newFormat = new Action(thisFormat, Action.AS_RADIO_BUTTON) {
                public void run() {
                    super.run();
                    _myStepperProperties.setPropertyValue(TimeControlProperties.DTG_FORMAT_ID, thisIndex);

                    // todo: we need to tell the plot that it's changed - fake
                    // this by
                    // firing a quick formatting change
                    _myLayers.fireReformatted(null);

                }

            };
            formatMenu.add(newFormat);
        }
    }

    /**
     * @param menuManager
     */
    private void addBiSliderResolution(final IMenuManager menuManager) {
        // ok, second menu for the DTG formats
        final MenuManager formatMenu = new MenuManager("Time slider increment");

        // and store it
        menuManager.add(formatMenu);

        // and now the date formats
        final Object[][] stepSizes = { { "1 sec", new Long(1000) }, { "1 min", new Long(60 * 1000) },
                { "5 min", new Long(5 * 60 * 1000) }, { "15 min", new Long(15 * 60 * 1000) },
                { "1 hour", new Long(60 * 60 * 1000) }, };

        for (int i = 0; i < stepSizes.length; i++) {

            final String sizeLabel = (String) stepSizes[i][0];
            final Long thisSize = (Long) stepSizes[i][1];

            // and create a new action to represent the change
            final Action newFormat = new Action(sizeLabel, Action.AS_RADIO_BUTTON) {
                public void run() {
                    super.run();
                    _dtgRangeSlider.setStepSize(thisSize.longValue());

                    // ok, update the ranges of the slider
                    _dtgRangeSlider.updateOuterRanges(_myTemporalDataset.getPeriod());
                }

            };
            formatMenu.add(newFormat);
        }
    }

    protected void expandTimeSliderRangeToFull() {
        // hey - check we've got some data first....
        if (_myTemporalDataset != null) {
            final TimePeriod period = _myTemporalDataset.getPeriod();

            // do we know our period?
            if (period != null) {
                _slideManager.resetRange(period.getStartDTG(), period.getEndDTG());
            }
        }
    }

    protected void addMarker() {
        try {
            // right, do we have an editor with a file?
            final IEditorInput input = _currentEditor.getEditorInput();
            if (input instanceof IFileEditorInput) {
                // aaah, and is there a file present?
                final IFileEditorInput ife = (IFileEditorInput) input;
                final IResource file = ife.getFile();
                final String currentText = _timeLabel.getText();
                final long tNow = _myTemporalDataset.getTime().getMicros();
                if (file != null) {
                    // yup, get the description
                    final InputDialog inputD = new InputDialog(getViewSite().getShell(), "Add bookmark at this DTG",
                            "Enter description of this bookmark", currentText, null);
                    inputD.open();

                    final String content = inputD.getValue();
                    if (content != null) {
                        final IMarker marker = file.createMarker(IMarker.BOOKMARK);
                        final Map<String, Object> attributes = new HashMap<String, Object>(4);
                        attributes.put(IMarker.MESSAGE, content);
                        attributes.put(IMarker.LOCATION, currentText);
                        attributes.put(IMarker.LINE_NUMBER, "" + tNow);
                        attributes.put(IMarker.USER_EDITABLE, Boolean.FALSE);
                        marker.setAttributes(attributes);
                    }
                }

            }
        } catch (final CoreException e) {
            e.printStackTrace();
        }

    }

    /**
     * convenience class to help us manage the fwd/bwd step buttons
     * 
     * issue https://github.com/debrief/debrief/issues/710
     * 
     * @author Ian.Mayo
     * @author snpe
     */
    private class RepeatingTimeButtonListener implements Listener {
        private static final long DELAY = 300;

        protected final boolean _fwd;

        protected final STEP_SIZE _step;

        private boolean _doRepeat;

        private java.util.Timer _timer;

        /**
         * 
         * @param fwd whether this button moves forward or backwards
         * @param large whether this button moves in large or small steps
         * @param doRepeat whether this button repeats if held down
         */
        public RepeatingTimeButtonListener(final boolean fwd, final STEP_SIZE step, final boolean doRepeat) {
            _fwd = fwd;
            _step = step;
            _doRepeat = doRepeat;
        }

        public void process() {
            try {
                processClick(_step, _fwd);
            } catch (final RuntimeException e1) {
                CorePlugin.logError(Status.ERROR, "Failed when trying to time step", e1);
                purge();
            }
        }

        public void handleEvent(Event event) {
            int type = event.type;
            if (type == SWT.MouseDown) {
                // clear any previous timers
                purge();

                // fire a single step
                process();

                // do we need a repeating behaviour?
                if (_doRepeat) {
                    // initiate a timer to give repeating behaviour
                    _timer = new java.util.Timer();
                    TimerTask task = new TimerTask() {
                        @Override
                        public void run() {
                            // fire a single step
                            process();
                        }
                    };
                    _timer.scheduleAtFixedRate(task, DELAY, DELAY);
                }
            }
            if (type == SWT.MouseUp) {
                purge();
            }
        }

        /** clear any active timer
         * 
         */
        public void purge() {
            if (_timer != null) {
                _timer.cancel();
                _timer.purge();
                _timer = null;
            }
        }
    }

    // /////////////////////////////////////////////////////////////////
    // AND PROPERTY EDITORS FOR THE
    // /////////////////////////////////////////////////////////////////

    public void setFocus() {
        // ok - put the cursor on the time sldier
        if (!_tNowSlider.isDisposed()) {
            _tNowSlider.setFocus();
        }

    }

    /**
     * @param memento
     */
    public void saveState(final IMemento memento) {

        super.saveState(memento);

        // // ok, store me bits
        // start off with the time step
        memento.putInteger(SLIDER_STEP_SIZE, (int) _dtgRangeSlider.getStepSize());

        // first the
    }

    /**
     * @param site
     * @param memento
     * @throws PartInitException
     */
    public void init(final IViewSite site, final IMemento memento) throws PartInitException {

        super.init(site, memento);

        if (memento != null) {

            // try the slider step size
            final Integer stepSize = memento.getInteger(SLIDER_STEP_SIZE);
            if (stepSize != null) {
                _defaultSliderResolution = stepSize;
            }
        }
    }

    /**
     * provide the currently selected period
     * 
     * @return
     */
    public TimePeriod getPeriod() {
        return getPeriodSlider().getPeriod();
    }

    /**
     * provide some support for external testing
     */
    public void doTests() {
        // check we have some data
        TestCase.assertNotNull("check we have time to control", _controllableTime);
        TestCase.assertNotNull("check we have time provider", _myTemporalDataset);
        TestCase.assertNotNull("check we have period to control", _controllablePeriod);

        Object oldDtgFormat = _myStepperProperties.getPropertyValue(TimeControlProperties.DTG_FORMAT_ID);

        try {
            _myStepperProperties.setPropertyValue(TimeControlProperties.DTG_FORMAT_ID, new Integer(4));

            final HiResDate tDemanded = new HiResDate(0, 818748000000000L);
            // note - time equates to: 120600:00

            // ok, try stepping forward. get the current time
            final HiResDate tNow = _myTemporalDataset.getTime();

            // step forward one
            final Event ev = new Event();
            _forwardButton.notifyListeners(SWT.Selection, ev);

            // find the new time
            final HiResDate tNew = _myTemporalDataset.getTime();

            TestCase.assertNotSame("time has changed", "" + tNew.getMicros(), "" + tNow.getMicros());

            // ok, go back to the demanded time (in case we loaded the plot with a
            // different saved time)
            _controllableTime.setTime(new Integer(111), tDemanded, true);

            // have a look at the date
            final String timeStr = _timeLabel.getText();

            // check it's what we're expecting
            TestCase.assertEquals("time is correct", timeStr, "120600:00");

        } finally {
            _myStepperProperties.setPropertyValue(TimeControlProperties.DTG_FORMAT_ID, oldDtgFormat);
        }
    }

    // /////////////////////////////////////////////////
    // RELATIVE PROJECTION-RELATED BITS
    // /////////////////////////////////////////////////

    public double getHeading() {
        double res = 0;
        if (_relativeProjector != null)
            res = _relativeProjector.getHeading();
        return res;
    }

    public WorldLocation getLocation() {
        WorldLocation res = null;
        if (_relativeProjector != null)
            res = _relativeProjector.getLocation();
        return res;
    }

    @SuppressWarnings("rawtypes")
    @Override
    public Object getAdapter(final Class adapter) {
        Object res = null;
        if (adapter == TimePeriod.class) {
            // NOTE: xy plot plugin relies on getting this time period value from the
            // time controller
            res = getPeriod();
        } else
            res = super.getAdapter(adapter);

        return res;
    }

    public TimeProvider getTimeProvider() {
        return _myTemporalDataset;
    }

}