net.tourbook.conconi.ConconiView.java Source code

Java tutorial

Introduction

Here is the source code for net.tourbook.conconi.ConconiView.java

Source

/*******************************************************************************
 * Copyright (C) 2005, 2016 Wolfgang Schramm and Contributors
 *
 * This program 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 version 2 of the License.
 *
 * This program 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
 * this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110, USA
 *******************************************************************************/
package net.tourbook.conconi;

import gnu.trove.list.array.TDoubleArrayList;

import java.util.ArrayList;
import java.util.Collections;

import net.tourbook.Messages;
import net.tourbook.application.TourbookPlugin;
import net.tourbook.chart.Chart;
import net.tourbook.chart.ChartDataModel;
import net.tourbook.chart.ChartDataXSerie;
import net.tourbook.chart.ChartDataYSerie;
import net.tourbook.chart.ChartType;
import net.tourbook.chart.IChartLayer;
import net.tourbook.common.CommonActivator;
import net.tourbook.common.UI;
import net.tourbook.common.color.GraphColorManager;
import net.tourbook.common.preferences.ICommonPreferences;
import net.tourbook.common.tooltip.ActionToolbarSlideout;
import net.tourbook.common.tooltip.ToolbarSlideout;
import net.tourbook.common.util.Util;
import net.tourbook.data.TourData;
import net.tourbook.preferences.ITourbookPreferences;
import net.tourbook.tour.ITourEventListener;
import net.tourbook.tour.SelectionDeletedTours;
import net.tourbook.tour.SelectionTourData;
import net.tourbook.tour.SelectionTourId;
import net.tourbook.tour.SelectionTourIds;
import net.tourbook.tour.TourEventId;
import net.tourbook.tour.TourManager;

import org.eclipse.jface.action.IToolBarManager;
import org.eclipse.jface.dialogs.IDialogSettings;
import org.eclipse.jface.layout.GridDataFactory;
import org.eclipse.jface.layout.GridLayoutFactory;
import org.eclipse.jface.layout.PixelConverter;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.preference.PreferenceConverter;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.ModifyEvent;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseWheelListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.RGB;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Scale;
import org.eclipse.swt.widgets.Spinner;
import org.eclipse.swt.widgets.ToolBar;
import org.eclipse.ui.IPartListener2;
import org.eclipse.ui.ISelectionListener;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.IWorkbenchPartReference;
import org.eclipse.ui.forms.widgets.FormToolkit;
import org.eclipse.ui.part.PageBook;
import org.eclipse.ui.part.ViewPart;

/**
 * Show selected tours in a conconi test chart
 */
public class ConconiView extends ViewPart {

    public static final String ID = "net.tourbook.conconi.ConconiView"; //$NON-NLS-1$

    private static final int ADJUST_MAX_PULSE_VALUE = 5;
    private static final int ADJUST_MAX_POWER_VALUE = 10;

    private static final String STATE_CONCONI_IS_LOG_SCALING = "STATE_CONCONI_LOG_SCALING"; //$NON-NLS-1$
    private static final String STATE_CONCONI_SCALING_FACTOR = "STATE_CONCONI_SCALING_FACTOR"; //$NON-NLS-1$

    private static final RGB DEFAULT_RGB = new RGB(0xd0, 0xd0, 0xd0);

    private static final String GRID_PREF_PREFIX = "GRID_CONCONI__"; //$NON-NLS-1$

    private static final String GRID_IS_SHOW_VERTICAL_GRIDLINES = (GRID_PREF_PREFIX
            + ITourbookPreferences.CHART_GRID_IS_SHOW_VERTICAL_GRIDLINES);
    private static final String GRID_IS_SHOW_HORIZONTAL_GRIDLINES = (GRID_PREF_PREFIX
            + ITourbookPreferences.CHART_GRID_IS_SHOW_HORIZONTAL_GRIDLINES);
    private static final String GRID_VERTICAL_DISTANCE = (GRID_PREF_PREFIX
            + ITourbookPreferences.CHART_GRID_VERTICAL_DISTANCE);
    private static final String GRID_HORIZONTAL_DISTANCE = (GRID_PREF_PREFIX
            + ITourbookPreferences.CHART_GRID_HORIZONTAL_DISTANCE);

    private final IPreferenceStore _prefStore = TourbookPlugin.getPrefStore();
    private final IPreferenceStore _commonPrefStore = CommonActivator.getPrefStore();
    private final IDialogSettings _state = TourbookPlugin.getState(ID);

    private IPartListener2 _partListener;
    private ISelectionListener _postSelectionListener;
    private IPropertyChangeListener _prefChangeListener;
    private ITourEventListener _tourEventListener;

    private ChartDataYSerie _yDataPulse;
    private ConconiData _conconiDataForSelectedTour;

