org.lmn.fc.frameworks.starbase.plugins.observatory.ui.tabs.charts.ChartUIComponent.java Source code

Java tutorial

Introduction

Here is the source code for org.lmn.fc.frameworks.starbase.plugins.observatory.ui.tabs.charts.ChartUIComponent.java

Source

// Copyright 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009,
//           2010, 2011, 2012, 2013, 2014
// Laurence Newell
// starbase@ukraa.com
// radio.telescope@btinternet.com
//
// This file is part of Starbase.
//
// Starbase is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Starbase is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Starbase.  If not, see http://www.gnu.org/licenses.

package org.lmn.fc.frameworks.starbase.plugins.observatory.ui.tabs.charts;

import info.clearthought.layout.TableLayout;
import org.jfree.chart.ChartPanel;
import org.jfree.chart.ChartRenderingInfo;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.plot.PlotRenderingInfo;
import org.jfree.data.time.TimeSeries;
import org.jfree.data.time.TimeSeriesCollection;
import org.jfree.data.time.TimeSeriesDataItem;
import org.jfree.data.xy.XYDataItem;
import org.jfree.data.xy.XYDataset;
import org.jfree.data.xy.XYSeries;
import org.jfree.data.xy.XYSeriesCollection;
import org.lmn.fc.common.constants.ResourceKeys;
import org.lmn.fc.common.datatranslators.DatasetType;
import org.lmn.fc.common.utilities.threads.SwingWorker;
import org.lmn.fc.frameworks.starbase.plugins.observatory.common.ObservatoryConstants;
import org.lmn.fc.frameworks.starbase.plugins.observatory.events.*;
import org.lmn.fc.frameworks.starbase.plugins.observatory.ui.instruments.ObservatoryClockInterface;
import org.lmn.fc.frameworks.starbase.plugins.observatory.ui.instruments.ObservatoryInstrumentDAOInterface;
import org.lmn.fc.frameworks.starbase.plugins.observatory.ui.instruments.ObservatoryInstrumentHelper;
import org.lmn.fc.frameworks.starbase.plugins.observatory.ui.instruments.ObservatoryInstrumentInterface;
import org.lmn.fc.frameworks.starbase.plugins.observatory.ui.instruments.common.MetadataHelper;
import org.lmn.fc.frameworks.starbase.plugins.observatory.ui.instruments.common.ObservatoryUIHelper;
import org.lmn.fc.frameworks.starbase.plugins.observatory.ui.tabs.ChannelSelectionMode;
import org.lmn.fc.frameworks.starbase.plugins.observatory.ui.tabs.ChannelSelectorUIComponentInterface;
import org.lmn.fc.frameworks.starbase.plugins.observatory.ui.tabs.ChartUIComponentPlugin;
import org.lmn.fc.frameworks.starbase.plugins.observatory.ui.tabs.DatasetDomainUIComponentInterface;
import org.lmn.fc.frameworks.starbase.plugins.observatory.ui.tabs.data.DataUpdateType;
import org.lmn.fc.model.logging.Logger;
import org.lmn.fc.model.tasks.TaskPlugin;
import org.lmn.fc.model.xmlbeans.metadata.Metadata;
import org.lmn.fc.ui.UIComponentPlugin;
import org.lmn.fc.ui.components.BlankUIComponent;
import org.lmn.fc.ui.components.UIComponent;
import org.lmn.fc.ui.multipleslider.MThumbSlider;
import org.lmn.fc.ui.multipleslider.MThumbSliderAdditionalUI;
import org.lmn.fc.ui.widgets.ControlKnobInterface;

import javax.swing.*;
import javax.swing.event.ChangeEvent;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.Rectangle2D;
import java.util.*;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

import static org.lmn.fc.frameworks.starbase.plugins.observatory.ui.tabs.DatasetDomainUIComponentInterface.*;

/***************************************************************************************************
 * The base class for all Charts.
 */