    protected boolean _isUpdateUI = false;
    private boolean _isSelectionDisabled = true;
    private boolean _isSaving;

    private TourData _selectedTour;

    private int _originalTourDeflection = -1;
    private int _modifiedTourDeflection = -1;

    private PixelConverter _pc;
    private FormToolkit _tk;

    private ActionConconiOptions _actionConconiOptions;

    private int _hintDefaultSpinnerWidth;

    /*
     * UI controls
     */
    private PageBook _pageBook;
    private Composite _page_NoTour;
    private Composite _page_ConconiTest;

    private ArrayList<TourData> _conconiTours;

    private Chart _chartConconiTest;
    private ChartLayerConconiTest _conconiLayer;

    private Combo _comboTests;
    private Scale _scaleDeflection;
    private Label _lblDeflactionPulse;
    private Label _lblDeflactionPower;

    private Button _chkExtendedScaling;
    private Label _lblFactor;
    private Spinner _spinFactor;

    private class ActionConconiOptions extends ActionToolbarSlideout {

        @Override
        protected ToolbarSlideout createSlideout(final ToolBar toolbar) {

            return new SlideoutConconiOptions(_pageBook, toolbar, GRID_PREF_PREFIX, ConconiView.this);
        }
    }

    private void addPartListener() {
        _partListener = new IPartListener2() {

            @Override
            public void partActivated(final IWorkbenchPartReference partRef) {
            }

            @Override
            public void partBroughtToTop(final IWorkbenchPartReference partRef) {
            }

            @Override
            public void partClosed(final IWorkbenchPartReference partRef) {
                if (partRef.getPart(false) == ConconiView.this) {
                    saveTour();
                    saveState();
                }
            }

            @Override
            public void partDeactivated(final IWorkbenchPartReference partRef) {
            }

            @Override
            public void partHidden(final IWorkbenchPartReference partRef) {
            }

            @Override
            public void partInputChanged(final IWorkbenchPartReference partRef) {
            }

            @Override
            public void partOpened(final IWorkbenchPartReference partRef) {
            }

            @Override
            public void partVisible(final IWorkbenchPartReference partRef) {
            }
        };
        getViewSite().getPage().addPartListener(_partListener);
    }

    private void addPrefListener() {

        _prefChangeListener = new IPropertyChangeListener() {
            @Override
            public void propertyChange(final PropertyChangeEvent event) {

                final String property = event.getProperty();

                /*
                 * set a new chart configuration when the preferences has changed
                 */
                if (property.equals(GRID_HORIZONTAL_DISTANCE) || property.equals(GRID_VERTICAL_DISTANCE)
                        || property.equals(GRID_IS_SHOW_HORIZONTAL_GRIDLINES)
                        || property.equals(GRID_IS_SHOW_VERTICAL_GRIDLINES)
                //
                ) {

                    // grid has changed, update chart
                    updateChartProperties();
                }
            }
        };

        _prefStore.addPropertyChangeListener(_prefChangeListener);
    }

    /**
     * listen for events when a tour is selected
     */
    private void addSelectionListener() {

        _postSelectionListener = new ISelectionListener() {
            @Override
            public void selectionChanged(final IWorkbenchPart part, final ISelection selection) {

                if (part == ConconiView.this) {
                    return;
                }

                onSelectionChanged(selection);
            }
        };
        getSite().getPage().addPostSelectionListener(_postSelectionListener);
    }

    private void addTourEventListener() {

        _tourEventListener = new ITourEventListener() {

            @Override
            public void tourChanged(final IWorkbenchPart part, final TourEventId eventId, final Object eventData) {

                if (part == ConconiView.this) {
                    return;
                }

                if (eventId == TourEventId.TOUR_SELECTION && eventData instanceof ISelection) {

                    onSelectionChanged((ISelection) eventData);
                }
            }
        };

        TourManager.getInstance().addTourEventListener(_tourEventListener);
    }

    private void clearView() {

        _conconiTours = null;
        _selectedTour = null;

        if (_chartConconiTest != null) {
            _chartConconiTest.updateChart(null, false);
        }

        _pageBook.showPage(_page_NoTour);
    }

    private void createActions() {

        _actionConconiOptions = new ActionConconiOptions();

    }

    /**
     * @param conconiTours
     *            contains all tours which are displayed in the conconi chart, they can be valid or
     *            invalid
     * @param markedTour
     *            contains tour which should be marked in the chart, when <code>null</code> the
     *            first tour will be marked
     * @return
     */
    private ChartDataModel createChartDataModelConconiTest(final ArrayList<TourData> conconiTours,
            TourData markedTour) {

        // reset data
        _conconiDataForSelectedTour = null;

        final ChartDataModel chartDataModel = new ChartDataModel(ChartType.XY_SCATTER);

        final int serieLengthRaw = conconiTours.size();

        final TourData[] toursArray = conconiTours.toArray(new TourData[serieLengthRaw]);
        final ArrayList<TourData> validTourList = new ArrayList<TourData>();

        /*
         * get all tours which has valid data
         */
        for (int serieIndex = 0; serieIndex < serieLengthRaw; serieIndex++) {

            final TourData tourData = toursArray[serieIndex];

            final float[] tdPowerSerie = tourData.getPowerSerie();
            final float[] tdPulseSerie = tourData.pulseSerie;

            // check if required data series are available
            if (tdPowerSerie != null && tdPowerSerie.length != 0 && tdPulseSerie != null
                    && tdPulseSerie.length != 0) {
                validTourList.add(tourData);
            }
        }

        final int validDataLength = validTourList.size();
        final int lastTourIndex = validDataLength - 1;

        // display error when required data are not available
        if (validDataLength == 0) {

            chartDataModel.setErrorMessage(Messages.Conconi_Chart_InvalidData);

            return chartDataModel;
        }

        // ensure a tour is marked
        if (markedTour == null) {
            markedTour = validTourList.get(0);
        }

        _selectedTour = markedTour;

        final String prefGraphName = ICommonPreferences.GRAPH_COLORS + GraphColorManager.PREF_GRAPH_HEARTBEAT + "."; //$NON-NLS-1$

        final RGB rgbPrefLine = PreferenceConverter.getColor(//
                _commonPrefStore, prefGraphName + GraphColorManager.PREF_COLOR_LINE);

        final RGB rgbPrefDark = PreferenceConverter.getColor(//
                _commonPrefStore, prefGraphName + GraphColorManager.PREF_COLOR_DARK);

        final RGB rgbPrefBright = PreferenceConverter.getColor(//
                _commonPrefStore, prefGraphName + GraphColorManager.PREF_COLOR_BRIGHT);

        final double[][] powerSerie = new double[validDataLength][];
        final double[][] pulseSerie = new double[validDataLength][];
        final RGB[] rgbLine = new RGB[validDataLength];
        final RGB[] rgbDark = new RGB[validDataLength];
        final RGB[] rgbBright = new RGB[validDataLength];

        final TourData[] validTours = validTourList.toArray(new TourData[validTourList.size()]);
        int markedIndex = 0;
        float maxXValue = 0;

        /*
         * create data series which contain valid data, reduce data that the highes value for an x
         * value is displayed
         */
        for (int tourIndex = 0; tourIndex < validDataLength; tourIndex++) {

            final TourData tourData = validTours[tourIndex];

            final float[] tourPowerSerie = tourData.getPowerSerie();
            final float[] tourPulseSerie = tourData.pulseSerie;

            // check if required data series are available
            if (tourPowerSerie != null && tourPowerSerie.length != 0 && tourPulseSerie != null
                    && tourPulseSerie.length != 0) {

                final TDoubleArrayList maxXValues = new TDoubleArrayList();
                final TDoubleArrayList maxYValues = new TDoubleArrayList();

                float lastMaxY = Float.MIN_VALUE;
                float currentXValue = tourPowerSerie[0];

                // loop: all values in the current serie
                for (int valueIndex = 0; valueIndex < tourPowerSerie.length; valueIndex++) {

                    // check array bounds
                    if (valueIndex >= tourPulseSerie.length) {
                        break;
                    }

                    final float xValue = tourPowerSerie[valueIndex];
                    final float yValue = tourPulseSerie[valueIndex];

                    if (xValue == currentXValue) {

                        // get maximum y value for the same x value

                        if (yValue > lastMaxY) {
                            lastMaxY = yValue;
                        }

                    } else {

                        // next x value is displayed, keep last max y

                        maxXValues.add(currentXValue);
                        maxYValues.add(lastMaxY);

                        currentXValue = xValue;
                        lastMaxY = yValue;
                    }

                    // get max x value
                    if (currentXValue > maxXValue) {
                        maxXValue = currentXValue;
                    }
                }

                // get last value
                maxXValues.add(currentXValue);
                maxYValues.add(lastMaxY);

                powerSerie[tourIndex] = maxXValues.toArray();
                pulseSerie[tourIndex] = maxYValues.toArray();

                /*
                 * marked tour is displayed with pulse color
                 */
                if (tourData.equals(markedTour)) {

                    // get index of marked tour
                    markedIndex = tourIndex;

                    rgbLine[tourIndex] = rgbPrefLine;
                    rgbDark[tourIndex] = rgbPrefDark;
                    rgbBright[tourIndex] = rgbPrefBright;

                } else {

                    rgbLine[tourIndex] = DEFAULT_RGB;
                    rgbDark[tourIndex] = DEFAULT_RGB;
                    rgbBright[tourIndex] = DEFAULT_RGB;
                }
            }
        }

        /*
         * swap last tour with marked tour that the marked tour is painted at last to be not covered
         * by other tours
         */
        final double[] markedPowerSerie = powerSerie[markedIndex];
        final double[] markedPulseSerie = pulseSerie[markedIndex];
        final RGB markedRgbLine = rgbLine[markedIndex];
        final RGB markedRgbDark = rgbDark[markedIndex];
        final RGB markedRgbBright = rgbBright[markedIndex];

        powerSerie[markedIndex] = powerSerie[lastTourIndex];
        pulseSerie[markedIndex] = pulseSerie[lastTourIndex];
        rgbLine[markedIndex] = rgbLine[lastTourIndex];
        rgbDark[markedIndex] = rgbDark[lastTourIndex];
        rgbBright[markedIndex] = rgbBright[lastTourIndex];

        powerSerie[lastTourIndex] = markedPowerSerie;
        pulseSerie[lastTourIndex] = markedPulseSerie;
        rgbLine[lastTourIndex] = markedRgbLine;
        rgbDark[lastTourIndex] = markedRgbDark;
        rgbBright[lastTourIndex] = markedRgbBright;

        /*
         * power
         */
        final ChartDataXSerie xDataPower = new ChartDataXSerie(powerSerie);
        xDataPower.setLabel(net.tourbook.common.Messages.Graph_Label_Power);
        xDataPower.setUnitLabel(net.tourbook.common.Messages.Graph_Label_Power_Unit);

        /*
         * double is not yet supported for the y-axis
         */
        final float[][] pulseSerieFloat = Util.convertDoubleToFloat(pulseSerie);

        /*
         * pulse
         */
        _yDataPulse = new ChartDataYSerie(ChartType.XY_SCATTER, pulseSerieFloat);
        _yDataPulse.setYTitle(net.tourbook.common.Messages.Graph_Label_Heartbeat);
        _yDataPulse.setUnitLabel(net.tourbook.common.Messages.Graph_Label_Heartbeat_Unit);
        _yDataPulse.setDefaultRGB(rgbPrefLine);
        _yDataPulse.setRgbLine(rgbLine);
        _yDataPulse.setRgbDark(rgbDark);
        _yDataPulse.setRgbBright(rgbBright);

        // check x-data visible min value
        //adjust min/max values that the chart do not stick to a border
        xDataPower.forceXAxisMinValue(0);
        xDataPower.forceXAxisMaxValue(xDataPower.getVisibleMaxValue() + ADJUST_MAX_POWER_VALUE);

        _yDataPulse.forceYAxisMinValue(_yDataPulse.getVisibleMinValue() - ADJUST_MAX_PULSE_VALUE);
        _yDataPulse.forceYAxisMaxValue(_yDataPulse.getVisibleMaxValue() + ADJUST_MAX_PULSE_VALUE);

        // setup chart data model
        chartDataModel.setXData(xDataPower);
        chartDataModel.addYData(_yDataPulse);

        // create conconi data for the selected tour
        _conconiDataForSelectedTour = createConconiData(powerSerie[lastTourIndex], pulseSerie[lastTourIndex]);

        if (_chkExtendedScaling.getSelection()) {
            xDataPower.setScalingFactors(//
                    (double) _spinFactor.getSelection() / 10, maxXValue + ADJUST_MAX_POWER_VALUE);
        }

        /*
         * updata layer for regression lines
         */
        final ArrayList<IChartLayer> chartCustomLayers = new ArrayList<IChartLayer>();
        chartCustomLayers.add(_conconiLayer);

        _yDataPulse.setCustomForegroundLayers(chartCustomLayers);
        _yDataPulse.setCustomData(TourManager.CUSTOM_DATA_CONCONI_TEST, _conconiDataForSelectedTour);

        return chartDataModel;
    }