public class ChartUIComponent extends UIComponent implements ChartUIComponentPlugin,
        ChannelSelectionChangedListener, DatasetDomainChangedListener, ObservatoryConstants {
    private static final long serialVersionUID = 610799415134903715L;

    // TableLayout row and column size definitions
    private static final double[][] size = { { // Columns
            TableLayout.FILL, TableLayout.PREFERRED },
            { // Rows
                    TableLayout.PREFERRED, TableLayout.FILL, TableLayout.PREFERRED } };

    // TableLayout constraints
    // http://www.clearthought.info/sun/products/jfc/tsc/articles/tablelayout/Cells.html
    // Indicates TableLayoutConstraints's position and justification as a string in the form
    // "column, row, horizontal justification, vertical justification"
    // Valid values: LEFT, RIGHT, CENTER, TOP, BOTTOM
    // The horizontal justification is specified before the vertical justification
    // Multiple Cells
    // A component can also be added to a rectangular set of cells.
    // This is done by specifying the upper, left and lower, right corners of that set.
    // Components that occupy more than one cell will have a size equal to the total area
    // of all cells that component occupies.
    // There is no justification attribute for multi-celled components.

    private static final String[] constraints = { // Column, Row, JustificationX, JustificationY
            "0, 0, 1, 0", // Toolbar (optional)
            "0, 1, CENTER, CENTER", // Chart
            "1, 1, CENTER, TOP", // Channel Selector
            "0, 2, CENTER, CENTER" // Slider
    };

    // Injections
    private final TaskPlugin pluginTask;
    private final ObservatoryInstrumentInterface hostInstrument;
    private String strReportName;
    private final String strResourceKey;
    private DataUpdateType updateType;
    private int intDisplayLimit;
    private boolean boolRefreshable;
    private boolean boolClickRefresh;

    private int intDomainStartPoint;
    private int intDomainEndPoint;
    private double dblRangeCrosshair;
    private double dblDomainCrosshair;

    // Underlying data, passed in from a DAO
    private ObservatoryInstrumentDAOInterface daoChart;
    private List<Metadata> listMetadata;
    private XYDataset xyDatasetPrimary;
    private List<XYDataset> xyDatasetSecondaries;
    private int intChannelCount;
    private boolean boolTemperatureChannel;

    // Chart configuration
    private DatasetType datasetType;
    private boolean boolChartHasLinearMode;
    private boolean boolChartHasLogarithmicMode;
    private boolean boolChartCanAutorange;

    // User Interface
    private final JPanel panelUIContainer;
    private final JPanel panelChartContainer;

    // The dynamic components
    private ChartPanel chartPanel;
    private boolean boolHasChannelSelector;
    private ChannelSelectorUIComponentInterface channelSelectorOccupant;
    private boolean boolHasDatasetDomainControl;
    private DatasetDomainUIComponentInterface datasetDomainControlOccupant;

    // Chart Status
    private boolean boolChannelSelectionChanged;
    private boolean boolDatasetDomainChanged;

    // Refresh Worker
    private SwingWorker workerRefresh;

    // Listeners
    private final Vector<DatasetChangedListener> vecDatasetChangedListeners;

    /***********************************************************************************************
     * Construct a ChartUIComponent with no Toolbar.
     *
     * @param task
     * @param hostinstrument
     * @param name
     * @param resourcekey
     * @param updatetype
     * @param displaylimit
     * @param refreshable
     * @param clickrefresh
     */

    public ChartUIComponent(final TaskPlugin task, final ObservatoryInstrumentInterface hostinstrument,
            final String name, final String resourcekey, final DataUpdateType updatetype, final int displaylimit,
            final boolean refreshable, final boolean clickrefresh) {
        // Create the ChartUIComponent
        // UIComponent has a BorderLayout
        super();

        // Injections
        this.pluginTask = task;
        this.hostInstrument = hostinstrument;
        this.strReportName = name;
        this.strResourceKey = resourcekey;
        this.updateType = updatetype;
        this.intDisplayLimit = displaylimit;
        this.boolRefreshable = refreshable;
        this.boolClickRefresh = clickrefresh;

        this.intDomainStartPoint = 0;
        this.intDomainEndPoint = DOMAIN_SLIDER_MAXIMUM;
        this.dblRangeCrosshair = 0.0;
        this.dblDomainCrosshair = 0.0;

        this.daoChart = null;
        this.listMetadata = null;
        this.xyDatasetPrimary = null;
        this.xyDatasetSecondaries = new ArrayList<XYDataset>(10);
        this.intChannelCount = 0;
        this.boolTemperatureChannel = false;

        this.datasetType = DatasetType.TIMESTAMPED;
        this.boolChartHasLinearMode = true;
        this.boolChartHasLogarithmicMode = false;
        this.boolChartCanAutorange = false;

        // Make the UI and Chart container panels only once
        this.panelUIContainer = new JPanel();
        this.panelChartContainer = new JPanel();
        this.chartPanel = null;

        // Most Charts have a ChannelSelector
        this.boolHasChannelSelector = true;
        this.channelSelectorOccupant = null;
        this.boolChannelSelectionChanged = false;

        // Most Charts have a DatasetDomainControl
        this.boolHasDatasetDomainControl = true;
        this.datasetDomainControlOccupant = null;
        this.boolDatasetDomainChanged = false;

        this.workerRefresh = null;

        this.vecDatasetChangedListeners = new Vector<DatasetChangedListener>(10);
    }

    /**********************************************************************************************/
    /* UI State                                                                                   */
    /***********************************************************************************************
     * Initialise the Chart.
     */

    public synchronized void initialiseUI() {
        final String SOURCE = "ChartUIComponent.initialiseUI() ";

        super.initialiseUI();

        LOGGER.debug(LOADER_PROPERTIES.isChartDebug(), SOURCE + "[uistate=" + getUIState().getName() + "]");

        setRangeCrosshair(0.0);
        setDomainCrosshair(0.0);

        // Remove any previous components and start with a blank panel UIComponent
        removeAll();
        setBackground(UIComponentPlugin.DEFAULT_COLOUR_CANVAS.getColor());
        setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2));

        getUIContainer().setLayout(new TableLayout(size));
        getUIContainer().setBackground(DEFAULT_COLOUR_CANVAS.getColor());

        getChartContainer().removeAll();
        getChartContainer().setLayout(new BorderLayout());
        getChartContainer().setBackground(DEFAULT_COLOUR_CANVAS.getColor());
        // Make sure that the Chart uses all available space...
        getChartContainer().setPreferredSize(new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE));
        getChartContainer().add(new BlankUIComponent(MSG_WAITING_FOR_DATA, DEFAULT_COLOUR_CANVAS, COLOUR_INFO_TEXT),
                BorderLayout.CENTER);

        // There may not be a Toolbar
        if (getToolBar() != null) {
            getUIContainer().add(getToolBar(), constraints[0]);
        }

        getUIContainer().add(getChartContainer(), constraints[1]);

        // Start with no Chart or container
        // The refresh Thread will create what's needed
        LOGGER.debug(LOADER_PROPERTIES.isChartDebug(), SOURCE + "setChartPanel(NULL)");
        setChartPanel(null);

        // Set up the ChannelSelector
        // It will be empty until an XYDataset is loaded
        setChannelSelectorOccupant(new ChannelSelectorUIComponent(getHostInstrument(), this, getMetadata(),
                getUpdateType(), getResourceKey()));
        getChannelSelectorOccupant().initialiseUI();
        getChannelSelectorOccupant().addChannelSelectionChangedListener(this);

        if (hasChannelSelector()) {
            getUIContainer().add((Component) getChannelSelectorOccupant(), constraints[2]);
        }

        setDatasetDomainControlOccupant(new DatasetDomainUIComponent(getHostInstrument(), this, getMetadata(),
                getUpdateType(), getResourceKey()));
        getDatasetDomainControlOccupant().initialiseUI();
        getDatasetDomainControlOccupant().addDatasetDomainChangedListener(this);
        setDatasetDomainStartPoint(DOMAIN_SLIDER_MINIMUM);
        setDatasetDomainEndPoint(DOMAIN_SLIDER_MAXIMUM);

        if (hasDatasetDomainControl()) {
            getUIContainer().add((Component) getDatasetDomainControlOccupant(), constraints[3]);
        }

        // Consume all area on the underlying UIComponent
        add(getUIContainer(), BorderLayout.CENTER);

        // Set up for mouse click refresh, or none
        setRefreshType();
    }

    /***********************************************************************************************
     * Run the UI of this Chart.
     * Refresh the data and start the Timer, if any.
     * Usually called in RootData.runUI().
     */

    public synchronized void runUI() {
        final String SOURCE = "ChartUIComponent.runUI() ";

        super.runUI();

        LOGGER.debug(LOADER_PROPERTIES.isChartDebug(), SOURCE + "[uistate=" + getUIState().getName() + "]");

        // These don't do anything useful for now!
        if (getChannelSelectorOccupant() != null) {
            getChannelSelectorOccupant().runUI();
        }

        if (getDatasetDomainControlOccupant() != null) {
            getDatasetDomainControlOccupant().runUI();

            // However, we can make sure the slider data are up to date now we are running
            if (getDatasetDomainControlOccupant().getDatasetDomainSlider() != null) {
                setDatasetDomainStartPoint(
                        getDatasetDomainControlOccupant().getDatasetDomainSlider().getValueAt(INDEX_LEFT));
                setDatasetDomainEndPoint(
                        getDatasetDomainControlOccupant().getDatasetDomainSlider().getValueAt(INDEX_RIGHT));
            }
        }

        // Refresh the Chart using the current state of the Instrument, DAO and Chart
        refreshChart(getDAO(), true, SOURCE);
    }

    /***********************************************************************************************
     * Stop the UI of this Chart.
     * Usually called in RootData.stopUI().
     */

    public synchronized void stopUI() {
        final String SOURCE = "ChartUIComponent.stopUI() ";

        // These don't do anything useful for now!
        if (getChannelSelectorOccupant() != null) {
            getChannelSelectorOccupant().stopUI();
        }

        if (getDatasetDomainControlOccupant() != null) {
            getDatasetDomainControlOccupant().stopUI();
        }

        // Neither does this
        super.stopUI();

        LOGGER.debug(LOADER_PROPERTIES.isChartDebug(), SOURCE + "[uistate=" + getUIState().getName() + "]");
    }

    /***********************************************************************************************
     * Dispose of all UI components and remove the Toolbar Actions.
     */

    public synchronized void disposeUI() {
        final String SOURCE = "ChartUIComponent.disposeUI() ";

        SwingWorker.disposeWorker(workerRefresh, true, SWING_WORKER_STOP_DELAY);
        workerRefresh = null;

        clearUIComponentContextActionGroups();

        // Remove all UIComponents...
        getChartContainer().removeAll();

        LOGGER.debug(LOADER_PROPERTIES.isChartDebug(), SOURCE + "setChartPanel(NULL)");
        setChartPanel(null);

        if (getChannelSelectorOccupant() != null) {
            getChannelSelectorOccupant().removeChannelSelectionChangedListener(this);
            getChannelSelectorOccupant().disposeUI();
        }

        if (getDatasetDomainControlOccupant() != null) {
            getDatasetDomainControlOccupant().removeDatasetDomainChangedListener(this);
            getDatasetDomainControlOccupant().disposeUI();
        }

        super.disposeUI();

        LOGGER.debug(LOADER_PROPERTIES.isChartDebug(), SOURCE + "[uistate=" + getUIState().getName() + "]");
    }

    /***********************************************************************************************
     * Remove any Data associated with this UIComponent's appearance on the UI.
     * For instance, remove a Chart regardless of it being visible.
     */

    public void removeUIIdentity() {
        final String SOURCE = "ChartUIComponent.removeUIIdentity() ";

        super.removeUIIdentity();

        if ((getChannelSelectorOccupant() != null)
                && (getChannelSelectorOccupant().getSelectorContainer() != null)) {
            getChannelSelectorOccupant().getSelectorContainer().removeAll();
            getChannelSelectorOccupant().getSelectorContainer().revalidate();
        }

        if ((getDatasetDomainControlOccupant() != null)
                && (getDatasetDomainControlOccupant().getDatasetDomainContainer() != null)) {
            getDatasetDomainControlOccupant().getDatasetDomainContainer().removeAll();
            getDatasetDomainControlOccupant().getDatasetDomainContainer().revalidate();
        }

        LOGGER.debug(LOADER_PROPERTIES.isChartDebug(), SOURCE + "setChartPanel(NULL)");
        setChartPanel(null);

        if (getChartContainer() != null) {
            getChartContainer().removeAll();
            getChartContainer().add(
                    new BlankUIComponent(MSG_WAITING_FOR_DATA, DEFAULT_COLOUR_CANVAS, COLOUR_INFO_TEXT),
                    BorderLayout.CENTER);
            getChartContainer().revalidate();
        }

        revalidate();

        LOGGER.debug(LOADER_PROPERTIES.isChartDebug(),
                SOURCE + "Cleared Chart container and Chart [uistate=" + getUIState().getName() + "]");
    }

    /**********************************************************************************************/
    /* Chart Creation and Refresh                                                                 */
    /***********************************************************************************************
     * Create or refresh the visible Chart on a separate thread.
     * Only create or update if the Instrument is selected and the Chart tab is visible.
     *
     * @param message
     */

    //    public synchronized void refreshChart(final String message)
    //        {
    //        final String SOURCE = "ChartUIComponent.refreshChart(NO PARAMETERS) ";
    //
    //        if ((getHostInstrument() != null)
    //            && (getHostInstrument().getDAO() != null)
    //            && (getHostInstrument().getDAO().getXYDataset() != null))
    //            {
    //            LOGGER.debug(LOADER_PROPERTIES.isChartDebug(),
    //                         Logger.CONSOLE_SEPARATOR_MAJOR);
    //            LOGGER.debug(LOADER_PROPERTIES.isChartDebug(),
    //                         SOURCE + "DAO [classname=" + getHostInstrument().getDAO().getClass().getName()
    //                                + "] [name=" + getHostInstrument().getDAO().getInstrumentName()
    //                                + "] [crosshair.range=" + getRangeCrosshair()
    //                                + "] [crosshair.domain=" + getDomainCrosshair()
    //                                + "] [channel.count.raw=" + getHostInstrument().getDAO().getRawDataChannelCount()
    //                                + "] [channel.count.xy=" + getHostInstrument().getDAO().getXYDataset().getSeriesCount()
    //                                + "]");
    //            }
    //
    //        doRefreshChart(getHostInstrument().getDAO(),
    //                       (ObservatoryUIHelper.isSelectedInstrument(getHostInstrument()))
    //                        && (UIComponentHelper.isUIComponentShowing(this)),
    //                       message + " --> " + SOURCE);
    //        }

    /***********************************************************************************************
     * Conditionally create or refresh the Chart on a separate thread.
     * Only create or update if asked to do so via the generate flag.
     * Intended for use in exporting Charts which are not currently visible.
     *
     * @param dao
     * @param generateflag
     * @param message
     */

    public synchronized void refreshChart(final ObservatoryInstrumentDAOInterface dao, final boolean generateflag,
            final String message) {
        final String SOURCE = "ChartUIComponent.refreshChart(DAO) ";

        if ((dao != null) && (dao.getXYDataset() != null)) {
            LOGGER.debug(LOADER_PROPERTIES.isChartDebug(), Logger.CONSOLE_SEPARATOR_MAJOR);
            LOGGER.debug(LOADER_PROPERTIES.isChartDebug(),
                    SOURCE + "DAO [classname=" + dao.getClass().getName() + "] [name=" + dao.getInstrumentName()
                            + "] [crosshair.range=" + getRangeCrosshair() + "] [crosshair.domain="
                            + getDomainCrosshair() + "] [channel.count.raw=" + dao.getRawDataChannelCount()
                            + "] [channel.count.xy=" + dao.getXYDataset().getSeriesCount() + "] [generate.flag="
                            + generateflag + "]");
        }

        doRefreshChart(dao, generateflag, message + " --> " + SOURCE);
    }

    /***********************************************************************************************
     * Create or refresh the Chart on a separate thread.
     * Only create or update if asked to do so via the generate flag.
     *
     * @param dao
     * @param generateflag
     * @param message
     */

    private synchronized void doRefreshChart(final ObservatoryInstrumentDAOInterface dao,
            final boolean generateflag, final String message) {
        //final String SOURCE = "ChartUIComponent.refreshChart() ";
        final String SOURCE = message;
        final ChartUIComponentPlugin thisUI;
        final boolean boolDebug;

        boolDebug = true;
        //        boolDebug = LOADER_PROPERTIES.isMetadataDebug()
        //                    || LOADER_PROPERTIES.isChartDebug();

        // For use in inner classes
        thisUI = this;

        // Stop any existing SwingWorker
        if ((getHostInstrument() != null) && (dao != null)) {
            LOGGER.debug(boolDebug, SOURCE + "DAO [dao.classname=" + dao.getClass().getName()
                    + "] [instrument.name=" + dao.getInstrumentName() + "] [generate.flag=" + generateflag + "]");

            LOGGER.debug(boolDebug, SOURCE + "SwingWorker.controlledStop()");
        }

        SwingWorker.disposeWorker(workerRefresh, true, SWING_WORKER_STOP_DELAY >> 1);

        // Fire off another thread to process the Chart
        workerRefresh = new SwingWorker(REGISTRY.getThreadGroup(), SOURCE + "SwingWorker [group="
                + REGISTRY.getThreadGroup().getName() + "] [thread=" + getChartName() + "]") {
            public Object construct() {
                LOGGER.debug(boolDebug, SOURCE
                        + "SwingWorker START -------------------------------------------------------------------------!");

                // The result of this Thread is a JFreeChart
                // which may have been produced in a long process...
                if ((!isStopping()) && (getHostInstrument() != null)
                        && (getHostInstrument().getInstrumentState().isOn())) {
                    try {
                        final JFreeChart chart;
                        final Calendar calObservatory;

                        // Either find the Current Observatory calendar, or provide a default
                        calObservatory = ObservatoryInstrumentHelper
                                .getCurrentObservatoryCalendar(REGISTRY.getFramework(), dao, boolDebug);

                        //-----------------------------------------------------------------------------
                        // Debug the data we are trying to display

                        if (boolDebug) {
                            // Dump the (partial) contents of each Series in the composite XYdataset
                            ChartHelper.dumpXYDataset(boolDebug, calObservatory, getPrimaryXYDataset(), 4, SOURCE
                                    + "Original unmodified Primary XYDataset before channel or domain selection");

                            if ((getSecondaryXYDatasets() != null) && (!getSecondaryXYDatasets().isEmpty())) {
                                for (int intDatasetIndex = 0; intDatasetIndex < getSecondaryXYDatasets()
                                        .size(); intDatasetIndex++) {
                                    ChartHelper.dumpXYDataset(boolDebug, calObservatory,
                                            getSecondaryXYDatasets().get(intDatasetIndex), 4,
                                            SOURCE + "Original unmodified Secondary XYDataset [" + intDatasetIndex
                                                    + "] before channel or domain selection");
                                }
                            } else {
                                LOGGER.debug(boolDebug,
                                        SOURCE + "There are no SecondaryXYDatasets associated with this Chart");
                            }
                        }

                        //-----------------------------------------------------------------------------
                        // Apply a ChartUI and its Metadata to the specified DAO

                        ChartHelper.associateChartUIWithDAO(thisUI, getMetadata(), dao);

                        //-----------------------------------------------------------------------------
                        // Create a new Channel Selector showing the channels of the Primary Dataset

                        if ((getChannelSelectorOccupant() != null)
                                && (getHostInstrument().getInstrumentState().isOn()) && (dao != null)
                                && (generateflag)) {
                            LOGGER.debug(boolDebug,
                                    SOURCE + "SwingWorker --> ChannelSelector --> createOrUpdateSelectors()");

                            // The Channel Selector will be empty until an XYDataset is loaded
                            // Note that ChannelCount etc. must be set before calling createOrUpdateSelectors()
                            getChannelSelectorOccupant().setChannelCount(getChannelCount());
                            getChannelSelectorOccupant().setTemperatureChannel(hasTemperatureChannel());
                            getChannelSelectorOccupant().setMetadata(getMetadata());
                            getChannelSelectorOccupant().setUpdateType(getUpdateType());

                            // Force a rebuild of the Channel Selector only if necessary
                            getChannelSelectorOccupant().createOrUpdateSelectors(getDatasetType(),
                                    getPrimaryXYDataset(), getSecondaryXYDatasets(), dao.isDatasetTypeChanged(),
                                    dao.isChannelCountChanged(), dao.isMetadataChanged(), dao.isRawDataChanged(),
                                    dao.isProcessedDataChanged(), isRefreshable(), isClickRefresh(), boolDebug);
                        } else {
                            // This debug ASSUMES the Instrument is not NULL
                            LOGGER.debug(boolDebug, SOURCE + "Unable to configure the Channel Selector UIComponent"
                                    + "  [has.channelselector=" + (hasChannelSelector())
                                    + "] [channelselector.notnull=" + (getChannelSelectorOccupant() != null)
                                    + "] [isselectedinstrument="
                                    + ObservatoryUIHelper.isSelectedInstrument(getHostInstrument())
                                    + "] [isinstrument.on=" + getHostInstrument().getInstrumentState().isOn()
                                    + "] [dao.notnull=" + (dao != null) + "] [generateflag=" + generateflag + "]");
                        }

                        //-----------------------------------------------------------------------------
                        // Force a rebuild of the DatasetDomain Control only if necessary

                        if ((hasDatasetDomainControl()) && (getDatasetDomainControlOccupant() != null)
                                && (getHostInstrument().getInstrumentState().isOn()) && (dao != null)
                                && (generateflag)) {
                            LOGGER.debug(boolDebug, SOURCE
                                    + "SwingWorker --> DatasetDomainControl --> createOrUpdateDomainControl()");

                            // Note that ChannelCount etc. must be set before calling createOrUpdateDomainControl()
                            getDatasetDomainControlOccupant().setRawDataChannelCount(getChannelCount());
                            getDatasetDomainControlOccupant().setTemperatureChannel(hasTemperatureChannel());
                            getDatasetDomainControlOccupant().setMetadata(getMetadata());
                            getDatasetDomainControlOccupant().setUpdateType(getUpdateType());

                            getDatasetDomainControlOccupant().createOrUpdateDomainControl(getDatasetType(),
                                    getPrimaryXYDataset(), getSecondaryXYDatasets(), dao.isDatasetTypeChanged(),
                                    dao.isChannelCountChanged(), dao.isMetadataChanged(), dao.isRawDataChanged(),
                                    dao.isProcessedDataChanged(), isRefreshable(), isClickRefresh(), boolDebug);
                        } else {
                            // This debug ASSUMES the Instrument is not NULL
                            LOGGER.debug(boolDebug, SOURCE
                                    + "Unable to configure the DatasetDomainControl UIComponent"
                                    + "  [has.domaincontrol=" + (hasDatasetDomainControl())
                                    + "] [domaincontrol.notnull=" + (getDatasetDomainControlOccupant() != null)
                                    + "] [isselectedinstrument="
                                    + ObservatoryUIHelper.isSelectedInstrument(getHostInstrument())
                                    + "] [isinstrument.on=" + getHostInstrument().getInstrumentState().isOn()
                                    + "] [dao.notnull=" + (dao != null) + "] [generateflag=" + generateflag + "]");
                        }

                        // Do this anyway, because it doesn't affect the UI
                        updateSliderLocalValues();

                        //-----------------------------------------------------------------------------
                        // If the Chart does not exist, create it
                        // If the Chart structure has changed, recreate it using the new configuration
                        // If only the data have changed, update the existing JFreeChart
                        // and return NULL here, to avoid redrawing

                        LOGGER.debug(boolDebug, SOURCE + "SwingWorker --> createOrUpdateChart()");

                        // Use the Observatory ResourceKey
                        setDisplayLimit(
                                REGISTRY.getIntegerProperty(getHostInstrument().getHostAtom().getResourceKey()
                                        + ResourceKeys.KEY_DISPLAY_DATA_MAX));

                        // Only create or update if asked to do so
                        // This is the last step, so can reset the DAO changed status flags
                        chart = ChartHelper.createOrUpdateChart(getHostInstrument(), thisUI, dao, generateflag,
                                getDatasetType(), getPrimaryXYDataset(), getSecondaryXYDatasets(), getUpdateType(),
                                isRefreshable(), isClickRefresh(), getDisplayLimit(), getDatasetDomainStartPoint(),
                                getDatasetDomainEndPoint(), getChannelSelectorOccupant(), boolDebug);
                        return (chart);
                    }

                    catch (final Exception exception) {
                        LOGGER.debug(boolDebug, SOURCE + "SwingWorker Thread GENERIC EXCEPTION");
                        exception.printStackTrace();
                        return null;
                    }
                } else {
                    LOGGER.debug(boolDebug,
                            SOURCE + "SwingWorker Thread stopping, or the Instrument has been turned OFF...");
                    return (null);
                }
            }

            // Return a JFreeChart or NULL, depending on the outcome of createOrUpdateChart()
            // If NULL, don't affect the ChartPanel contents
            public void finished() {
                MetadataHelper.showMetadataList(getMetadata(), SOURCE + "Chart Metadata on arrival at finished()",
                        boolDebug);

                // Update the Chart on the Event Dispatching Thread
                // Check thoroughly that there is some point to this update...
                if ((workerRefresh != null) && (workerRefresh.get() != null)
                        && (workerRefresh.get() instanceof JFreeChart) && (SwingUtilities.isEventDispatchThread())
                        && (!isStopping()) && (getHostInstrument() != null)
                        && (getHostInstrument().getInstrumentState().isOn())) {
                    // See if we already have a ChartPanel to hold the new JFreeChart
                    if (getChartPanel() != null) {
                        LOGGER.debug(boolDebug, SOURCE
                                + "finished() Apply the JFreeChart returned by get() to exisiting ChartPanel");

                        getChartPanel().setChart((JFreeChart) workerRefresh.get());
                    } else {
                        // There is NO ChartPanel, so start again from scratch
                        LOGGER.debug(boolDebug, SOURCE
                                + "finished() Create a NEW ChartPanel and apply the JFreeChart returned by get()");

                        setChartPanel(ChartPanelFactory.createChartPanel((JFreeChart) workerRefresh.get()));
                    }

                    // NOTE: Chart applied to the specified DAO which was passed as a parameter
                    LOGGER.debug(boolDebug, SOURCE + "finished() returned JFreeChart on a ChartPanel");

                    getChartContainer().removeAll();
                    getChartContainer().add(getChartPanel(), BorderLayout.CENTER);
                    getChartPanel().getChart().fireChartChanged();
                    getChartContainer().revalidate();
                } else {
                    // We failed to return a JFreeChart for some reason

                    if (getPrimaryXYDataset() == null) {
                        LOGGER.debug(boolDebug, SOURCE
                                + "finished() No JFreeChart returned, Dataset is NULL, show BlankUIComponent");

                        // Getting here with a NULL dataset should mean no Chart!
                        // ChartContainer is always NOT NULL
                        getChartContainer().removeAll();
                        getChartContainer().add(
                                new BlankUIComponent(MSG_WAITING_FOR_DATA, DEFAULT_COLOUR_CANVAS, COLOUR_INFO_TEXT),
                                BorderLayout.CENTER);
                        getChartContainer().revalidate();

                        // ToDo Consider setting DAO Chart and ChartPanel Chart to NULL?
                        //SOURCE + "finished() No JFreeChart returned, Dataset is NULL, set Chart to NULL if possible");

                        //                        if (getChartPanel() != null)
                        //                            {
                        //                            getChartPanel().setChart(null);
                        //                            }
                    } else {
                        // We have some data, if we also have a Chart, just display it again
                        if ((getChartPanel() != null) && (getChartPanel().getChart() != null)) {
                            LOGGER.debug(boolDebug, SOURCE
                                    + "finished() No JFreeChart returned, Dataset is NOT NULL, redraw data on existing ChartPanel");

                            getChartContainer().removeAll();
                            getChartContainer().add(getChartPanel(), BorderLayout.CENTER);
                            getChartPanel().getChart().fireChartChanged();
                            getChartContainer().revalidate();
                        } else {
                            LOGGER.error(SOURCE
                                    + "finished() No JFreeChart returned, Dataset is NOT NULL, but no Chart for display, no action taken");
                        }
                    }
                }

                // Handle the DatasetDomainControl

                if ((getChartPanel() != null) && (hasDatasetDomainControl())
                        && (getDatasetDomainControlOccupant() != null)
                        && (getDatasetDomainControlOccupant().getDatasetDomainContainer() != null)) {
                    final ChartRenderingInfo infoChart;
                    final PlotRenderingInfo infoPlot;
                    final Rectangle2D rectPlot;
                    final Rectangle2D rectData;

                    infoChart = getChartPanel().getChartRenderingInfo();
                    infoPlot = infoChart.getPlotInfo();
                    rectPlot = infoPlot.getPlotArea();
                    rectData = infoPlot.getDataArea();

                    // Trap the cases where the Plot hasn't been rendered, or there's nothing to lay out,
                    // in which cases use the default sizes
                    if ((rectPlot != null) && (rectData != null) && ((int) rectData.getWidth() > 0)
                            && ((int) rectData.getHeight() > 0) && ((int) rectData.getX() > 0)
                            && ((int) rectData.getY() > 0)) {
                        int intLeft;
                        int intRight;

                        // Try to get the slider to align with the extents of the data area
                        // Ideally this should happen also after WindowResizing events
                        intLeft = (int) rectData.getX();
                        intRight = (int) (getChartPanel().getWidth() - rectData.getWidth() - rectData.getX());

                        LOGGER.debug(boolDebug,
                                "DatasetDomainControl -- PANEL, PLOT & DATA AREAS" + "  [panel.width="
                                        + getChartPanel().getWidth() + "] [panel.height="
                                        + getChartPanel().getHeight() + "] [plot.width=" + rectPlot.getWidth()
                                        + "] [plot.height=" + rectPlot.getHeight() + "] [plot.x=" + rectPlot.getX()
                                        + "] [plot.y=" + rectPlot.getY() + "] [data.width=" + rectData.getWidth()
                                        + "] [data.height=" + rectData.getHeight() + "] [data.x=" + rectData.getX()
                                        + "] [data.y=" + rectData.getY() + "] [indent.left=" + intLeft
                                        + "] [indent.right=" + intRight + "]");

                        if (intLeft < 0) {
                            intLeft = 0;
                        }

                        if (intRight < 0) {
                            intRight = 0;
                        }

                        if ((intLeft + rectData.getWidth() + intRight) > getChartPanel().getWidth()) {
                            intRight = 5;
                        }

                        getDatasetDomainControlOccupant().getDatasetDomainContainer()
                                .setBorder(BorderFactory.createEmptyBorder(0, intLeft, 5, intRight));
                    } else {
                        getDatasetDomainControlOccupant().getDatasetDomainContainer()
                                .setBorder(BorderFactory.createEmptyBorder(0, INDENT_LEFT, 5, INDENT_RIGHT));
                    }

                    // Layout the Domain slider control again
                    getDatasetDomainControlOccupant().getDatasetDomainContainer().revalidate();
                }

                LOGGER.debug(boolDebug, SOURCE
                        + "SwingWorker.finished() STOP ---------------------------------------------------------------!\n");
            }
        };

        // Start the Thread we have prepared...
        workerRefresh.start();
    }

    /**********************************************************************************************
     * Create a new Chart and customise the XYPlot, e.g. for fixed range axes.
     *
     * @param datasettype
     * @param primarydataset
     * @param secondarydatasets
     * @param updatetype
     * @param displaylimit
     * @param channelselector
     * @param debug
     *
     * @return JFreeChart
     */

    public JFreeChart createCustomisedChart(final DatasetType datasettype, final XYDataset primarydataset,
            final List<XYDataset> secondarydatasets, final DataUpdateType updatetype, final int displaylimit,
            final ChannelSelectorUIComponentInterface channelselector, final boolean debug) {
        final String SOURCE = "ChartUIComponent.createCustomisedChart() ";

        LOGGER.error(SOURCE + "Chart subclass is missing implementation!");

        return (null);
    }

    /***********************************************************************************************
     * Update the existing Chart referenced by the DAO, by applying the specified datasets,
     * using all existing channel selections and range selections.
     * This implementation ASSUMES a ChannelSelector is present.
     *
     * @param dao
     * @param datasettype
     * @param primarydataset
     * @param secondarydatasets
     * @param displaylimit
     * @param domainstartpoint
     * @param domainendpoint
     * @param channelselector
     * @param debug
     */

    public void updateChartForSelection(final ObservatoryInstrumentDAOInterface dao, final DatasetType datasettype,
            final XYDataset primarydataset, final List<XYDataset> secondarydatasets, final int displaylimit,
            final int domainstartpoint, final int domainendpoint,
            final ChannelSelectorUIComponentInterface channelselector, final boolean debug) {
        final String SOURCE = "ChartUIComponent.updateChartForSelection() ";

        if ((dao != null) && (dao.getChartUI() != null) && (dao.getChartUI().getChartPanel() != null)
                && (dao.getChartUI().getChartPanel().getChart() != null)
                && (dao.getChartUI().getChartPanel().getChart().getXYPlot() != null) && (datasettype != null)
                && (primarydataset != null) && (secondarydatasets != null) && (channelselector != null)) {
            final XYDataset xyNewPrimaryDataset;
            final List<XYDataset> listNewSecondaryDatasets;
            final List<XYDataset> listParentSecondaryDatasetForSeries;
            final Iterator iterOriginalSecondaryDatasets;
            final Calendar calObservatory;

            // Either find the Current Observatory calendar, or provide a default
            calObservatory = ObservatoryInstrumentHelper.getCurrentObservatoryCalendar(REGISTRY.getFramework(), dao,
                    debug);

            // Create a list of NewSecondaryDatasets for display
            // Add an appropriate *empty* new dataset for every one existing in the secondary set
            // and record the parent new Dataset for each Series in each original Dataset
            // So much complexity just to handle the very few cases of secondary datasets...

            listNewSecondaryDatasets = new ArrayList<XYDataset>(10);
            listParentSecondaryDatasetForSeries = new ArrayList<XYDataset>(10);

            iterOriginalSecondaryDatasets = secondarydatasets.iterator();

            while (iterOriginalSecondaryDatasets.hasNext()) {
                final XYDataset xyDatasetOriginal;

                xyDatasetOriginal = (XYDataset) iterOriginalSecondaryDatasets.next();

                if ((xyDatasetOriginal != null) && (xyDatasetOriginal.getSeriesCount() > 0)) {
                    final XYDataset xyDatasetNew;

                    // Create an empty Dataset to correspond with the original
                    if (datasettype.getName().equals(DatasetType.XY.getName())) {
                        xyDatasetNew = new XYSeriesCollection();
                        listNewSecondaryDatasets.add(xyDatasetNew);
                    } else if (datasettype.getName().equals(DatasetType.TIMESTAMPED.getName())) {
                        xyDatasetNew = new TimeSeriesCollection(calObservatory.getTimeZone());
                        listNewSecondaryDatasets.add(xyDatasetNew);
                    } else {
                        xyDatasetNew = new XYSeriesCollection();
                        listNewSecondaryDatasets.add(xyDatasetNew);
                    }

                    // Record the *same* parent Dataset for each Series in each original Secondary Dataset
                    // This creates a List in channel order, but with references to Datasets not Series
                    for (int i = 0; i < xyDatasetOriginal.getSeriesCount(); i++) {
                        listParentSecondaryDatasetForSeries.add(xyDatasetNew);
                    }
                }
            }

            // Confirm the DatasetType
            if ((datasettype.getName().equals(DatasetType.XY.getName()))
                    && (primarydataset instanceof XYSeriesCollection)) {
                final XYSeriesCollection collectionPrimary;

                // Prepare a new XYSeriesCollection for display
                xyNewPrimaryDataset = new XYSeriesCollection();

                // There should be a collection of <channelcount> XYSeries in the Primary Dataset
                // but there may also be some in the Secondary Datasets
                collectionPrimary = (XYSeriesCollection) primarydataset;

                // ToDo Make this work for ChannelSelector NULL, get channel count from primary and secondaries? Set mode to X1?

                if ((collectionPrimary.getSeriesCount() > 0) && (collectionPrimary.getSeries() != null)
                        && (channelselector.getChannelSelectionModes() != null)) {
                    LOGGER.debug(debug, SOURCE + "XY domainstartpoint=" + domainstartpoint + " domainendpoint="
                            + domainendpoint);

                    ChartHelper.dumpXYDataset(debug, calObservatory, collectionPrimary, 4,
                            SOURCE + "XYSeriesCollection --> Original Dataset");

                    // Find which channels and data range to use this time round
                    for (int intChannelIndex = 0; intChannelIndex < channelselector.getChannelSelectionModes()
                            .size(); intChannelIndex++) {
                        final ChannelSelectionMode selectionMode;

                        selectionMode = channelselector.getChannelSelectionModes().get(intChannelIndex);

                        if (!ChannelSelectionMode.OFF.equals(selectionMode)) {
                            final XYSeries seriesOriginalData;

                            // We needed to know about the secondary datasets in order to get the correct index
                            seriesOriginalData = (XYSeries) ChartHelper.getSeriesForIndex(datasettype,
                                    primarydataset, secondarydatasets, intChannelIndex, debug);
                            if (seriesOriginalData != null) {
                                final XYSeries seriesChangedData;
                                final List listOriginalDataItems;
                                int intStartIndex;
                                int intEndIndex;

                                listOriginalDataItems = seriesOriginalData.getItems();

                                // Prepare a new Series for the changed data, with the same Key
                                seriesChangedData = new XYSeries(seriesOriginalData.getKey());

                                if (listOriginalDataItems.size() > 0) {
                                    // Map the slider values to data indexes
                                    intStartIndex = ChartHelper.transformDomainSliderValueToSeriesIndex(
                                            ChartUIComponentPlugin.DOMAIN_SLIDER_MINIMUM,
                                            ChartUIComponentPlugin.DOMAIN_SLIDER_MAXIMUM, domainstartpoint,
                                            DatasetDomainUIComponentInterface.INDEX_LEFT,
                                            collectionPrimary.getDomainLowerBound(true),
                                            collectionPrimary.getDomainUpperBound(true), DatasetType.XY,
                                            calObservatory, seriesOriginalData, debug);

                                    intEndIndex = ChartHelper.transformDomainSliderValueToSeriesIndex(
                                            ChartUIComponentPlugin.DOMAIN_SLIDER_MINIMUM,
                                            ChartUIComponentPlugin.DOMAIN_SLIDER_MAXIMUM, domainendpoint,
                                            DatasetDomainUIComponentInterface.INDEX_RIGHT,
                                            collectionPrimary.getDomainLowerBound(true),
                                            collectionPrimary.getDomainUpperBound(true), DatasetType.XY,
                                            calObservatory, seriesOriginalData, debug);
                                    if ((intStartIndex == -1) || (intEndIndex == -1)) {
                                        // If either index is returned as -1, then there's nothing to do...
                                        // ...so stop the for() loop
                                        intStartIndex = 0;
                                        intEndIndex = 0;
                                    } else if (intEndIndex <= intStartIndex) {
                                        intEndIndex = intStartIndex + 1;
                                    }

                                    LOGGER.debug(debug,
                                            SOURCE + "before copy of selected series subset [channel="
                                                    + intChannelIndex + "] [index.start=" + intStartIndex
                                                    + "] [index.end=" + intEndIndex + "]");

                                    // Copy over only the selected range from the Slider
                                    for (int intDataIndex = intStartIndex; intDataIndex < intEndIndex; intDataIndex++) {
                                        final XYDataItem dataOriginalItem;
                                        final XYDataItem dataChangedItem;

                                        dataOriginalItem = (XYDataItem) listOriginalDataItems.get(intDataIndex);

                                        if (!ChannelSelectionMode.X1.equals(selectionMode)) {
                                            // Change each value of the series according to the multiplier
                                            dataChangedItem = new XYDataItem(dataOriginalItem.getX(),
                                                    dataOriginalItem.getY().doubleValue()
                                                            * selectionMode.getMultiplier());
                                        } else {
                                            // Just use the whole series unaltered for gain of X1
                                            dataChangedItem = new XYDataItem(dataOriginalItem.getX(),
                                                    dataOriginalItem.getY());
                                        }

                                        seriesChangedData.add(dataChangedItem);
                                    }

                                    // Did we collect any data for this Series?
                                    // If not, place a dummy point at the origin of the *visible* chart
                                    if (seriesChangedData.getItemCount() == 0) {
                                        // TODO ChartHelper.createDummyXYSeriesDataItemAtOrigin()
                                        seriesChangedData.add(new XYDataItem(0, 0));
                                    }

                                    // Place the changed series in the correct collection
                                    // to correspond with the original
                                    if (intChannelIndex < collectionPrimary.getSeriesCount()) {
                                        // Simply add the changed Primary series to the PrimaryDataset collection
                                        ((XYSeriesCollection) xyNewPrimaryDataset).addSeries(seriesChangedData);
                                    } else {
                                        // It must be a secondary dataset
                                        // Add the changed Secondary series to the parent SecondaryDataset
                                        // given by the *secondary* channel index
                                        ChartHelper.addSecondarySeries(datasettype,
                                                listParentSecondaryDatasetForSeries, seriesChangedData,
                                                intChannelIndex - collectionPrimary.getSeriesCount());
                                    }
                                } else {
                                    LOGGER.warn(SOURCE + "OriginalData XYSeries unexpectedly NULL");
                                }
                            } else {
                                // There are no data! Do nothing...
                                LOGGER.warn(SOURCE + "There are no data, so do nothing");
                            }
                        }
                    }

                    LOGGER.debug(debug, SOURCE + "Update the data shown on existing Chart");

                    // The outputs are xyNewPrimaryDataset and listNewSecondaryDatasets
                    // This Chart does not use secondary datasets (yet)

                    // Dump the (partial) contents of each Series in the new XYdataset
                    ChartHelper.dumpXYDataset(debug, calObservatory, xyNewPrimaryDataset, 4,
                            SOURCE + "XYSeriesCollection --> setDataset");

                    dao.getChartUI().getChartPanel().getChart().getXYPlot().setDataset(INDEX_DATA,
                            xyNewPrimaryDataset);
                } else {
                    LOGGER.error(SOURCE + " The XYSeriesCollection does not have any XYSeries");
                }
            } else if ((datasettype.getName().equals(DatasetType.TIMESTAMPED.getName()))
                    && (primarydataset instanceof TimeSeriesCollection)) {
                final TimeSeriesCollection collectionPrimary;

                LOGGER.debug(debug, SOURCE + "TIMESTAMPED domainstartpoint=" + domainstartpoint + " domainendpoint="
                        + domainendpoint);

                // Prepare a new TimeSeriesCollection for display
                xyNewPrimaryDataset = new TimeSeriesCollection(calObservatory.getTimeZone());

                // There should be a collection of <channelcount> TimeSeries in the Primary Dataset
                // but there may also be some in the Secondary Datasets
                collectionPrimary = (TimeSeriesCollection) primarydataset;

                if ((collectionPrimary.getSeriesCount() > 0) && (collectionPrimary.getSeries() != null)
                        && (channelselector.getChannelSelectionModes() != null)) {
                    ChartHelper.dumpXYDataset(debug, calObservatory, collectionPrimary, 4,
                            SOURCE + "TimeSeriesCollection PrimaryDataset --> Original Dataset");

                    // Find which channels and data range to use this time round
                    for (int intChannelIndex = 0; intChannelIndex < channelselector.getChannelSelectionModes()
                            .size(); intChannelIndex++) {
                        final ChannelSelectionMode selectionMode;

                        selectionMode = channelselector.getChannelSelectionModes().get(intChannelIndex);

                        if (!ChannelSelectionMode.OFF.equals(selectionMode)) {
                            final TimeSeries seriesOriginalData;

                            // We needed to know about the secondary datasets in order to get the correct index
                            seriesOriginalData = (TimeSeries) ChartHelper.getSeriesForIndex(datasettype,
                                    primarydataset, secondarydatasets, intChannelIndex, debug);
                            if (seriesOriginalData != null) {
                                final List listOriginalDataItems;
                                final TimeSeries seriesChangedData;

                                listOriginalDataItems = seriesOriginalData.getItems();

                                // Prepare a new Series for the changed data, with the same Key
                                seriesChangedData = new TimeSeries(seriesOriginalData.getKey().toString(),
                                        seriesOriginalData.getTimePeriodClass());

                                if (listOriginalDataItems.size() > 0) {
                                    int intStartIndex;
                                    int intEndIndex;

                                    // Map the slider values to data indexes
                                    intStartIndex = ChartHelper.transformDomainSliderValueToSeriesIndex(
                                            ChartUIComponentPlugin.DOMAIN_SLIDER_MINIMUM,
                                            ChartUIComponentPlugin.DOMAIN_SLIDER_MAXIMUM, domainstartpoint,
                                            DatasetDomainUIComponentInterface.INDEX_LEFT,
                                            collectionPrimary.getDomainLowerBound(true),
                                            collectionPrimary.getDomainUpperBound(true), DatasetType.TIMESTAMPED,
                                            calObservatory, seriesOriginalData, debug);

                                    intEndIndex = ChartHelper.transformDomainSliderValueToSeriesIndex(
                                            ChartUIComponentPlugin.DOMAIN_SLIDER_MINIMUM,
                                            ChartUIComponentPlugin.DOMAIN_SLIDER_MAXIMUM, domainendpoint,
                                            DatasetDomainUIComponentInterface.INDEX_RIGHT,
                                            collectionPrimary.getDomainLowerBound(true),
                                            collectionPrimary.getDomainUpperBound(true), DatasetType.TIMESTAMPED,
                                            calObservatory, seriesOriginalData, debug);
                                    if ((intStartIndex == -1) || (intEndIndex == -1)) {
                                        // If either index is returned as -1, then there's nothing to do...
                                        // ...so stop the for() loop
                                        LOGGER.debug(debug, SOURCE + "Set EndIndex = StartIndex = 0");
                                        intStartIndex = 0;
                                        intEndIndex = 0;
                                    } else if (intEndIndex <= intStartIndex) {
                                        LOGGER.debug(debug, SOURCE + "Correcting EndIndex less than StartIndex");
                                        intEndIndex = intStartIndex + 1;
                                    }

                                    LOGGER.debug(debug,
                                            SOURCE + "before copy of selected series subset [channel="
                                                    + intChannelIndex + "] [start_index=" + intStartIndex
                                                    + "] [end_index=" + intEndIndex + "]");

                                    // Copy over only the selected range from the Slider
                                    for (int intDataIndex = intStartIndex; intDataIndex < intEndIndex; intDataIndex++) {
                                        final TimeSeriesDataItem dataOriginalItem;
                                        final TimeSeriesDataItem dataChangedItem;

                                        dataOriginalItem = (TimeSeriesDataItem) listOriginalDataItems
                                                .get(intDataIndex);

                                        if (!ChannelSelectionMode.X1.equals(selectionMode)) {
                                            // Change each value of the series according to the multiplier
                                            dataChangedItem = new TimeSeriesDataItem(dataOriginalItem.getPeriod(),
                                                    dataOriginalItem.getValue().doubleValue()
                                                            * selectionMode.getMultiplier());
                                        } else {
                                            // Just use the whole series unaltered for gain of X1
                                            dataChangedItem = new TimeSeriesDataItem(dataOriginalItem.getPeriod(),
                                                    dataOriginalItem.getValue().doubleValue());
                                        }

                                        seriesChangedData.add(dataChangedItem);
                                    }

                                    // Did we collect any data for this Series?
                                    // If not, place a dummy point at the origin of the *visible* chart
                                    if (seriesChangedData.getItemCount() == 0) {
                                        seriesChangedData.add(ChartHelper.createDummyTimeSeriesDataItemAtOrigin(
                                                collectionPrimary, seriesOriginalData, domainstartpoint, debug));
                                    }

                                    // Place the changed series in the correct collection
                                    // to correspond with the original
                                    if (intChannelIndex < collectionPrimary.getSeriesCount()) {
                                        // Simply add the changed Primary series to the PrimaryDataset collection
                                        ((TimeSeriesCollection) xyNewPrimaryDataset).addSeries(seriesChangedData);
                                    } else {
                                        // It must be a secondary dataset
                                        // Add the changed Secondary series to the parent SecondaryDataset
                                        // given by the *secondary* channel index
                                        ChartHelper.addSecondarySeries(datasettype,
                                                listParentSecondaryDatasetForSeries, seriesChangedData,
                                                intChannelIndex - collectionPrimary.getSeriesCount());
                                    }
                                } else {
                                    // There are no data! Do nothing...
                                    LOGGER.warn(SOURCE + "There are no data, so do nothing [channel.index="
                                            + intChannelIndex + "]");
                                }
                            } else {
                                LOGGER.warn(SOURCE + "OriginalData TimeSeries unexpectedly NULL [channel.index="
                                        + intChannelIndex + "]");
                            }
                        } else {
                            LOGGER.debug(debug, SOURCE + "Channel is OFF [channel.index=" + intChannelIndex + "]");
                        }
                    }

                    LOGGER.debug(debug, SOURCE + "Update the data shown on existing Chart");

                    // The outputs are xyNewPrimaryDataset and listNewSecondaryDatasets
                    // This Chart superclass does not use secondary datasets (yet)

                    // Dump the (partial) contents of each Series in the new XYdataset
                    ChartHelper.dumpXYDataset(debug, calObservatory, xyNewPrimaryDataset, 4,
                            SOURCE + "TimeSeriesCollection NewPrimaryDataset --> setDataset");

                    dao.getChartUI().getChartPanel().getChart().getXYPlot().setDataset(INDEX_DATA,
                            xyNewPrimaryDataset);
                } else {
                    LOGGER.error(SOURCE + " The TimeSeriesCollection does not have any TimeSeries");
                }
            } else {
                LOGGER.error(SOURCE + " The Dataset is of an invalid type");
            }
        } else {
            LOGGER.debug(debug, SOURCE + " Unable to change the Chart - invalid parameters");
        }
    }

    /**********************************************************************************************/
    /* Data and Metadata                                                                          */
    /***********************************************************************************************
     * Get the DAO providing data for this Chart.
     *
     * @return ObservatoryInstrumentDAOInterface
     */

    public ObservatoryInstrumentDAOInterface getDAO() {
        return (this.daoChart);
    }

    /***********************************************************************************************
     * Set the DAO providing data for this Chart.
     *
     * @param dao
     */

    public void setDAO(final ObservatoryInstrumentDAOInterface dao) {
        this.daoChart = dao;
    }

    /***********************************************************************************************
     * Get the List of Metadata for this Chart.
     *
     * @return List<Metadata>
     */

    public List<Metadata> getMetadata() {
        return (this.listMetadata);
    }

    /***********************************************************************************************
     * Set the List of Metadata for this Chart, and associate the Chart with the specified DAO.
     * Set updatedaometadata to TRUE if the specified Metadata should be copied into the
     * appropriate containers in the specified DAO.
     * If the Metadata was already collected from the DAO, just set updatedaometadata to FALSE.
     *
     * @param metadatalist
     * @param dao
     * @param updatedaometadata
     * @param debug
     */

    public void setMetadata(final List<Metadata> metadatalist, final ObservatoryInstrumentDAOInterface dao,
            final boolean updatedaometadata, final boolean debug) {
        final String SOURCE = "ChartUIComponent.setMetadata() ";

        // Set the List of Metadata for this Chart
        this.listMetadata = metadatalist;

        // Associate the specified DAO (even if NULL) with the Chart
        setDAO(dao);

        // Associate the Chart with the specified DAO (if any)
        if (dao != null) {
            dao.setChartUI(this);

            // The specified Metadata should be copied into the appropriate containers in the DAO
            if (updatedaometadata) {
                dao.addAllMetadataToContainersTraced(metadatalist, SOURCE, debug);
            }
        }

        MetadataHelper.showMetadataList(metadatalist, SOURCE + "[chart=" + getChartName() + "]", debug);
    }

    /***********************************************************************************************
     * Get the Primary XYDataset to be shown on the ChartUIComponent.
     * Supported datasets: XYSeriesCollection and TimeSeriesCollection.
     *
     * @return XYDataset
     */

    public XYDataset getPrimaryXYDataset() {
        return (this.xyDatasetPrimary);
    }

    /***********************************************************************************************
     * Set the primary XYDataset, adjust the type of the Dataset if required.
     * Supported datasets: XYSeriesCollection and TimeSeriesCollection.
     * This relies on ChannelCount having been set by the caller.
     *
     * @param dao
     * @param dataset
     */

    public void setPrimaryXYDataset(final ObservatoryInstrumentDAOInterface dao, final XYDataset dataset) {
        final String SOURCE = "ChartUIComponent.setPrimaryXYDataset() ";

        LOGGER.debug(LOADER_PROPERTIES.isChartDebug(), SOURCE);

        // We can rely on ChannelCount having been set by the caller
        if ((dao != null) && (dataset != null) && (dataset.getSeriesCount() > 0)) {
            final DatasetType typeNew;

            // Supported datasets: XYSeriesCollection and TimeSeriesCollection
            if (dataset instanceof XYSeriesCollection) {
                typeNew = DatasetType.XY;
            } else if (dataset instanceof TimeSeriesCollection) {
                typeNew = DatasetType.TIMESTAMPED;
            } else {
                LOGGER.error(SOURCE + " Unexpected DatasetType");
                typeNew = DatasetType.XY;
            }

            setDatasetType(typeNew);

            this.xyDatasetPrimary = dataset;
            this.xyDatasetSecondaries.clear();

            // Remove all crosshairs because this may be a new import or capture
            setRangeCrosshair(0.0);
            setDomainCrosshair(0.0);

            // Always reset to extents when new data arrive,
            // otherwise we might not see any realtime updates
            if ((hasDatasetDomainControl()) && (getDatasetDomainControlOccupant() != null)) {
                setDatasetDomainChanged(getDatasetDomainControlOccupant().resetToExtents());
            }

            LOGGER.debug(
                    ((LOADER_PROPERTIES.isChartDebug()) && (getHostInstrument() != null)
                            && (getDatasetType() != null)),
                    SOURCE + "New XYDataset configuration " + "[dataset_type=" + getDatasetType().getName() // NullPointerException will not occur
                            + "] [dataset_type.changed=" + dao.isDatasetTypeChanged() + "] [channel_count.changed="
                            + dao.isChannelCountChanged() + "] [channel_count=" + dataset.getSeriesCount()
                            + "] [dataset_domain.changed=" + isDatasetDomainChanged()
                            + "] [channel_selection.changed=" + isChannelSelectionChanged() + "] [raw_data.changed="
                            + dao.isRawDataChanged() + "] [processed_data.changed=" + dao.isProcessedDataChanged()
                            + "]");
        } else {
            // Force re-creation of the Chart to show a blank, since there is no DAO or dataset
            setDatasetType(null);
            setChannelCount(0);

            this.xyDatasetPrimary = null;
            this.xyDatasetSecondaries.clear();

            setRangeCrosshair(0.0);
            setDomainCrosshair(0.0);

            if ((hasDatasetDomainControl()) && (getDatasetDomainControlOccupant() != null)) {
                setDatasetDomainChanged(getDatasetDomainControlOccupant().resetToExtents());
            }

            notifyDatasetChangedEvent(this, 0, 0);

            // Don't show DAO or DatasetType, because they are NULL!
            LOGGER.debug(((LOADER_PROPERTIES.isChartDebug()) && (getHostInstrument() != null)),
                    SOURCE + "Supplied with invalid or null XYDataset " + "[domain_changed="
                            + isDatasetDomainChanged() + "] [dataset_notnull=" + (dataset != null) + "]");
        }
    }

    /***********************************************************************************************
     * Get the Secondary XYDatasets to be shown on the ChartUIComponent.
     * This is rarely used: for example GOES Client uses it for the Ratio axis.
     * Supported datasets: XYSeriesCollection and TimeSeriesCollection.
     *
     * @return List<XYDataset>
     */

    public List<XYDataset> getSecondaryXYDatasets() {
        return (this.xyDatasetSecondaries);
    }

    /***********************************************************************************************
     * Set the Secondary XYDatasets to be shown on the ChartUIComponent.
     * This is rarely used: for example GOES Client uses it for the Ratio axis.
     * Supported datasets: XYSeriesCollection and TimeSeriesCollection.
     *
     * @param datasets
     */

    public void setSecondaryDatasets(final List<XYDataset> datasets) {
        this.xyDatasetSecondaries = datasets;
    }

    /***********************************************************************************************
     * Get the DatasetType - Tabular, XY or Timestamped.
     *
     * @return DatasetType
     */

    public DatasetType getDatasetType() {
        return (this.datasetType);
    }

    /***********************************************************************************************
     * Set the DatasetType - Tabular, XY or Timestamped.
     *
     * @param type
     */

    private void setDatasetType(final DatasetType type) {
        this.datasetType = type;
    }

    /***********************************************************************************************
     * Get the Update Type - Preserve, Truncate or Decimate.
     *
     * @return DataUpdateType
     */

    protected DataUpdateType getUpdateType() {
        return (this.updateType);
    }

    /***********************************************************************************************
     * Set the Update Type - Preserve, Truncate or Decimate.
     *
     * @param type
     */

    protected void setUpdateType(final DataUpdateType type) {
        this.updateType = type;
    }

    /***********************************************************************************************
     * Set up for mouse click refresh, or no refresh.
     */

    private synchronized void setRefreshType() {
        final String SOURCE = "ChartUIComponent.setRefreshType()";

        // Check to see if we need to refresh the Chart
        if (getChartContainer() != null) {
            if ((isRefreshable()) && (isClickRefresh())) {
                // Refresh on a mouse click
                getChartContainer().setToolTipText(TOOLTIP_CLICK_HERE);

                getChartContainer().addMouseListener(new MouseAdapter() {
                    public void mousePressed(final MouseEvent event) {
                        LOGGER.debug(LOADER_PROPERTIES.isChartDebug(), "CLICK! Refresh Chart " + getChartName());

                        // If there is a User click, then the Chart must be visible
                        refreshChart(getDAO(), true, SOURCE + "mousePressed() ");
                    }
                });
            } else {
                getChartContainer().setToolTipText(EMPTY_STRING);
            }
        }
    }

    /***********************************************************************************************
     * Get the Channel count.
     *
     * @return int
     */

    public int getChannelCount() {
        return (this.intChannelCount);
    }

    /***********************************************************************************************
     * Set the Channel count.
     *
     * @param count
     */

    public void setChannelCount(final int count) {
        this.intChannelCount = count;
    }

    /***********************************************************************************************
     * Indicate if the first data channel represents Temperature (Usually a Staribus dataset).
     *
     * @return boolean
     */

    public boolean hasTemperatureChannel() {
        return (this.boolTemperatureChannel);
    }

    /***********************************************************************************************
     * Set a flag to indicate if the first data channel represents Temperature
     * (Usually a Staribus dataset).
     *
     * @param temperature
     */

    public void setTemperatureChannel(final boolean temperature) {
        this.boolTemperatureChannel = temperature;
    }

    /**********************************************************************************************/
    /* User Interface                                                                             */
    /***********************************************************************************************
     * Get the JPanel holding the complete UI of the Chart Tab.
     *
     * @return JPanel
     */

    private JPanel getUIContainer() {
        return (this.panelUIContainer);
    }

    /***********************************************************************************************
     * Get the optional JToolBar.
     * Default is NULL, override if needed.
     *
     * @return JToolBar
     */

    public JToolBar getToolBar() {
        return (null);
    }

    /***********************************************************************************************
     * Get the Chart JPanel container.
     *
     * @return JPanel
     */

    public synchronized JPanel getChartContainer() {
        return (this.panelChartContainer);
    }

    /***********************************************************************************************
     * Get the Chart name.
     *
     * @return String
     */

    public synchronized final String getChartName() {
        return (this.strReportName);
    }

    /***********************************************************************************************
     * Set the Chart name.
     *
     * @param name
     */

    public synchronized final void setChartName(final String name) {
        strReportName = name;
    }

    /***********************************************************************************************
     * Get the ChartPanel containing the JFreeChart.
     *
     * @return ChartPanel
     */

    public final ChartPanel getChartPanel() {
        return (this.chartPanel);
    }

    /***********************************************************************************************
     * Set the ChartPanel containing the JFreeChart.
     *
     * @param chartpanel
     */

    public final void setChartPanel(final ChartPanel chartpanel) {
        this.chartPanel = chartpanel;
    }

    /***********************************************************************************************
     * Indicate if the Chart has a ChannelSelector.
     *
     * @return boolean
     */

    public boolean hasChannelSelector() {
        return (this.boolHasChannelSelector);
    }

    /***********************************************************************************************
     * Indicate if the Chart has a ChannelSelector.
     *
     * @param hasselector
     */

    public void setChannelSelector(final boolean hasselector) {
        this.boolHasChannelSelector = hasselector;
    }

    /***********************************************************************************************
     * Get the Chart Channel Selector.
     *
     * @return ChannelSelectorUIComponentInterface
     */

    public synchronized ChannelSelectorUIComponentInterface getChannelSelectorOccupant() {
        return (this.channelSelectorOccupant);
    }

    /***********************************************************************************************
     * Set the Chart Channel Selector.
     *
     * @param selector
     */

    public synchronized void setChannelSelectorOccupant(final ChannelSelectorUIComponentInterface selector) {
        this.channelSelectorOccupant = selector;
    }

    /***********************************************************************************************
     * Indicate if the Chart has a DatasetDomainControl.
     *
     * @return boolean
     */

    public boolean hasDatasetDomainControl() {
        return (this.boolHasDatasetDomainControl);
    }

    /***********************************************************************************************
     * Indicate if the Chart has a DatasetDomainControl.
     *
     * @param hasdomaincontrol
     */

    public void setDatasetDomainControl(final boolean hasdomaincontrol) {
        this.boolHasDatasetDomainControl = hasdomaincontrol;
    }

    /***********************************************************************************************
     * Get the DatasetDomain UIComponent.
     *
     * @return DatasetDomainUIComponentInterface
     */

    protected synchronized DatasetDomainUIComponentInterface getDatasetDomainControlOccupant() {
        return (this.datasetDomainControlOccupant);
    }

    /***********************************************************************************************
     * Set the DatasetDomain UIComponent.
     *
     * @param datasetdomain
     */

    private synchronized void setDatasetDomainControlOccupant(
            final DatasetDomainUIComponentInterface datasetdomain) {
        this.datasetDomainControlOccupant = datasetdomain;
    }

    /***********************************************************************************************
     * Indicate if the ChannelSelection of the chart data has changed.
     *
     * @return boolean
     */

    public boolean isChannelSelectionChanged() {
        return (this.boolChannelSelectionChanged);
    }

    /***********************************************************************************************
     * Indicate if the ChannelSelection of the chart data has changed.
     *
     * @param changed
     */

    public void setChannelSelectionChanged(final boolean changed) {
        this.boolChannelSelectionChanged = changed;
    }

    /***********************************************************************************************
     * Indicate if the DatasetDomain of the chart data has changed.
     *
     * @return boolean
     */

    public boolean isDatasetDomainChanged() {
        return (this.boolDatasetDomainChanged);
    }

    /***********************************************************************************************
     * Indicate if the DatasetDomain of the chart data has changed.
     *
     * @param changed
     */

    public void setDatasetDomainChanged(final boolean changed) {
        this.boolDatasetDomainChanged = changed;
    }

    /***********************************************************************************************
     * Indicate if this Chart has a Linear display mode.
     *
     * @return boolean
     */

    public boolean hasLinearMode() {
        return (this.boolChartHasLinearMode);
    }

    /***********************************************************************************************
     * Indicate if this Chart has a Linear display mode.
     *
     * @param linearmode
     */

    protected void setLinearMode(final boolean linearmode) {
        this.boolChartHasLinearMode = linearmode;
    }

    /***********************************************************************************************
     * Indicate if this Chart has a Logarithmic display mode.
     *
     * @return boolean
     */

    public boolean hasLogarithmicMode() {
        return (this.boolChartHasLogarithmicMode);
    }

    /***********************************************************************************************
     * Indicate if this Chart has a Logarithmic display mode.
     *
     * @param logmode
     */

    protected void setLogarithmicMode(final boolean logmode) {
        this.boolChartHasLogarithmicMode = logmode;
    }

    /***********************************************************************************************
     * Get the Domain Start Point.
     *
     * @return int
     */

    protected int getDatasetDomainStartPoint() {
        return (this.intDomainStartPoint);
    }

    /***********************************************************************************************
     * Set the Domain Start Point.
     *
     * @param startpoint
     */

    private void setDatasetDomainStartPoint(final int startpoint) {
        this.intDomainStartPoint = startpoint;
    }

    /***********************************************************************************************
     * Get the Domain End Point.
     *
     * @return int
     */

    protected int getDatasetDomainEndPoint() {
        return (this.intDomainEndPoint);
    }

    /***********************************************************************************************
     * Set the Domain End Point.
     *
     * @param endpoint                                              c
     */

    private void setDatasetDomainEndPoint(final int endpoint) {
        this.intDomainEndPoint = endpoint;
    }

    /***********************************************************************************************
     * Get the Range Crosshair value.
     * Returns 0 if no crosshair is in use.
     *
     * @return double
     */

    public double getRangeCrosshair() {
        return (this.dblRangeCrosshair);
    }

    /***********************************************************************************************
     * Set the Range Crosshair value.
     * Set 0 if no crosshair is in use.
     *
     * @param value
     */

    public void setRangeCrosshair(final double value) {
        final String SOURCE = "ChartUIComponent.setRangeCrosshair() ";

        this.dblRangeCrosshair = value;

        LOGGER.debug(LOADER_PROPERTIES.isChartDebug(),
                SOURCE + "Set Range Crosshair on ChartUI [value=" + value + "]");
    }

    /***********************************************************************************************
     * Get the Domain Crosshair value.
     * Returns DOMAIN_SLIDER_MINIMUM if no crosshair is in use.
     *
     * @return double
     */

    public double getDomainCrosshair() {
        return (this.dblDomainCrosshair);
    }

    /***********************************************************************************************
     * Set the Domain Crosshair value.
     * Set DOMAIN_SLIDER_MINIMUM if no crosshair is in use.
     *
     * @param value
     */

    public void setDomainCrosshair(final double value) {
        final String SOURCE = "ChartUIComponent.setDomainCrosshair() ";

        this.dblDomainCrosshair = value;

        LOGGER.debug(LOADER_PROPERTIES.isChartDebug(),
                SOURCE + "Set Domain Crosshair on ChartUI [value=" + value + "]");
    }

    /***********************************************************************************************
     * Get the Display Limit.
     *
     * @return int
     */

    public int getDisplayLimit() {
        return (this.intDisplayLimit);
    }

    /***********************************************************************************************
     * Set the Display Limit.
     *
     * @param limit
     */

    protected void setDisplayLimit(final int limit) {
        this.intDisplayLimit = limit;
    }

    /***********************************************************************************************
     * Indicate if this ChartUIComponent is refreshable.
     *
     * @return boolean
     */

    private synchronized boolean isRefreshable() {
        return (this.boolRefreshable);
    }

    /***********************************************************************************************
     * Indicate if this ChartUIComponent is refreshable.
     *
     * @param refreshable
     */

    public synchronized final void setRefreshable(final boolean refreshable) {
        this.boolRefreshable = refreshable;
    }

    /***********************************************************************************************
     * Indicate if this Chart is to be refreshed on a mouse click.
     *
     * @return boolean
     */

    private synchronized boolean isClickRefresh() {
        return (this.boolClickRefresh);
    }

    /***********************************************************************************************
     * Indicate if this Chart is to be refreshed on a mouse click.
     *
     * @param click
     */

    public synchronized final void setClickRefresh(final boolean click) {
        this.boolClickRefresh = click;
    }

    /**********************************************************************************************/
    /* Injections                                                                                 */
    /***********************************************************************************************
     * Get the Task on which this Chart is based.
     *
     * @return TaskData
     */

    private synchronized TaskPlugin getTask() {
        return (this.pluginTask);
    }

    /***********************************************************************************************
     * Get the ObservatoryInstrument to which this UIComponent is attached.
     *
     * @return ObservatoryInstrumentInterface
     */

    protected ObservatoryInstrumentInterface getHostInstrument() {
        return (this.hostInstrument);
    }

    /***********************************************************************************************
     * Indicate if the Chart can Autorange, or must stay at FixedRange
     *
     * @return boolean
     */

    public boolean canAutorange() {
        return (this.boolChartCanAutorange);
    }

    /***********************************************************************************************
     * Indicate if the Chart can Autorange, or must stay at FixedRange
     *
     * @param autorange
     */

    public void setCanAutorange(final boolean autorange) {
        this.boolChartCanAutorange = autorange;
    }

    /***********************************************************************************************
     * Get the ObservatoryClock.
     *
     * @return ObservatoryClockInterface
     */

    protected ObservatoryClockInterface getObservatoryClock() {
        final ObservatoryClockInterface clock;

        clock = getHostInstrument().getObservatoryClock();

        return (clock);
    }

    /***********************************************************************************************
     * Get the ResourceKey for the Chart.
     *
     * @return String
     */

    public synchronized final String getResourceKey() {
        return (this.strResourceKey);
    }

    /**********************************************************************************************/
    /* Events                                                                                     */
    /***********************************************************************************************
     * Indicate that something changed on the ChannelSelection panel.
     *
     * @param event
     */

    public void channelSelectionChanged(final ChannelSelectionChangedEvent event) {
        final String SOURCE = "ChartUIComponent.channelSelectionChanged() ";

        LOGGER.debug(LOADER_PROPERTIES.isChartDebug(), SOURCE + "Channel Selection changed");

        if ((event != null) && (event.isCropped())) {
            // Use the datasets being displayed as the new RawData (Primary and Secondaries)
            if ((hasDatasetDomainControl()) && (getDatasetDomainControlOccupant() != null)) {
                setDatasetDomainChanged(getDatasetDomainControlOccupant().resetToExtents());
            }
        }

        // A change in Channel selection must force a refresh
        setDomainCrosshair(0.0);
        setChannelSelectionChanged(true);

        // If the Channel selection has changed, then the Chart must be visible
        refreshChart(getDAO(), true, SOURCE);
    }

    /*******************************************************************************************
     * Indicate that something changed on the DatasetDomain panel.
     *
     * @param event
     */

    public void datasetDomainChanged(final DatasetDomainChangedEvent event) {
        final String SOURCE = "ChartUIComponent.datasetDomainChanged() ";

        if ((event != null) && (event.getSource() instanceof MThumbSlider)) {
            final MThumbSlider slider;

            slider = (MThumbSlider) event.getSource();

            // Only update the values from the DatasetDomainUIComponent if the slider has stopped moving,
            // and if there has been a change
            if ((slider != null) && (!slider.getValueIsAdjusting())
                    && (slider.getValueAt(INDEX_LEFT) >= DOMAIN_SLIDER_MINIMUM)
                    && (slider.getValueAt(INDEX_RIGHT) <= DOMAIN_SLIDER_MAXIMUM)
                    && (slider.getValueAt(INDEX_LEFT) < slider.getValueAt(INDEX_RIGHT))
                    && ((getDatasetDomainStartPoint() != slider.getValueAt(INDEX_LEFT))
                            || (getDatasetDomainEndPoint() != slider.getValueAt(INDEX_RIGHT)))) {
                setDatasetDomainStartPoint(slider.getValueAt(INDEX_LEFT));
                setDatasetDomainEndPoint(slider.getValueAt(INDEX_RIGHT));

                LOGGER.debug(LOADER_PROPERTIES.isChartDebug(), SOURCE + "DatasetDomain changed [startpoint="
                        + getDatasetDomainStartPoint() + "] [endpoint=" + getDatasetDomainEndPoint() + "]");

                // A change in Dataset Domain must force a refresh
                setDatasetDomainChanged(true);

                // If the Dataset Domain has changed, then the Chart must be visible
                refreshChart(getDAO(), true, SOURCE);
            }
            //            else
            //                {
            //                LOGGER.error(SOURCE + "Incorrect slider value range!");
            //                }
        } else {
            LOGGER.error(SOURCE + "Incorrect event source!");
        }
    }

    /***********************************************************************************************
     * Update the DatasetDomain Slider Values which are stored in the ChartUIComponent.
     */

    private void updateSliderLocalValues() {
        final String SOURCE = "ChartUIComponent.updateSliderLocalValues() ";

        if ((hasDatasetDomainControl()) && (getDatasetDomainControlOccupant() != null)
                && (getDatasetDomainControlOccupant().getDatasetDomainSlider() != null)) {
            setDatasetDomainStartPoint(
                    getDatasetDomainControlOccupant().getDatasetDomainSlider().getValueAt(INDEX_LEFT));
            setDatasetDomainEndPoint(
                    getDatasetDomainControlOccupant().getDatasetDomainSlider().getValueAt(INDEX_RIGHT));

            LOGGER.debug(LOADER_PROPERTIES.isChartDebug(), SOURCE + "[startpoint=" + getDatasetDomainStartPoint()
                    + "] [endpoint=" + getDatasetDomainEndPoint() + "]");
        } else {
            LOGGER.debug(LOADER_PROPERTIES.isChartDebug(),
                    SOURCE + "Can't update slider values, since there is currently no slider");
        }
    }

    /***********************************************************************************************
     * Respond to changes in the DatasetDomain Sliders or a ControlKnobUIComponent.
     *
     * @param event
     */

    public void stateChanged(final ChangeEvent event) {
        final String SOURCE = "ChartUIComponent.stateChanged() ";
        final Object source;
        final boolean boolDebug;

        boolDebug = LOADER_PROPERTIES.isChartDebug();

        LOGGER.debug(boolDebug, "Chart State Changed " + Logger.CONSOLE_SEPARATOR_MAJOR);

        source = event.getSource();

        if (source instanceof MThumbSlider) {
            final MThumbSlider slider;

            slider = (MThumbSlider) source;

            // Try to update the moving crosshair, but only if there is a Chart
            if ((slider.getModelAt(MThumbSliderAdditionalUI.INDEX_LEFT) != null)
                    && (slider.getModelAt(MThumbSliderAdditionalUI.INDEX_RIGHT) != null)) {
                LOGGER.debug(boolDebug, SOURCE + "[value.current.left="
                        + slider.getValueAt(MThumbSliderAdditionalUI.INDEX_LEFT) + "] [value.current.right="
                        + slider.getValueAt(MThumbSliderAdditionalUI.INDEX_RIGHT) + "] [value.previous.left="
                        + getDatasetDomainStartPoint() + "] [value.previous.right=" + getDatasetDomainEndPoint()
                        + "] [slider.isadjusting=" + slider.getValueIsAdjusting() + "] [model.isadjusting.left="
                        + slider.getModelAt(MThumbSliderAdditionalUI.INDEX_LEFT).getValueIsAdjusting()
                        + "] [model.isadjusting.right="
                        + slider.getModelAt(MThumbSliderAdditionalUI.INDEX_RIGHT).getValueIsAdjusting() + "]");

                if (slider.getModelAt(MThumbSliderAdditionalUI.INDEX_LEFT).getValueIsAdjusting()) {
                    ChartUIHelper.updateDomainCrosshairForDomainSlider(this, INDEX_LEFT,
                            getDatasetDomainStartPoint(), getDatasetDomainEndPoint(), slider.getValueAt(INDEX_LEFT),
                            DOMAIN_SLIDER_MINIMUM, DOMAIN_SLIDER_MAXIMUM, boolDebug);
                } else if (slider.getModelAt(MThumbSliderAdditionalUI.INDEX_RIGHT).getValueIsAdjusting()) {
                    ChartUIHelper.updateDomainCrosshairForDomainSlider(this, INDEX_RIGHT,
                            getDatasetDomainStartPoint(), getDatasetDomainEndPoint(),
                            slider.getValueAt(INDEX_RIGHT), DOMAIN_SLIDER_MINIMUM, DOMAIN_SLIDER_MAXIMUM,
                            boolDebug);
                } else {
                    // This should never occur!
                    setDomainCrosshair(0.0);
                }

                LOGGER.debug(boolDebug,
                        SOURCE + "Domain Slider Crosshair updated [crosshair.domain=" + getDomainCrosshair() + "]");
            } else {
                LOGGER.debug(boolDebug, SOURCE + "There is no Slider, so cannot update Domain Crosshair");
            }
        } else if (source instanceof ControlKnobInterface) {
            final ControlKnobInterface knob;

            knob = (ControlKnobInterface) source;

            LOGGER.debug(boolDebug, SOURCE + "ControlKnobUIComponent [value.knob=" + knob.getValue() + "]");

            //            ChartHelper.updateDomainCrosshairForOffsetControl(this,
            //                                                              OFFSET_CONTROL_COARSE_MINIMUM,
            //                                                              OFFSET_CONTROL_COARSE_MAXIMUM,
            //                                                              knob.getValue(),
            //                                                              boolDebug);
        } else {
            LOGGER.error(SOURCE + "Unexpected Source of ChangeEvent");
        }
    }

    /***********************************************************************************************
     * Notify all listeners of DatasetChangedEvents.
     *
     * @param eventsource
     * @param seriescount
     * @param itemcount0
     */

    public final void notifyDatasetChangedEvent(final Object eventsource, final int seriescount,
            final int itemcount0) {
        List<DatasetChangedListener> listeners;
        final DatasetChangedEvent changeEvent;

        // Create a Thread-safe List of Listeners
        listeners = new CopyOnWriteArrayList<DatasetChangedListener>(getDatasetChangedListeners());

        // Create an DatasetChangedEvent
        changeEvent = new DatasetChangedEvent(eventsource, seriescount, itemcount0);

        // Fire the event to every listener
        synchronized (listeners) {
            for (int i = 0; i < listeners.size(); i++) {
                final DatasetChangedListener changeListener;

                changeListener = listeners.get(i);
                changeListener.datasetChanged(changeEvent);
            }
        }

        // Help the GC?
        listeners = null;
    }

    /***********************************************************************************************
     * Get the DatasetChanged Listeners (mostly for testing).
     *
     * @return Vector<DatasetChangedListener>
     */

    public final Vector<DatasetChangedListener> getDatasetChangedListeners() {
        return (this.vecDatasetChangedListeners);
    }

    /***********************************************************************************************
     * Add a listener for this event.
     *
     * @param listener
     */

    public final void addDatasetChangedListener(final DatasetChangedListener listener) {
        if ((listener != null) && (getDatasetChangedListeners() != null)
                && (!getDatasetChangedListeners().contains(listener))) {
            getDatasetChangedListeners().addElement(listener);
        }
    }

    /***********************************************************************************************
     * Remove a listener for this event.
     *
     * @param listener
     */

    public final void removeDatasetChangedListener(final DatasetChangedListener listener) {
        if ((listener != null) && (getDatasetChangedListeners() != null)) {
            getDatasetChangedListeners().removeElement(listener);
        }
    }
}