    private ConconiData createConconiData(final double[] powerSerie, final double[] pulseSerie) {

        final TDoubleArrayList maxXValues = new TDoubleArrayList();
        final TDoubleArrayList maxYValues = new TDoubleArrayList();

        double lastMaxY = Double.MIN_VALUE;
        double currentXValue = powerSerie[0];

        // loop: all values in the current serie
        for (int valueIndex = 0; valueIndex < powerSerie.length; valueIndex++) {

            // check array bounds
            if (valueIndex >= pulseSerie.length) {
                break;
            }

            final double xValue = powerSerie[valueIndex];
            final double yValue = pulseSerie[valueIndex];

            if (xValue == currentXValue) {

                // get maximum y value for the same x value

                if (yValue > lastMaxY) {
                    lastMaxY = yValue;
                }

            } else {

                // next x value is displayed, keep last max y

                maxXValues.add(currentXValue);
                maxYValues.add(lastMaxY);

                currentXValue = xValue;
                lastMaxY = yValue;
            }
        }

        // get last value
        maxXValues.add(currentXValue);
        maxYValues.add(lastMaxY);

        final ConconiData conconiData = new ConconiData();
        conconiData.maxXValues = maxXValues;
        conconiData.maxYValues = maxYValues;

        return conconiData;
    }

    @Override
    public void createPartControl(final Composite parent) {

        createUI(parent);

        createActions();
        fillToolbar();

        restoreState();
        enableControls();

        addPrefListener();
        addTourEventListener();
        addSelectionListener();
        addPartListener();

        // show conconi chart from selection service
        onSelectionChanged(getSite().getWorkbenchWindow().getSelectionService().getSelection());

        // check if tour chart is displayed
        if (_conconiTours == null) {
            showTourFromTourProvider();
        }
    }

    private void createUI(final Composite parent) {

        initUI(parent);

        _pageBook = new PageBook(parent, SWT.NONE);

        _page_NoTour = net.tourbook.ui.UI.createPage(_tk, _pageBook, Messages.UI_Label_TourIsNotSelected);

        createUI_10_ConconiTest(_pageBook);
    }

    private void createUI_10_ConconiTest(final Composite parent) {

        _page_ConconiTest = new Composite(parent, SWT.NONE);
        GridDataFactory.fillDefaults().grab(true, false).applyTo(_page_ConconiTest);
        GridLayoutFactory.fillDefaults().numColumns(1).spacing(0, 0).applyTo(_page_ConconiTest);
        //      _pageConconiTest.setBackground(Display.getCurrent().getSystemColor(SWT.COLOR_BLUE));
        {
            createUI_20_Options(_page_ConconiTest);
            createUI_60_ConconiChart(_page_ConconiTest);
        }
    }

    private void createUI_20_Options(final Composite parent) {

        final Composite container = new Composite(parent, SWT.NONE);
        GridDataFactory.fillDefaults().grab(true, false).applyTo(container);
        GridLayoutFactory.fillDefaults().spacing(0, 0).numColumns(2).applyTo(container);
        _page_ConconiTest.setBackground(Display.getCurrent().getSystemColor(SWT.COLOR_BLUE));
        {
            createUI_30_Test(container);
            createUI_40_ExtendedScaling(container);
        }
    }

    private void createUI_30_Test(final Composite parent) {

        final Composite container = new Composite(parent, SWT.NONE);
        GridDataFactory.fillDefaults().grab(true, false).applyTo(container);
        GridLayoutFactory.fillDefaults()//
                .numColumns(2).spacing(0, 0).extendedMargins(3, 3, 3, 0).applyTo(container);
        //      container.setBackground(Display.getCurrent().getSystemColor(SWT.COLOR_RED));
        {
            /*
             * label: tour
             */
            Label label = new Label(container, SWT.NONE);
            label.setText(Messages.Conconi_Chart_Label_Tour);
            label.setToolTipText(Messages.Conconi_Chart_Label_Tour_Tooltip);

            /*
             * combo: tour date/time
             */
            _comboTests = new Combo(container, SWT.DROP_DOWN | SWT.READ_ONLY);
            GridDataFactory.fillDefaults().hint(_pc.convertWidthInCharsToPixels(40), SWT.DEFAULT).grab(true, false)
                    .applyTo(_comboTests);
            _comboTests.setVisibleItemCount(20);
            _comboTests.addSelectionListener(new SelectionAdapter() {
                @Override
                public void widgetSelected(final SelectionEvent e) {
                    onSelectTour();
                }
            });

            /*
             * label: deflaction point
             */
            label = new Label(container, SWT.NONE);
            label.setText(Messages.Conconi_Chart_DeflactionPoint);

            final Composite deflContainer = new Composite(container, SWT.NONE);
            GridDataFactory.fillDefaults().grab(true, false).applyTo(deflContainer);
            GridLayoutFactory.fillDefaults().numColumns(2).applyTo(deflContainer);
            //         deflContainer.setBackground(Display.getCurrent().getSystemColor(SWT.COLOR_MAGENTA));
            {
                /*
                 * scale: deflection point
                 */
                _scaleDeflection = new Scale(deflContainer, SWT.HORIZONTAL);
                GridDataFactory.fillDefaults().grab(true, false).applyTo(_scaleDeflection);
                _scaleDeflection.addSelectionListener(new SelectionAdapter() {
                    @Override
                    public void widgetSelected(final SelectionEvent e) {
                        onSelectDeflection();
                    }
                });

                final Composite containerValues = new Composite(deflContainer, SWT.NONE);
                GridDataFactory.fillDefaults().grab(false, false).align(SWT.FILL, SWT.CENTER)
                        .applyTo(containerValues);
                GridLayoutFactory.fillDefaults().numColumns(2).applyTo(containerValues);
                //      container.setBackground(Display.getCurrent().getSystemColor(SWT.COLOR_CYAN));
                {
                    // label: heartbeat value
                    _lblDeflactionPulse = new Label(containerValues, SWT.TRAIL);
                    GridDataFactory.fillDefaults().align(SWT.FILL, SWT.CENTER)
                            .hint(_pc.convertWidthInCharsToPixels(4), SWT.DEFAULT).applyTo(_lblDeflactionPulse);

                    // label: heartbeat unit
                    label = new Label(containerValues, SWT.NONE);
                    GridDataFactory.fillDefaults().align(SWT.FILL, SWT.CENTER).applyTo(label);
                    label.setText(net.tourbook.common.Messages.Graph_Label_Heartbeat_Unit);

                    // label: power value
                    _lblDeflactionPower = new Label(containerValues, SWT.TRAIL);
                    GridDataFactory.fillDefaults().align(SWT.FILL, SWT.CENTER)
                            .hint(_pc.convertWidthInCharsToPixels(4), SWT.DEFAULT).applyTo(_lblDeflactionPower);

                    // label: power unit
                    label = new Label(containerValues, SWT.NONE);
                    GridDataFactory.fillDefaults().align(SWT.FILL, SWT.CENTER).applyTo(label);
                    label.setText(UI.UNIT_POWER);
                }
            }
        }
    }

    private void createUI_40_ExtendedScaling(final Composite parent) {

        final Composite container = new Composite(parent, SWT.NONE);
        GridDataFactory.fillDefaults().grab(false, false).applyTo(container);
        GridLayoutFactory.swtDefaults().numColumns(2).applyTo(container);
        {
            /*
             * checkbox: log scaling
             */
            _chkExtendedScaling = new Button(container, SWT.CHECK);
            GridDataFactory//
                    .fillDefaults().align(SWT.FILL, SWT.CENTER).span(2, 1).applyTo(_chkExtendedScaling);
            _chkExtendedScaling.setText(Messages.Conconi_Chart_Chk_LogScaling);
            _chkExtendedScaling.setToolTipText(Messages.Conconi_Chart_Chk_LogScaling_Tooltip);
            _chkExtendedScaling.addSelectionListener(new SelectionAdapter() {
                @Override
                public void widgetSelected(final SelectionEvent e) {
                    enableControls();
                    updateChart_30_NewTour(_selectedTour);
                }
            });

            /*
             * label: factor
             */
            _lblFactor = new Label(container, SWT.NONE);
            GridDataFactory.fillDefaults()//
                    .indent(16, 0).align(SWT.BEGINNING, SWT.CENTER).applyTo(_lblFactor);
            _lblFactor.setText(Messages.Conconi_Chart_Label_ScalingFactor);

            /*
             * spinner: factor
             */
            _spinFactor = new Spinner(container, SWT.BORDER);
            GridDataFactory.fillDefaults()//
                    .hint(_hintDefaultSpinnerWidth, SWT.DEFAULT).align(SWT.BEGINNING, SWT.CENTER)
                    .applyTo(_spinFactor);
            _spinFactor.setMinimum(1);
            _spinFactor.setMaximum(100);
            _spinFactor.setDigits(1);

            _spinFactor.addModifyListener(new ModifyListener() {
                @Override
                public void modifyText(final ModifyEvent e) {
                    if (_isUpdateUI) {
                        return;
                    }
                    updateChart_30_NewTour(_selectedTour);
                }
            });
            _spinFactor.addSelectionListener(new SelectionAdapter() {
                @Override
                public void widgetSelected(final SelectionEvent e) {
                    if (_isUpdateUI) {
                        return;
                    }
                    updateChart_30_NewTour(_selectedTour);
                }
            });
            _spinFactor.addMouseWheelListener(new MouseWheelListener() {
                @Override
                public void mouseScrolled(final MouseEvent event) {
                    Util.adjustSpinnerValueOnMouseScroll(event);
                    if (_isUpdateUI) {
                        return;
                    }
                    updateChart_30_NewTour(_selectedTour);
                }
            });

        }
    }

    /**
     * chart: conconi test
     */
    private void createUI_60_ConconiChart(final Composite parent) {

        _chartConconiTest = new Chart(parent, SWT.FLAT);
        GridDataFactory.fillDefaults().grab(true, true).applyTo(_chartConconiTest);

        // set chart properties
        updateChartProperties();

        _conconiLayer = new ChartLayerConconiTest();
    }

    @Override
    public void dispose() {

        getViewSite().getPage().removePartListener(_partListener);
        getSite().getPage().removeSelectionListener(_postSelectionListener);
        TourManager.getInstance().removeTourEventListener(_tourEventListener);
        _prefStore.removePropertyChangeListener(_prefChangeListener);

        super.dispose();
    }

    private void enableControls() {

        final boolean isExtScaling = _chkExtendedScaling.getSelection();

        _lblFactor.setEnabled(isExtScaling);
        _spinFactor.setEnabled(isExtScaling);
    }

    private void fillToolbar() {

        /*
         * View toolbar
         */
        final IToolBarManager tbm = getViewSite().getActionBars().getToolBarManager();
        tbm.add(_actionConconiOptions);

        // update toolbar which creates the slideout
        tbm.update(true);
    }

    private void initUI(final Composite parent) {

        _pc = new PixelConverter(parent);
        _tk = new FormToolkit(parent.getDisplay());

        _hintDefaultSpinnerWidth = UI.IS_LINUX ? SWT.DEFAULT : _pc.convertWidthInCharsToPixels(UI.IS_OSX ? 10 : 5);
    }

    private void onSelectDeflection() {

        // update conconi data
        final int newDeflection = _scaleDeflection.getSelection();
        _conconiDataForSelectedTour.selectedDeflection = newDeflection;
        _yDataPulse.setCustomData(TourManager.CUSTOM_DATA_CONCONI_TEST, _conconiDataForSelectedTour);

        _modifiedTourDeflection = newDeflection;

        updateUI_20_ConconiValues();
    }

    private void onSelectionChanged(final ISelection selection) {

        if (_isSaving) {
            return;
        }

        if (_pageBook != null && _pageBook.isDisposed()) {
            return;
        }

        if (selection instanceof SelectionTourData) {

            final TourData tourData = ((SelectionTourData) selection).getTourData();
            if (tourData != null) {
                updateChart_20(tourData);
            }

        } else if (selection instanceof SelectionTourIds) {

            final SelectionTourIds selectionTourId = (SelectionTourIds) selection;
            final ArrayList<Long> tourIds = selectionTourId.getTourIds();
            if (tourIds != null && tourIds.size() > 0) {
                updateChart_12(tourIds);
            }

        } else if (selection instanceof SelectionTourId) {

            final SelectionTourId selectionTourId = (SelectionTourId) selection;
            final Long tourId = selectionTourId.getTourId();

            updateChart_10(tourId);

        } else if (selection instanceof SelectionDeletedTours) {

            clearView();
        }
    }

    private void onSelectTour() {

        if (_isSelectionDisabled) {
            return;
        }

        int selectedIndex = _comboTests.getSelectionIndex();
        if (selectedIndex == -1) {
            selectedIndex = 0;
        }

        updateChart_30_NewTour(_conconiTours.get(selectedIndex));
    }

    private void restoreState() {

        _isUpdateUI = true;
        {
            _chkExtendedScaling.setSelection(_state.getBoolean(STATE_CONCONI_IS_LOG_SCALING));
            _spinFactor.setSelection(Util.getStateInt(_state, STATE_CONCONI_SCALING_FACTOR, 20));
        }
        _isUpdateUI = false;
    }

    private void saveState() {

        // check if UI is disposed
        if (_pageBook.isDisposed()) {
            return;
        }

        _state.put(STATE_CONCONI_IS_LOG_SCALING, _chkExtendedScaling.getSelection());
        _state.put(STATE_CONCONI_SCALING_FACTOR, _spinFactor.getSelection());
    }

    private void saveTour() {

        if (_originalTourDeflection == _modifiedTourDeflection) {
            return;
        }

        _selectedTour.setConconiDeflection(_modifiedTourDeflection);

        TourData savedTour;
        _isSaving = true;
        {
            savedTour = TourManager.saveModifiedTour(_selectedTour);
        }
        _isSaving = false;

        if (savedTour != null) {

            _selectedTour = savedTour;

            // replace tour with saved tour
            final TourData[] conconiTours = _conconiTours.toArray(new TourData[_conconiTours.size()]);
            for (int tourIndex = 0; tourIndex < conconiTours.length; tourIndex++) {
                if (conconiTours[tourIndex].equals(savedTour)) {
                    _conconiTours.set(tourIndex, savedTour);
                    break;
                }
            }
        }

        _originalTourDeflection = _modifiedTourDeflection = -1;
    }

    @Override
    public void setFocus() {

        if (_page_ConconiTest != null && _page_ConconiTest.isVisible()) {
            _chartConconiTest.setFocus();
        }
    }

    private void showTourFromTourProvider() {

        _pageBook.showPage(_page_NoTour);

        // a tour is not displayed, find a tour provider which provides a tour
        Display.getCurrent().asyncExec(new Runnable() {
            @Override
            public void run() {

                // validate widget
                if (_pageBook.isDisposed()) {
                    return;
                }

                /*
                 * check if tour was set from a selection provider
                 */
                if (_conconiTours != null) {
                    return;
                }

                final ArrayList<TourData> selectedTours = TourManager.getSelectedTours();
                if (selectedTours != null && selectedTours.size() > 0) {
                    updateChart_22(selectedTours);
                }
            }
        });
    }

    private void updateChart_10(final Long tourId) {

        final ArrayList<Long> tourIds = new ArrayList<Long>();
        tourIds.add(tourId);

        updateChart_12(tourIds);
    }

    private void updateChart_12(final ArrayList<Long> tourIds) {
        updateChart_22(TourManager.getInstance().getTourData(tourIds));
    }

    private void updateChart_20(final TourData tourData) {

        if (tourData == null) {
            return;
        }

        final ArrayList<TourData> tourDataList = new ArrayList<TourData>();
        tourDataList.add(tourData);

        updateChart_22(tourDataList);
    }

    private void updateChart_22(final ArrayList<TourData> tourDataList) {

        /*
         * tour editor is not opened because it can cause a recursive attempt to active a part in
         * the middle of activating a part
         */
        if (tourDataList == null || tourDataList.size() == 0 || TourManager.isTourEditorModified(false)) {
            // nothing to do
            clearView();
            return;
        }

        // sort tours by date/time
        Collections.sort(tourDataList);

        _conconiTours = tourDataList;

        updateUI_10_SetupConconi();
        updateChart_30_NewTour(null);

        _pageBook.showPage(_page_ConconiTest);

        return;
    }

    /**
     * @param markedTour
     *            contains a tour which is marked in the conconi chart
     */
    private void updateChart_30_NewTour(final TourData markedTour) {

        // save modified tour before chart data for a new tour is created
        saveTour();

        if (_conconiTours == null) {
            // bugfix for: http://sourceforge.net/tracker/?func=detail&atid=890601&aid=3269916&group_id=179799
            _pageBook.showPage(_page_NoTour);
            return;
        }

        final ChartDataModel conconiChartDataModel = createChartDataModelConconiTest(_conconiTours, markedTour);

        updateUI_12_SetupNewTour();

        _chartConconiTest.updateChart(conconiChartDataModel, true, true);

        /*
         * force the chart to be repainted because updating the conconi layer requires that the
         * chart is already painted (it requires drawing data)
         */
        _chartConconiTest.resizeChart();
        updateUI_20_ConconiValues();
    }

    private void updateChartProperties() {

        net.tourbook.ui.UI.updateChartProperties(_chartConconiTest, GRID_PREF_PREFIX);
    }

    private void updateUI_10_SetupConconi() {

        _isSelectionDisabled = true;
        {
            /*
             * tour combo box
             */
            _comboTests.removeAll();

            for (final TourData tourData : _conconiTours) {
                _comboTests.add(TourManager.getTourTitleDetailed(tourData));
            }

            _comboTests.select(0);
            _comboTests.setEnabled(_conconiTours.size() > 1);
        }
        _isSelectionDisabled = false;
    }

    private void updateUI_12_SetupNewTour() {

        if (_conconiDataForSelectedTour == null) {
            return;
        }

        // update deflection scale
        final int maxDeflection = _conconiDataForSelectedTour.maxXValues.size();
        final int lastXIndex = maxDeflection - 1;
        _scaleDeflection.setMaximum(maxDeflection > 0 ? lastXIndex : 0);

        // ensure that too much scale ticks are displayed
        final int pageIncrement = maxDeflection < 20 ? 1
                : maxDeflection < 100 ? 5 : maxDeflection < 1000 ? 50 : 100;

        _scaleDeflection.setPageIncrement(pageIncrement);

        int tourDeflection = _selectedTour.getConconiDeflection();

        // ensure deflection is within x-values
        if (tourDeflection > lastXIndex) {
            tourDeflection = lastXIndex;
        }
        if (tourDeflection < 0) {
            tourDeflection = 0;
        }

        _originalTourDeflection = _modifiedTourDeflection = tourDeflection;

        // update conconi data
        _conconiDataForSelectedTour.selectedDeflection = tourDeflection;

        // update UI
        _scaleDeflection.setSelection(tourDeflection);

    }

    private void updateUI_20_ConconiValues() {

        if (_conconiDataForSelectedTour == null) {
            return;
        }

        // deflation values
        final int scaleIndex = _scaleDeflection.getSelection();
        final int pulseValue = (int) _conconiDataForSelectedTour.maxYValues.get(scaleIndex);
        final int powerValue = (int) _conconiDataForSelectedTour.maxXValues.get(scaleIndex);

        _lblDeflactionPulse.setText(Integer.toString(pulseValue));
        _lblDeflactionPower.setText(Integer.toString(powerValue));

        // update conconi layer
        _chartConconiTest.updateCustomLayers();
    }

}