All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.bidib.wizard.mvc.stepcontrol.view.StepControlPanel Maven / Gradle / Ivy

There is a newer version: 2.0.29
Show newest version
package org.bidib.wizard.mvc.stepcontrol.view;

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Frame;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyVetoException;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.net.URL;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;

import javax.swing.BorderFactory;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JSlider;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.ListModel;
import javax.swing.ListSelectionModel;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.ListDataEvent;
import javax.swing.event.ListDataListener;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import javax.swing.filechooser.FileNameExtensionFilter;

import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.MapUtils;
import org.bidib.jbidibc.core.node.ConfigurationVariable;
import org.bidib.jbidibc.exchange.vendorcv.CVType;
import org.bidib.jbidibc.exchange.vendorcv.DataType;
import org.bidib.jbidibc.messages.AccessoryState;
import org.bidib.jbidibc.messages.Feature;
import org.bidib.jbidibc.messages.enums.AccessoryExecutionState;
import org.bidib.jbidibc.messages.enums.FeatureEnum;
import org.bidib.jbidibc.messages.utils.ByteUtils;
import org.bidib.jbidibc.messages.utils.ProductUtils;
import org.bidib.wizard.api.locale.Resources;
import org.bidib.wizard.api.model.Accessory;
import org.bidib.wizard.api.model.NodeInterface;
import org.bidib.wizard.api.model.SwitchingNodeInterface;
import org.bidib.wizard.api.model.listener.CvDefinitionRequestListener;
import org.bidib.wizard.api.model.listener.DefaultFeedbackPortListener;
import org.bidib.wizard.api.model.listener.PortValueListener;
import org.bidib.wizard.api.service.console.ConsoleColor;
import org.bidib.wizard.api.utils.NodeUtils;
import org.bidib.wizard.client.common.controller.FeedbackPortStatusChangeProvider;
import org.bidib.wizard.client.common.converter.StringToUnsignedLongConverter;
import org.bidib.wizard.client.common.dialog.EscapeDialog;
import org.bidib.wizard.client.common.text.WizardComponentFactory;
import org.bidib.wizard.client.common.view.BasicPopupMenu;
import org.bidib.wizard.client.common.view.BusyFrame;
import org.bidib.wizard.client.common.view.TabPanelProvider;
import org.bidib.wizard.client.common.view.cvdef.CvDefinitionTreeTableModel;
import org.bidib.wizard.client.common.view.cvdef.CvNode;
import org.bidib.wizard.client.common.view.cvdef.CvNodeUtils;
import org.bidib.wizard.client.common.view.cvdef.LongCvNode;
import org.bidib.wizard.client.common.view.listener.TabStatusListener;
import org.bidib.wizard.client.common.view.statusbar.StatusBar;
import org.bidib.wizard.common.context.DefaultApplicationContext;
import org.bidib.wizard.common.model.settings.ExperimentalSettingsInterface;
import org.bidib.wizard.common.model.settings.WizardSettingsInterface;
import org.bidib.wizard.common.script.switching.AccessoryScripting;
import org.bidib.wizard.common.service.SettingsService;
import org.bidib.wizard.common.utils.ImageUtils;
import org.bidib.wizard.core.dialog.FileDialog;
import org.bidib.wizard.model.ports.FeedbackPort;
import org.bidib.wizard.model.ports.MotorPort;
import org.bidib.wizard.model.ports.SoundPort;
import org.bidib.wizard.model.status.FeedbackPortStatus;
import org.bidib.wizard.model.status.SoundPortStatus;
import org.bidib.wizard.model.status.SpeedSteps;
import org.bidib.wizard.model.stepcontrol.TurnTableType;
import org.bidib.wizard.mvc.common.view.cvdefinition.CvDefinitionPanelProvider;
import org.bidib.wizard.mvc.common.view.cvdefinition.CvValueUtils;
import org.bidib.wizard.mvc.main.controller.CvDefinitionPanelController;
import org.bidib.wizard.mvc.main.model.MainModel;
import org.bidib.wizard.mvc.main.view.component.DefaultTabSelectionPanel;
import org.bidib.wizard.mvc.main.view.panel.listener.TabComponentCreator;
import org.bidib.wizard.mvc.main.view.panel.listener.TabSelectionListener;
import org.bidib.wizard.mvc.stepcontrol.controller.StepControlControllerInterface;
import org.bidib.wizard.mvc.stepcontrol.model.AccelarationScaleEnum;
import org.bidib.wizard.mvc.stepcontrol.model.AspectExecutionModel;
import org.bidib.wizard.mvc.stepcontrol.model.AspectReference;
import org.bidib.wizard.mvc.stepcontrol.model.ConfigurationWizardModel;
import org.bidib.wizard.mvc.stepcontrol.model.ConfigurationWizardModel.WizardStatus;
import org.bidib.wizard.mvc.stepcontrol.model.CvConsoleModel;
import org.bidib.wizard.mvc.stepcontrol.model.Gearing;
import org.bidib.wizard.mvc.stepcontrol.model.MicroStepsEnum;
import org.bidib.wizard.mvc.stepcontrol.model.MotorSizeType;
import org.bidib.wizard.mvc.stepcontrol.model.MovementScaleEnum;
import org.bidib.wizard.mvc.stepcontrol.model.StepControlAspect;
import org.bidib.wizard.mvc.stepcontrol.model.StepControlAspect.AspectPersistanceStatus;
import org.bidib.wizard.mvc.stepcontrol.model.StepControlAspect.Polarity;
import org.bidib.wizard.mvc.stepcontrol.model.StepControlModel;
import org.bidib.wizard.mvc.stepcontrol.model.StepControlModel.OperationModeEnum;
import org.bidib.wizard.mvc.stepcontrol.view.AspectEditorPanel.EditorType;
import org.bidib.wizard.mvc.stepcontrol.view.excel.DataExchangeException;
import org.bidib.wizard.mvc.stepcontrol.view.excel.ExcelAspectReader;
import org.bidib.wizard.mvc.stepcontrol.view.excel.ExcelAspectReader.MotorData;
import org.bidib.wizard.mvc.stepcontrol.view.excel.ImportAspect;
import org.bidib.wizard.utils.FileUtils;
import org.oxbow.swingbits.dialog.task.TaskDialogs;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.Assert;

import com.jgoodies.binding.PresentationModel;
import com.jgoodies.binding.adapter.BoundedRangeAdapter;
import com.jgoodies.binding.list.IndirectListModel;
import com.jgoodies.binding.list.SelectionInList;
import com.jgoodies.binding.value.ConverterFactory;
import com.jgoodies.binding.value.ConverterValueModel;
import com.jgoodies.binding.value.ValueHolder;
import com.jgoodies.binding.value.ValueModel;
import com.jgoodies.common.base.Objects;
import com.jgoodies.forms.builder.ButtonBarBuilder;
import com.jgoodies.forms.builder.FormBuilder;
import com.jgoodies.forms.debug.FormDebugPanel;
import com.jgoodies.forms.factories.Paddings;
import com.jidesoft.grid.DefaultExpandableRow;
import com.jidesoft.grid.RowStripeTableStyleProvider;
import com.jidesoft.grid.SortableTableModel;
import com.jidesoft.grid.TablePopupMenuInstaller;
import com.jidesoft.pane.CollapsiblePane;
import com.jidesoft.swing.DefaultOverlayable;
import com.jidesoft.swing.JideBoxLayout;
import com.jidesoft.swing.JideButton;
import com.jidesoft.swing.JideSplitPane;
import com.jidesoft.swing.JideSwingUtilities;
import com.jidesoft.swing.JideToggleButton;
import com.jidesoft.swing.NullJideButton;
import com.vlsolutions.swing.toolbars.ToolBarConstraints;
import com.vlsolutions.swing.toolbars.ToolBarPanel;
import com.vlsolutions.swing.toolbars.VLToolBar;

import eu.hansolo.steelseries.extras.Led;

public class StepControlPanel
    implements TabSelectionListener, CvDefinitionPanelProvider, TabPanelProvider, PortValueListener,
    AccessoryScripting {

    private static final Logger LOGGER = LoggerFactory.getLogger(StepControlPanel.class);

    private static final String EMERGENCY_STOP = "EMERGENCY STOP";

    private static final String OPERATING = "OPERATING";

    private static final String HOMING_IN_PROGRESS = "HOMING";

    private static final String UNKNOWN = "UNKNOWN";

    private static final String READ = "read";

    private static final String WRITE = "write";

    private static final long INACTIVE_POSITION_VALUE = 0xFFFFFFFFL;

    private final StepControlModel stepControlModel;

    private final TabStatusListener tabStatusListener;

    private final FeedbackPortStatusChangeProvider feedbackPortStatusChangeProvider;

    private AspectTable aspectTable;

    private JPanel component;

    private NodeInterface selectedNode;

    private IndirectListModel aspectSelection;

    private PresentationModel stepControlPresentationModel;

    private ImageIcon accessoryErrorIcon;

    private ImageIcon accessorySuccessfulIcon;

    private ImageIcon accessoryWaitIcon;

    private ImageIcon accessoryUnknownIcon;

    private ImageIcon emergencyStopIcon;

    private ImageIcon normalOperatingIcon;

    private ImageIcon homingInProgressIcon;

    private ImageIcon selectedIcon;

    private ImageIcon unselectedIcon;

    private ImageIcon emptyIcon;

    private ImageIcon errorIcon;

    private JLabel operationalStateIconLabel = new JLabel();

    private JLabel executionStateIconLabel = new JLabel();

    private final List cvDefinitionRequestListeners = new ArrayList<>();

    private Map mapKeywordToNode;

    private final List requiredConfigVariables = new ArrayList<>();

    private final MainModel mainModel;

    private boolean initialized;

    private final StatusBar statusBar;

    private final List fieldsToUpdate = new ArrayList<>();

    private final List occupancyLeds = new ArrayList<>();

    private JButton readCvButton;

    private JButton writeCvButton;

    private VLToolBar toolbarCvDefinition;

    // turntable
    private double degreeOffset;

    private TurntableIconPanel turntableIconPanel;

    private AngleRenderer angleRenderer;

    private boolean showMotorSliderEditor = false;

    private MotorSlider motorSliderEditor;

    private MotorPort selectedMotorPort;

    private StepControlControllerInterface stepControlController;

    private Map functionButtonMap = new HashMap<>();

    private JPanel functionButtonParentPanel;

    private JSlider speedSlider;

    private ValueModel speedModel;

    private BoundedRangeAdapter boundedRangeAdapterSpeed;

    private JSlider accelSlider;

    private BoundedRangeAdapter boundedRangeAdapterAccel;

    private JSlider decelSlider;

    private BoundedRangeAdapter boundedRangeAdapterDecel;

    private JideToggleButton soundActiveButton;

    private JButton setPosition;

    private JButton rotateTurnTableFlipButton;

    private JButton leftStepsPosition;

    private JButton leftSingleStepPosition;

    private JButton rightSingleStepPosition;

    private JButton rightStepsPosition;

    private ScriptPanel scriptPanel;

    private final SettingsService settingsService;

    private final AspectExecutionModel aspectExecutionModel;

    private TableModelListener aspectTableModelListener;

    private JPanel detailsPanel;

    private FormBuilder positionFormBuilder;

    private JPanel buttonBarContainer;

    public StepControlPanel(final MainModel mainModel, final StepControlModel stepControlModel,
        final SettingsService settingsService, final TabStatusListener tabStatusListener,
        final FeedbackPortStatusChangeProvider feedbackPortStatusChangeProvider,
        StepControlControllerInterface stepControlController, final StatusBar statusBar) {
        this.tabStatusListener = tabStatusListener;
        this.mainModel = mainModel;
        this.feedbackPortStatusChangeProvider = feedbackPortStatusChangeProvider;
        this.stepControlController = stepControlController;

        this.stepControlModel = stepControlModel;
        this.settingsService = settingsService;

        this.aspectExecutionModel = new AspectExecutionModel();
        // the moving aspects are in accessory 0
        this.aspectExecutionModel.setAccessoryId(0);

        this.statusBar = statusBar;
    }

    private SortableTableModel getSortableTableModel() {
        return ((SortableTableModel) aspectTable.getModel());
    }

    private static class DebugPanel extends FormDebugPanel implements TabSelectionListener {
        private static final long serialVersionUID = 1L;

        private final TabSelectionListener parent;

        public DebugPanel(TabSelectionListener parent) {
            this.parent = parent;
        }

        @Override
        public void tabSelected(boolean selected) {
            LOGGER.debug("Select tab, current component: is StepControlPanel.");

            parent.tabSelected(selected);
        }
    }

    /**
     * Create the step control panel component.
     */
    public void createComponent() {

        final DefaultTabSelectionPanel container = new DefaultTabSelectionPanel(this) {
            private static final long serialVersionUID = 1L;

            @Override
            public boolean equals(Object other) {
                if (other instanceof TabComponentCreator) {
                    TabComponentCreator creator = (TabComponentCreator) other;
                    // TODO if more than a single instance is available this must be changed
                    if (creator.getCreator() instanceof StepControlPanel) {
                        return true;
                    }
                }
                return false;
            }

            @Override
            public int hashCode() {
                return super.hashCode();
            }

            @Override
            public void tabSelected(boolean selected) {
                LOGGER.info("Select tab, current component: is StepControlPanel, selected: {}", selected);

                StepControlPanel.this.tabSelected(selected);
            }
        };

        boolean debug = false;

        FormBuilder formBuilder =
            FormBuilder
                .create().columns("p, 3dlu, fill:p:g").rows("p, 3dlu, p")
                .panel(debug ? new DebugPanel(this) : container);
        formBuilder.border(Paddings.TABBED_DIALOG);

        // load the icons
        initializeAccessoryStateIcons();

        aspectSelection =
            new SelectionInList<>((ListModel) stepControlModel.getStepControlAspectsListModel());

        stepControlPresentationModel = new PresentationModel<>(new ValueHolder(stepControlModel, true));

        operationalStateIconLabel = new JLabel(UNKNOWN);
        formBuilder.add(Resources.getString(StepControlPanel.class, "operational_state")).xy(1, 1);
        formBuilder.add(operationalStateIconLabel).xy(3, 1);

        operationalStateIconLabel.setIcon(accessoryUnknownIcon);

        executionStateIconLabel = new JLabel();
        formBuilder.add(Resources.getString(StepControlPanel.class, "execution_state")).xy(1, 3);
        formBuilder.add(executionStateIconLabel).xy(3, 3);
        executionStateIconLabel.setIcon(accessoryUnknownIcon);

        formBuilder.appendRows("5dlu");

        // create the aspect table
        final JPanel overlayAspectTable = createAspectTable();

        // create the 'detail panel'
        this.detailsPanel = createDetailsPanel();

        // create the split pane for the table and the details panel
        JideSplitPane jideSplitPane = new JideSplitPane(JideSplitPane.HORIZONTAL_SPLIT);
        jideSplitPane.add(overlayAspectTable, JideBoxLayout.FLEXIBLE);
        jideSplitPane.add(new JScrollPane(this.detailsPanel), JideBoxLayout.FLEXIBLE);
        jideSplitPane.setShowGripper(true);

        formBuilder.appendRows("fill:300dlu:g");

        formBuilder.add(jideSplitPane).xyw(1, 5, 3);

        component = formBuilder.build();

        // the name is displayed on the tab
        component.setName(getName());

        // add the pending changes listener to the stepControlModel
        stepControlModel.addPropertyChangeListener(new PropertyChangeListener() {

            @Override
            public void propertyChange(PropertyChangeEvent evt) {
                LOGGER.info("Property has changed, evt: {}", evt);

                switch (evt.getPropertyName()) {
                    case StepControlModel.PROPERTYNAME_OPERATIONAL_MODE:

                        switch (stepControlModel.getOperationalMode()) {
                            case homingInProgress:
                                LOGGER.info("Set the operational state label to homing in progress mode.");
                                operationalStateIconLabel.setIcon(homingInProgressIcon);
                                operationalStateIconLabel.setText(HOMING_IN_PROGRESS);
                                break;
                            case operational:
                                LOGGER.info("Set the operational state label to operating mode.");
                                operationalStateIconLabel.setIcon(normalOperatingIcon);
                                operationalStateIconLabel.setText(OPERATING);
                                break;
                            case emergencyStop:
                                LOGGER.info("Set the operational state label to emergency mode.");
                                operationalStateIconLabel.setIcon(emergencyStopIcon);
                                operationalStateIconLabel.setText(EMERGENCY_STOP);
                                break;
                            default:
                                LOGGER.info("Set the operational state label to unknown.");
                                operationalStateIconLabel.setIcon(accessoryUnknownIcon);
                                operationalStateIconLabel.setText(UNKNOWN);
                                break;
                        }
                        break;
                    case StepControlModel.PROPERTYNAME_SELECTED_ASPECT:
                        // update the selected aspect
                        if (stepControlModel.getSelectedAspect() == null) {
                            executionStateIconLabel.setToolTipText(null);
                            executionStateIconLabel.setIcon(null);
                            executionStateIconLabel.setText(UNKNOWN);
                        }
                        break;
                    case StepControlModel.PROPERTYNAME_CURRENT_DEGREES:
                        // no update of pending changes
                        break;
                    case StepControlModel.PROPERTYNAME_TARGET_DEGREES:
                        // no update of pending changes
                        break;
                    case StepControlModel.PROPERTYNAME_MOTOR_PORT:
                        // no update of pending changes

                        // selectedMotorPort = stepControlModel.getMotorPort();
                        // LOGGER.info("The selected motor port has changed, selectedMotorPort: {}", selectedMotorPort);
                        //
                        // motorSliderEditor.setPort(selectedMotorPort);
                        // TODO enable the motor slider when the firmware is ready
                        // motorSliderEditor.setEnabled(selectedMotorPort != null);
                        break;
                    case StepControlModel.PROPERTYNAME_SOUND_PORTS:
                        // no update of pending changes

                        List soundPorts = stepControlModel.getSoundPorts();
                        LOGGER.info("The sound ports have changed, soundPorts: {}", soundPorts);

                        functionButtonParentPanel.removeAll();

                        if (CollectionUtils.isNotEmpty(soundPorts)) {
                            final List functionButtons = new ArrayList<>();
                            createFunctionButtonPanel(functionButtonParentPanel, functionButtons);
                        }
                        functionButtonParentPanel.revalidate();
                        break;

                    case StepControlModel.PROPERTYNAME_TURNTABLE_TYPE:
                        turnTableTypeChanged(StepControlPanel.this.aspectTable, false);
                        break;
                    case StepControlModel.PROPERTYNAME_SOUND_ACTIVE:
                        // LOGGER.info("The sound active property has changed.");
                        if (evt.getNewValue() instanceof Boolean) {
                            boolean soundActive = stepControlModel.isSoundActive();
                            LOGGER.info("The sound active flag has changed: {}", soundActive);
                            if (soundActiveButton != null) {
                                soundActiveButton.setSelected(!soundActive);
                            }
                        }
                        break;
                    case StepControlModel.PROPERTYNAME_CURRENT_POSITION:
                        LOGGER.info("The current position was changed.");
                        break;
                    default:
                        LOGGER
                            .info("The property has changed: {}. This will set the pending changes marker on the tab.",
                                evt.getPropertyName());
                        tabStatusListener.updatePendingChanges(component, true);
                        break;
                }
            }
        });

        speedSlider.addChangeListener(new ChangeListener() {

            @Override
            public void stateChanged(ChangeEvent e) {

                // the slider must not fire when setting the values
                if (!initialized || selectedNode == null) {
                    LOGGER
                        .info(
                            "The panel is not initialized or no node is selected, discard the state change of speed.");
                    return;
                }

                JSlider source = (JSlider) e.getSource();
                if (!source.getValueIsAdjusting() && initialized) {
                    firePerformSpeed();
                }
            }
        });

        accelSlider.addChangeListener(new ChangeListener() {

            @Override
            public void stateChanged(ChangeEvent e) {

                // the slider must not fire when setting the values
                if (!initialized || selectedNode == null) {
                    LOGGER
                        .info(
                            "The panel is not initialized or no node is selected, discard the state change of accel.");
                    return;
                }

                JSlider source = (JSlider) e.getSource();
                if (!source.getValueIsAdjusting() && initialized) {
                    firePerformAccel();
                }
            }
        });

        decelSlider.addChangeListener(new ChangeListener() {

            @Override
            public void stateChanged(ChangeEvent e) {

                // the slider must not fire when setting the values
                if (!initialized || selectedNode == null) {
                    LOGGER
                        .info(
                            "The panel is not initialized or no node is selected, discard the state change of decel.");
                    return;
                }

                JSlider source = (JSlider) e.getSource();
                if (!source.getValueIsAdjusting() && initialized) {
                    firePerformDecel();
                }
            }
        });

        // motorSliderEditor.addSliderValueChangeListener(new SliderValueChangeListener() {
        //
        // @Override
        // public void stateChanged(ChangeEvent e, boolean isAdjusting, int value) {
        // // only handle if not adjusting
        // if (!isAdjusting) {
        // MotorPort motorPort = stepControlModel.getMotorPort();
        //
        // if (motorPort != null) {
        // LOGGER.info("Update motor port: {}, new value: {}", motorPort.getDebugString(), value);
        //
        // motorPort.setValue(value);
        //
        // stepControlController.setMotorPortValue(motorPort);
        // }
        //
        // }
        // }
        // });

        toolbarCvDefinition = new VLToolBar("stepControlCvDefinition");
        addToolBarButtons(toolbarCvDefinition);
        addToolBar(toolbarCvDefinition, new ToolBarConstraints(0, 2));
        // initially invisible
        toolbarCvDefinition.setVisible(false);

        feedbackPortStatusChangeProvider.addFeedbackPortListener(new DefaultFeedbackPortListener() {
            @Override
            public Class getPortClass() {
                return FeedbackPort.class;
            }

            @Override
            public void statusChanged(final NodeInterface node, final FeedbackPort port) {
                if (selectedNode != null && ProductUtils.isStepControl(selectedNode.getUniqueId())) {

                    if (port.getId() < occupancyLeds.size()) {
                        FeedbackPortStatus status = port.getStatus();
                        LOGGER.info("The status of the feedback port has changed: {}, status: {}", port, status);
                        Led occupancyLed = occupancyLeds.get(port.getId());
                        if (occupancyLed != null) {

                            SwingUtilities
                                .invokeLater(
                                    () -> occupancyLed.setLedOn(status == FeedbackPortStatus.OCCUPIED ? true : false));
                        }
                    }
                    else {
                        LOGGER.debug("Port number of feedback port is out of range: {}", port.getId());
                    }
                }
                else {
                    LOGGER.info("Do not update occupancy leds because the selected node is not a StepControl.");
                }
            }
        });
    }

    private JPanel createDetailsPanel() {

        boolean debugDetail = false;

        final FormBuilder detailFormBuilder =
            FormBuilder
                .create().columns("p, 3dlu, p, 3dlu, p, 3dlu, p, 3dlu, p, 3dlu, p, 3dlu, p:g")
                .rows("p, 3dlu, p, 3dlu, p, 3dlu, p").panel(debugDetail ? new FormDebugPanel() : new JPanel());
        detailFormBuilder.border(Paddings.DLU4);

        // prepare the configuration wizard button
        final ImageIcon iconConfigurationWizard =
            ImageUtils.createImageIcon(StepControlPanel.class, "/icons/16x16/wrench.png");

        JButton startConfigurationWizard =
            new JButton(Resources.getString(StepControlPanel.class, "button.wizard"), iconConfigurationWizard);
        startConfigurationWizard.addActionListener(new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent e) {
                LOGGER.info("Open configuration wizard.");
                fireConfigurationWizard();
            }
        });

        detailFormBuilder.add(buildLeftAlignedButtonBar(startConfigurationWizard)).xyw(1, 1, 13);

        boolean debugPosition = false;

        // final FormBuilder positionFormBuilder =
        this.positionFormBuilder =
            FormBuilder
                .create().columns("max(40dlu;p), 3dlu, p, 6dlu, p, 3dlu, max(40dlu;p), 3dlu, p:g").rows("p")
                .panel(debugPosition ? new FormDebugPanel() : new JPanel());
        positionFormBuilder.border(Paddings.EMPTY);

        final ValueModel currentPositionConverterModel =
            new ConverterValueModel(
                stepControlPresentationModel.getModel(StepControlModel.PROPERTYNAME_CURRENT_POSITION),
                new StringToUnsignedLongConverter());
        JTextField currentPosition = WizardComponentFactory.createTextField(currentPositionConverterModel);
        currentPosition.setEditable(false);

        positionFormBuilder.add(currentPosition).xy(1, 1);

        JButton getPosition = new JButton(Resources.getString(StepControlPanel.class, "get_position"));
        getPosition.setToolTipText(Resources.getString(StepControlPanel.class, "get_position.tooltip"));
        getPosition.addActionListener(new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent e) {
                LOGGER.info("Get the position value.");
                fireGetPosition();
            }
        });
        positionFormBuilder.add(getPosition).xy(3, 1);

        final ValueModel directPositionModel = new ValueHolder();
        final ValueModel directPositionConverterModel =
            new ConverterValueModel(directPositionModel, new StringToUnsignedLongConverter());
        JTextField directPosition = WizardComponentFactory.createTextField(directPositionConverterModel, false);
        positionFormBuilder.addLabel(Resources.getString(StepControlPanel.class, "direct")).xy(5, 1);
        positionFormBuilder.add(directPosition).xy(7, 1);

        // add button for setpos and getpos
        setPosition = new JButton(Resources.getString(StepControlPanel.class, "set_position"));
        setPosition.setToolTipText(Resources.getString(StepControlPanel.class, "set_position.tooltip"));
        setPosition.addActionListener(new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent e) {

                Long directPosition = (Long) directPositionModel.getValue();
                if (directPosition != null) {
                    LOGGER.info("Set the direct position value: {}", directPosition);

                    fireSetDirectPosition(directPosition.intValue());
                }
            }
        });

        // rotate turntable button
        ImageIcon turntableRotateFlipIcon =
            ImageUtils.loadImageIcon(StepControlPanel.class, "/icons/stepcontrol/turntable-rotate-half.png", 16, 16);

        rotateTurnTableFlipButton = new JButton(turntableRotateFlipIcon);
        rotateTurnTableFlipButton.setToolTipText(Resources.getString(StepControlPanel.class, "rotateFlip.tooltip"));

        rotateTurnTableFlipButton.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                LOGGER.info("Rotate turntable flip");
                firePerformFlipRotate();
            }
        });

        // Change position by steps buttons
        leftStepsPosition = new JButton(Resources.getString(StepControlPanel.class, "left_steps_position"));
        leftStepsPosition.setToolTipText(Resources.getString(StepControlPanel.class, "left_steps_position.tooltip"));
        leftStepsPosition.addActionListener(new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent e) {
                LOGGER.info("10 steps in left position.");

                Long directPosition = stepControlModel.getCurrentPosition();
                if (directPosition != null) {
                    int newPosition = directPosition.intValue();
                    newPosition -= 10;

                    LOGGER.info("Set the position value: {}", newPosition);
                    fireSetDirectPosition(newPosition);
                }
            }
        });

        leftSingleStepPosition = new JButton(Resources.getString(StepControlPanel.class, "left_single_step_position"));
        leftSingleStepPosition
            .setToolTipText(Resources.getString(StepControlPanel.class, "left_single_step_position.tooltip"));
        leftSingleStepPosition.addActionListener(new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent e) {
                LOGGER.info("Single step in left position.");

                Long directPosition = stepControlModel.getCurrentPosition();
                if (directPosition != null) {
                    int newPosition = directPosition.intValue();
                    newPosition -= 1;

                    LOGGER.info("Set the position value: {}", newPosition);
                    fireSetDirectPosition(newPosition);
                }
            }
        });

        rightSingleStepPosition =
            new JButton(Resources.getString(StepControlPanel.class, "right_single_step_position"));
        rightSingleStepPosition
            .setToolTipText(Resources.getString(StepControlPanel.class, "right_single_step_position.tooltip"));
        rightSingleStepPosition.addActionListener(new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent e) {
                LOGGER.info("Single step in right position.");

                Long directPosition = stepControlModel.getCurrentPosition();
                if (directPosition != null) {
                    int newPosition = directPosition.intValue();
                    newPosition += 1;

                    LOGGER.info("Set the position value: {}", newPosition);
                    fireSetDirectPosition(newPosition);
                }
            }
        });

        rightStepsPosition = new JButton(Resources.getString(StepControlPanel.class, "right_steps_position"));
        rightStepsPosition.setToolTipText(Resources.getString(StepControlPanel.class, "right_steps_position.tooltip"));
        rightStepsPosition.addActionListener(new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent e) {
                LOGGER.info("10 steps in right position.");

                Long directPosition = stepControlModel.getCurrentPosition();
                if (directPosition != null) {
                    int newPosition = directPosition.intValue();
                    newPosition += 10;

                    LOGGER.info("Set the position value: {}", newPosition);
                    fireSetDirectPosition(newPosition);
                }
            }
        });

        this.buttonBarContainer = new JPanel();
        buildPositionsButtonBar(buttonBarContainer, positionFormBuilder);
        positionFormBuilder.add(buttonBarContainer).xy(9, 1);

        // final JComponent buttonBar =
        // buildLeftAlignedButtonBar(setPosition, leftStepsPosition, leftSingleStepPosition, rightSingleStepPosition,
        // rightStepsPosition, rotateTurnTableFlipButton);
        // positionFormBuilder.add(buttonBar).xy(9, 1);

        detailFormBuilder.addLabel(Resources.getString(StepControlPanel.class, "position")).xy(1, 3);
        detailFormBuilder.add(positionFormBuilder.build()).xyw(3, 3, 11);

        // create the speed related silders
        JPanel speedSlidersPanel = createSpeedSlidersPanel();

        detailFormBuilder.add(speedSlidersPanel).xyw(1, 5, 13);

        // add the occupancy leds
        Led occupancy1 = new Led();
        occupancy1.setPreferredSize(new Dimension(32, 32));
        Led occupancy2 = new Led();
        occupancy2.setPreferredSize(new Dimension(32, 32));
        Led occupancy3 = new Led();
        occupancy3.setPreferredSize(new Dimension(32, 32));
        Led occupancy4 = new Led();
        occupancy4.setPreferredSize(new Dimension(32, 32));

        occupancyLeds.add(occupancy1);
        occupancyLeds.add(occupancy2);
        occupancyLeds.add(occupancy3);
        occupancyLeds.add(occupancy4);

        detailFormBuilder.addLabel(Resources.getString(StepControlPanel.class, "occupancy")).xy(1, 7);
        detailFormBuilder.add(buildLeftAlignedButtonBar(occupancy1, occupancy2, occupancy3, occupancy4)).xyw(3, 7, 11);

        // add the lower part with animation and sound buttons
        detailFormBuilder.appendRows("3dlu, top:p:g");

        // create the turntable panel
        JPanel turnTablePanel = createTurnTablePanel();
        detailFormBuilder.add(turnTablePanel).xyw(1, 9, 13);

        return detailFormBuilder.build();
    }

    private JComponent buildPositionsButtonBar(final JPanel container, final FormBuilder positionFormBuilder) {

        final JComponent buttonBar =
            TurnTableType.pendular != stepControlModel.getTurnTableType()
                ? buildLeftAlignedButtonBar(container, setPosition, leftStepsPosition, leftSingleStepPosition,
                    rightSingleStepPosition, rightStepsPosition, rotateTurnTableFlipButton)
                : buildLeftAlignedButtonBar(container, setPosition, leftStepsPosition, leftSingleStepPosition,
                    rightSingleStepPosition, rightStepsPosition);

        return buttonBar;
    }

    private JPanel createSpeedSlidersPanel() {

        boolean debug = false;
        final FormBuilder detailFormBuilder =
            FormBuilder
                .create().columns("p, 3dlu, p, 3dlu, p:g:fill").rows("p, 3dlu, p, 3dlu, p")
                .panel(debug ? new FormDebugPanel() : new JPanel());

        detailFormBuilder.border(Paddings.EMPTY);

        int row = 1;

        if (showMotorSliderEditor) {

            detailFormBuilder.appendRows("3dlu, p");

            motorSliderEditor = new MotorSlider(-SpeedSteps.DCC128.getSteps(), SpeedSteps.DCC128.getSteps());

            // create the component
            // motorSliderEditor.createComponent(0);

            // disable the motor slider by default
            motorSliderEditor.setEnabled(false);

            JPanel sliderComponent = motorSliderEditor.getComponent();

            detailFormBuilder.addLabel("Motor").xy(1, row);
            detailFormBuilder.add(sliderComponent).xy(3, row);

            row += 2;
        }

        // slider for speed
        speedModel = stepControlPresentationModel.getModel(StepControlModel.PROPERTYNAME_SPEED);
        speedSlider = new JSlider();
        speedSlider.setOpaque(false);

        int maxSpeed = 2001;
        int minSpeed = 1;
        LOGGER.info("Use initial speed range, min: {}, max: {}", minSpeed, maxSpeed);

        boundedRangeAdapterSpeed = new BoundedRangeAdapter(speedModel, 0, minSpeed, maxSpeed);
        speedSlider.setModel(boundedRangeAdapterSpeed);

        JLabel speedLabel =
            WizardComponentFactory
                .createLabel(ConverterFactory.createStringConverter(speedModel, new DecimalFormat("#####")));
        speedLabel.setPreferredSize(new Dimension(40, speedLabel.getPreferredSize().height));
        speedLabel.setHorizontalAlignment(SwingConstants.RIGHT);
        detailFormBuilder.addLabel(Resources.getString(StepControlPanel.class, "speed")).xy(1, row);
        detailFormBuilder.add(speedLabel).xy(3, row);
        detailFormBuilder.add(speedSlider).xy(5, row);

        row += 2;

        // slider for accel
        ValueModel accelModel = stepControlPresentationModel.getModel(StepControlModel.PROPERTYNAME_ACCEL);
        accelSlider = new JSlider() {
            private static final long serialVersionUID = 1L;

            @Override
            public void setValueIsAdjusting(boolean b) {
                try {
                    super.setValueIsAdjusting(b);
                }
                catch (Exception ex) {
                    LOGGER.warn("Set the new value failed.", ex);
                    // JOptionPane.showConfirmDialog(detailFormBuilder.getPanel(), "set accel failed.");
                    showErrorDialog(detailFormBuilder.getPanel(),
                        Resources.getString(StepControlPanel.class, "adjust-accel-value-failed.title"),
                        Resources.getString(StepControlPanel.class, "adjust-accel-value-failed.instruction"),
                        Resources.getString(StepControlPanel.class, "adjust-accel-value-failed.text"), ex);
                }
            }

            @Override
            public void setValue(int n) {
                try {
                    super.setValue(n);
                }
                catch (Exception ex) {
                    LOGGER.warn("Set the new value failed.", ex);
                    // JOptionPane.showConfirmDialog(detailFormBuilder.getPanel(), "set accel failed.");
                    showErrorDialog(detailFormBuilder.getPanel(),
                        Resources.getString(StepControlPanel.class, "adjust-accel-value-failed.title"),
                        Resources.getString(StepControlPanel.class, "adjust-accel-value-failed.instruction"),
                        Resources.getString(StepControlPanel.class, "adjust-accel-value-failed.text"), ex);
                }
            }

            private void showErrorDialog(
                JComponent component, String title, String instruction, String text, Exception ex) {
                TaskDialogs
                    .build(JOptionPane.getFrameForComponent(component), instruction, text).title(title)
                    .showException(ex);
            }
        };

        boundedRangeAdapterAccel = new BoundedRangeAdapter(accelModel, 1, 1, 65536);
        accelSlider.setModel(boundedRangeAdapterAccel);
        accelSlider.setOpaque(false);

        JLabel accelLabel =
            WizardComponentFactory
                .createLabel(ConverterFactory.createStringConverter(accelModel, new DecimalFormat("#####")));
        accelLabel.setHorizontalAlignment(SwingConstants.RIGHT);
        detailFormBuilder.addLabel(Resources.getString(StepControlPanel.class, "accel")).xy(1, row);
        detailFormBuilder.add(accelLabel).xy(3, row);
        detailFormBuilder.add(accelSlider).xy(5, row);

        row += 2;

        // slider for decel
        ValueModel decelModel = stepControlPresentationModel.getModel(StepControlModel.PROPERTYNAME_DECEL);
        decelSlider = new JSlider() {
            private static final long serialVersionUID = 1L;

            @Override
            public void setValueIsAdjusting(boolean b) {
                try {
                    super.setValueIsAdjusting(b);
                }
                catch (Exception ex) {
                    LOGGER.warn("Set the new value failed.", ex);
                    // JOptionPane.showConfirmDialog(detailFormBuilder.getPanel(), "set decel failed.");
                    showErrorDialog(detailFormBuilder.getPanel(),
                        Resources.getString(StepControlPanel.class, "adjust-decel-value-failed.title"),
                        Resources.getString(StepControlPanel.class, "adjust-decel-value-failed.instruction"),
                        Resources.getString(StepControlPanel.class, "adjust-decel-value-failed.text"), ex);
                }
            }

            @Override
            public void setValue(int n) {
                try {
                    super.setValue(n);
                }
                catch (Exception ex) {
                    LOGGER.warn("Set the new value failed.", ex);
                    // JOptionPane.showConfirmDialog(detailFormBuilder.getPanel(), "set decel failed.");
                    showErrorDialog(detailFormBuilder.getPanel(),
                        Resources.getString(StepControlPanel.class, "adjust-decel-value-failed.title"),
                        Resources.getString(StepControlPanel.class, "adjust-decel-value-failed.instruction"),
                        Resources.getString(StepControlPanel.class, "adjust-decel-value-failed.text"), ex);
                }
            }

            private void showErrorDialog(
                JComponent component, String title, String instruction, String text, Exception ex) {
                TaskDialogs
                    .build(JOptionPane.getFrameForComponent(component), instruction, text).title(title)
                    .showException(ex);
            }
        };

        boundedRangeAdapterDecel = new BoundedRangeAdapter(decelModel, 1, 1, 65536);
        decelSlider.setModel(boundedRangeAdapterDecel);
        decelSlider.setOpaque(false);

        JLabel decelLabel =
            WizardComponentFactory
                .createLabel(ConverterFactory.createStringConverter(decelModel, new DecimalFormat("#####")));
        decelLabel.setHorizontalAlignment(SwingConstants.RIGHT);
        detailFormBuilder.addLabel(Resources.getString(StepControlPanel.class, "decel")).xy(1, row);
        detailFormBuilder.add(decelLabel).xy(3, row);
        detailFormBuilder.add(decelSlider).xy(5, row);

        JPanel speedSliderPanel = detailFormBuilder.build();

        CollapsiblePane pane =
            new CollapsiblePane(Resources.getString(StepControlPanel.class, "pane-speed-and-accelaration"));

        final ImageIcon speedAndAccelarationIcon =
            ImageUtils.createImageIcon(StepControlPanel.class, "/icons/16x16/wrench.png");
        pane.setIcon(speedAndAccelarationIcon);

        pane.setContentPane(JideSwingUtilities.createTopPanel(speedSliderPanel));
        try {
            pane.setCollapsed(true);
        }
        catch (PropertyVetoException ex) {
            LOGGER.warn("Collapse pane was vetoed.", ex);
        }

        return pane;
    }

    private JPanel createTurnTablePanel() {

        boolean debugTTFB = false;
        final FormBuilder turntableFormBuilder =
            FormBuilder
                .create().columns("p, 3dlu, p, 10dlu, p:g").rows("top:p:g")
                .panel(debugTTFB ? new FormDebugPanel() : new JPanel());

        // add the turntable image
        turntableIconPanel = prepareTurntableIconPanel();

        // create a panel with the turntable animation
        JPanel contentPanel = new JPanel(new BorderLayout());
        contentPanel.setMinimumSize(new Dimension(140, 140));
        contentPanel.setPreferredSize(new Dimension(140, 140));
        contentPanel.setOpaque(true);
        contentPanel.add(turntableIconPanel, BorderLayout.CENTER);

        turntableFormBuilder.add(contentPanel).xy(1, 1);

        // create the panel for the sound function buttons
        functionButtonParentPanel = new JPanel();
        turntableFormBuilder.add(functionButtonParentPanel).xy(3, 1);

        if (settingsService.getWizardSettings().isPowerUser()) {
            // prepare the script panel
            scriptPanel = new ScriptPanel(this, settingsService);

            JPanel panel = scriptPanel.createPanel();

            panel
                .setBorder(BorderFactory
                    .createTitledBorder(BorderFactory.createEtchedBorder(),
                        Resources.getString(getClass(), "script") + ":"));

            turntableFormBuilder.add(panel).xy(5, 1);
        }
        else {
            LOGGER.info("The script panel for StepControl is skipped because the user is not a power user.");
        }

        return turntableFormBuilder.build();
    }

    private TurntableIconPanel prepareTurntableIconPanel() {

        final ExperimentalSettingsInterface experimentalSettings = this.settingsService.getExperimentalSettings();
        experimentalSettings
            .addPropertyChangeListener(ExperimentalSettingsInterface.PROPERTY_TURNTABLE_ANGLE_OFFSET, evt -> {

                this.degreeOffset = experimentalSettings.getTurntableAngleOffset();

                double currentDegrees = stepControlModel.getTurntableCurrentDegrees();
                setTurntableDegrees(currentDegrees);
            });

        // initial offset
        this.degreeOffset = experimentalSettings.getTurntableAngleOffset();

        ImageIcon turntableBackgroundIcon =
            ImageUtils.loadImageIcon(StepControlPanel.class, "/icons/stepcontrol/turntable-bg.png", 120, 120);
        ImageIcon turntableBackgroundLinearIcon =
            ImageUtils.loadImageIcon(StepControlPanel.class, "/icons/stepcontrol/turntable-bg-linear.png", 120, 120);
        ImageIcon turntableBackgroundPendularIcon =
            ImageUtils.loadImageIcon(StepControlPanel.class, "/icons/stepcontrol/turntable-bg-pendular.png", 120, 120);
        ImageIcon turntableBasicIcon =
            ImageUtils.loadImageIcon(StepControlPanel.class, "/icons/stepcontrol/turntable-platform.png", 110, 110);
        ImageIcon turntablePendularIcon =
            ImageUtils.loadImageIcon(StepControlPanel.class, "/icons/stepcontrol/turntable-cableways.png", 40, 60);

        TurntableIconPanel turntableIconPanel =
            new TurntableIconPanel(turntableBackgroundIcon, turntableBackgroundLinearIcon,
                turntableBackgroundPendularIcon, turntableBasicIcon, turntablePendularIcon, degreeOffset,
                TurnTableType.unknown);
        turntableIconPanel.prepareComponent();

        turntableIconPanel.setMinimumSize(new Dimension(140, 140));
        turntableIconPanel.setPreferredSize(new Dimension(140, 140));
        turntableIconPanel.setBounds(0, 0, 140, 140);

        return turntableIconPanel;
    }

    private void turnTableTypeChanged(final AspectTable aspectTable, boolean initial) {

        final TurnTableType turnTableType = stepControlModel.getTurnTableType();

        LOGGER.info("The turntable type has changed: {}", turnTableType);

        // refresh the aspects
        aspectTable.turnTableTypeChanged(turnTableType);

        if (this.turntableIconPanel != null) {
            this.turntableIconPanel.setTurnTableType(turnTableType);
        }

        if (this.rotateTurnTableFlipButton != null) {

            this.buttonBarContainer.removeAll();
            buildPositionsButtonBar(this.buttonBarContainer, positionFormBuilder);

            this.detailsPanel.updateUI();
        }

        if (!initial && selectedNode != null) {

            int maxConfiguredAspects = StepControlControllerInterface.MAX_CONFIGURED_ASPECTS;
            switch (turnTableType) {
                case round:
                    maxConfiguredAspects = StepControlControllerInterface.MAX_CONFIGURED_ASPECTS / 2;
                    break;
                case pendular:
                    maxConfiguredAspects = 2;
                    break;
                default:
                    break;
            }
            stepControlModel.setMaxConfiguredAspects(maxConfiguredAspects);
            LOGGER
                .info("The turnTableType was set to: {}, maxConfiguredAspects: {}", turnTableType,
                    maxConfiguredAspects);

            final Map cvNumberToNodeMap = getCvNumberToNodeMap(selectedNode);

            final AtomicBoolean errorDetected = new AtomicBoolean();
            // create the list of aspects with the values from the config variables
            final List configuredAspects =
                prepareConfiguredAspects(cvNumberToNodeMap, errorDetected);

            // set the aspects silently
            try {
                aspectTable.getModel().removeTableModelListener(aspectTableModelListener);

                stepControlModel.setStepControlAspects(configuredAspects);
            }
            finally {
                aspectTable.getModel().addTableModelListener(aspectTableModelListener);
            }

            this.aspectTable.expandAllRows();
        }
    }

    /**
     * Create the aspect table
     */
    private JPanel createAspectTable() {

        final JPanel contentPanel = new JPanel();
        contentPanel.setLayout(new BorderLayout());

        final AspectTableModel tableModel =
            new AspectTableModel(aspectSelection, new String[] { //
                Resources.getString(StepControlPanel.class, "aspect"), //
                Resources.getString(StepControlPanel.class, "position"), //
                Resources.getString(StepControlPanel.class, "angle"), //
                Resources.getString(StepControlPanel.class, "polarity") //
            }, this.stepControlModel);

        // this is the callback listener for the update of aspect
        final AspectCallbackListener aspectCallbackListener = new AddAspectCallbackListener();

        // prepare the icons
        selectedIcon = ImageUtils.createImageIcon(StepControlPanel.class, "/icons/stepcontrol/arrow_straight.png");
        unselectedIcon = ImageUtils.createImageIcon(StepControlPanel.class, "/icons/stepcontrol/arrow_switch.png");

        // create the table
        this.aspectTable =
            new AspectTable(tableModel, Resources.getString(StepControlPanel.class, "emptyTable"), selectedIcon,
                unselectedIcon, aspectCallbackListener);

        this.aspectTable
            .setTableStyleProvider(new RowStripeTableStyleProvider(UIManager.getColor("tableRowStripe.background"),
                UIManager.getColor("tableRowStripe.alternativeBackground")));
        this.aspectTable.setName("Aspect Table");

        // aspectTable.setShowGrid(false);
        this.aspectTable.setRowHeight(24);

        this.aspectTable.setSortingEnabled(false);
        // sort the table automatically
        this.aspectTable.setAutoResort(true);

        this.aspectTable.sortColumn(AspectTableModel.COLUMN_POSITION);
        this.aspectTable.setShowSortOrderNumber(false);

        this.aspectTable.setRestoreSelectionAndRowHeightAutomatically(true);

        this.aspectTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
        this.aspectTable.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
            @Override
            public void valueChanged(ListSelectionEvent e) {
                int row = aspectTable.getSelectedRow();
                if (row != -1) {
                    aspectTable.expandRow(row);
                }
            }
        });

        this.aspectTable.createComponentFactory(stepControlModel);

        this.aspectTable.getColumnModel().getColumn(AspectTableModel.COLUMN_ASPECT).setPreferredWidth(80);
        this.aspectTable.getColumnModel().getColumn(AspectTableModel.COLUMN_ASPECT).setMaxWidth(80);

        // create a renderer for the aspect column that renders the correct row number
        this.aspectTable
            .getColumnModel().getColumn(AspectTableModel.COLUMN_ASPECT)
            .setCellRenderer(new AspectCellRenderer(this.stepControlModel));

        this.aspectTable.getColumnModel().getColumn(AspectTableModel.COLUMN_POSITION).setPreferredWidth(100);
        this.aspectTable
            .getColumnModel().getColumn(AspectTableModel.COLUMN_POSITION)
            .setCellRenderer(new StepControlAspectCellRenderer(stepControlModel, emptyIcon, errorIcon));

        // this.aspectTable.setPreferredScrollableViewportSize(new Dimension(500, 200));
        this.aspectTable.setFillsViewportHeight(true);
        this.aspectTable.setFillsViewportWithStripe(false);

        final JScrollPane scrollPane = new JScrollPane(this.aspectTable);

        final DefaultOverlayable overlayAspectTable = new DefaultOverlayable(scrollPane);
        this.aspectTable.getModel().addTableModelListener(new TableModelListener() {
            @Override
            public void tableChanged(TableModelEvent e) {
                // set the overlay for empty table visible
                overlayAspectTable.setOverlayVisible(aspectTable.getModel().getRowCount() == 0);
            }
        });

        final NullJideButton createAspectLinkButton = new NullJideButton(this.aspectTable.getEmptyTableText());
        createAspectLinkButton.setButtonStyle(NullJideButton.HYPERLINK_STYLE);
        createAspectLinkButton.addActionListener(evt -> {
            fireCreateNewAspect();
        });
        overlayAspectTable.addOverlayComponent(createAspectLinkButton);

        contentPanel.add(overlayAspectTable, BorderLayout.CENTER);

        final TablePopupMenuInstaller installer = new TablePopupMenuInstaller(this.aspectTable) {
            @Override
            protected JPopupMenu createPopupMenu() {
                LOGGER.info("Create the popup menu.");
                return new AspectTablePopupMenu();
            }

            @Override
            protected void customizeMenuItems(JTable table, JPopupMenu popup, int clickingRow, int clickingColumn) {
                LOGGER.info("Customize the popup menu: {}", popup);

                if (clickingRow > -1 && table.getSelectedRow() != clickingRow) {

                    // check if the current selected row is dirty
                    int selectedRow = aspectTable.getSelectedRow();
                    if (selectedRow > -1) {
                        if (!aspectTable.checkAspectEditorIsDirty(selectedRow)) {
                            LOGGER.info("Check if aspect editor is dirty failed. Do not show the add aspect editor.");

                            // remove all items to prevent show
                            popup.removeAll();
                            return;
                        }
                    }

                    table.setRowSelectionInterval(clickingRow, clickingRow);
                }
                super.customizeMenuItems(table, popup, clickingRow, clickingColumn);
            }
        };

        // force refresh the aspects
        turnTableTypeChanged(this.aspectTable, true);

        aspectSelection.addListDataListener(new ListDataListener() {

            @Override
            public void intervalRemoved(ListDataEvent e) {
                LOGGER.info("intervalRemoved, e: {}", e);
                tableModel.fireTableDataChanged();

            }

            @Override
            public void intervalAdded(ListDataEvent e) {
                LOGGER.info("intervalAdded");
                tableModel.fireTableDataChanged();

            }

            @Override
            public void contentsChanged(ListDataEvent e) {
                LOGGER.info("contentsChanged, resort the table.");
                aspectTable.resort();

                LOGGER.info("contentsChanged, scroll to top.");
                aspectTable.scrollToTop();
            }
        });

        return contentPanel;
    }

    protected void firePerformFlipRotate() {
        LOGGER.info("Rotate the turntable half.");

        fireGetPosition();

        Long value = stepControlModel.getCurrentPosition();

        if (value == null) {
            LOGGER.warn("The position value is null.");
            return;
        }

        Long totalSteps = stepControlModel.getTotalSteps();
        if (totalSteps == null) {
            LOGGER.warn("The total steps value is null.");
            return;
        }

        int currentPosition = value.intValue();

        // check if the current position is on an aspect and if so use the paired aspect as target
        if (stepControlModel.getSelectedAspect() != null
            && stepControlModel.getSelectedAspect().getPosition() == currentPosition) {
            AspectReference oppositeAspect = stepControlModel.getSelectedAspect().getOppositeAspect();
            LOGGER.info("Flip turntable to opposite aspect: {}", oppositeAspect);

            firePerformAspect((StepControlAspect) oppositeAspect);
            return;
        }

        long targetPosition = currentPosition + totalSteps / 2;
        if (targetPosition >= totalSteps) {
            targetPosition -= totalSteps;
        }

        LOGGER.info("Calculated target position: {}", targetPosition);

        try {

            moveToTargetPosition((int) targetPosition);
        }
        catch (PositionOutOfRangeException ex) {
            LOGGER.info("Set direct position failed: {}", targetPosition);

            JOptionPane
                .showMessageDialog(component,
                    Resources.getString(StepControlPanel.class, "set_position_failed_message", targetPosition),
                    Resources.getString(StepControlPanel.class, "set_position_title"), JOptionPane.ERROR_MESSAGE);
        }

    }

    protected void fireGetPosition() {
        LOGGER.info("Read the current position value.");

        try {
            final List cvList = new ArrayList<>();

            final Function, List> prepareFunction =
                (cvList1) -> prepareGetCurrentPosition(cvList1);

            final Function, List> loadFunction =
                (cvList1) -> fireLoadConfigVariablesPosition(cvList1);

            final Function, List> prepareAndLoad =
                prepareFunction.andThen(loadFunction);

            List result = prepareAndLoad.apply(cvList);
            LOGGER.info("Fetched the current position: {}", result);

            Long currentPosition = Long.parseLong(result.get(0).getValue());
            stepControlModel.setCurrentPosition(currentPosition);

        }
        catch (Exception ex) {
            LOGGER.warn("Get the new {} value failed.", StepControlKeywords.CVKEY_CURRENT_POSITION, ex);
        }
    }

    private List prepareGetCurrentPosition(final List cvList) {

        LOGGER.info("prepareGetCurrentPosition, current items in fieldsToUpdate: {}", fieldsToUpdate);

        fieldsToUpdate.clear();

        // add the current position
        final ConfigurationVariable cv = new ConfigurationVariable(StepControlKeywords.CVKEY_CURRENT_POSITION, null);
        addConfigVariableDistinct(cvList, cv);

        final Map cvNumberToNodeMap = getCvNumberToNodeMap(selectedNode);

        CvNode cvNode = cvNumberToNodeMap.get(StepControlKeywords.CVKEY_CURRENT_POSITION);
        if (cvNode == null) {
            CVType cvType = new CVType();
            cvType.setNumber(StepControlKeywords.CVKEY_CURRENT_POSITION);
            cvType.setType(DataType.STRING);
            cvNode = new CvNode(cvType, cv);
            LOGGER.info("Add the transient CvNode for current position: {}", cvNode);

            cvNumberToNodeMap.put(StepControlKeywords.CVKEY_CURRENT_POSITION, cvNode);
        }

        return cvList;
    }

    protected void fireSetDirectPosition(Integer directPosition) {

        if (directPosition == null) {
            LOGGER.warn("The position value is null.");
            return;
        }
        int currentPosition = directPosition.intValue();

        try {
            moveToTargetPosition(currentPosition);
        }
        catch (PositionOutOfRangeException ex) {
            LOGGER.info("Set direct position failed: {}", currentPosition);

            JOptionPane
                .showMessageDialog(component,
                    Resources.getString(StepControlPanel.class, "set_position_failed_message", currentPosition),
                    Resources.getString(StepControlPanel.class, "set_position_title"), JOptionPane.ERROR_MESSAGE);
        }

    }

    /**
     * Move the turntable to the provided position.
     * 
     * @param targetPosition
     *            the new position
     * @throws PositionOutOfRangeException
     */
    protected void moveToTargetPosition(int targetPosition) throws PositionOutOfRangeException {

        LOGGER.info("Write the new target position value: {}", targetPosition);

        int maxPosition = 0;
        Long totalSteps = stepControlModel.getTotalSteps();
        if (totalSteps != null) {
            maxPosition = totalSteps.intValue();
        }
        if (targetPosition < 0 || targetPosition > maxPosition) {
            LOGGER.warn("Position is outside range: {}", targetPosition);
            throw new PositionOutOfRangeException(targetPosition, 0, maxPosition - 1, -1);
        }

        try {

            final List cvList = new ArrayList<>();
            // add the current position
            ConfigurationVariable cv =
                new ConfigurationVariable(StepControlKeywords.CVKEY_CURRENT_POSITION, Integer.toString(targetPosition));
            cvList.add(cv);

            final Map cvNumberToNodeMap = getCvNumberToNodeMap(selectedNode);

            CvNode cvNode = cvNumberToNodeMap.get(StepControlKeywords.CVKEY_CURRENT_POSITION);
            if (cvNode == null) {
                CVType cvType = new CVType();
                cvType.setNumber(StepControlKeywords.CVKEY_CURRENT_POSITION);
                cvType.setType(DataType.STRING);
                cvNode = new CvNode(cvType, cv);
                LOGGER.info("Add the transient CvNode for current position: {}", cvNode);

                cvNumberToNodeMap.put(StepControlKeywords.CVKEY_CURRENT_POSITION, cvNode);
            }
            else {
                cvNode.setNewValue(Integer.toString(targetPosition));
            }

            // write the config variables to the node
            fireWriteConfigVariablesPosition(cvList);
        }
        catch (Exception ex) {
            LOGGER.warn("Set the new {} value failed.", StepControlKeywords.CVKEY_CURRENT_POSITION, ex);
        }
    }

    public void setTurntableDegrees(double degree) {

        LOGGER.info("Set degree: {}", degree);

        if (turntableIconPanel != null) {

            degree = degree + this.degreeOffset;
            if (degree > 359) {
                degree = degree - 360;
            }

            turntableIconPanel.setDegrees(degree);
            turntableIconPanel.repaintTurntable();
        }
    }

    /**
     * Initialize the accessory state icons
     */
    private void initializeAccessoryStateIcons() {
        // Set the icon for leaf nodes.
        accessoryErrorIcon = ImageUtils.createImageIcon(StepControlPanel.class, "/icons/accessory-error.png");
        accessorySuccessfulIcon = ImageUtils.createImageIcon(StepControlPanel.class, "/icons/accessory-successful.png");
        accessoryWaitIcon = ImageUtils.createImageIcon(StepControlPanel.class, "/icons/accessory-wait.png");
        accessoryUnknownIcon = ImageUtils.createImageIcon(StepControlPanel.class, "/icons/accessory-unknown.png");

        emergencyStopIcon =
            ImageUtils.createImageIcon(StepControlPanel.class, "/icons/stepcontrol/emergency-stop.png", 16, 16);
        normalOperatingIcon = ImageUtils.createImageIcon(StepControlPanel.class, "/icons/green-leaf.png");
        homingInProgressIcon = ImageUtils.createImageIcon(StepControlPanel.class, "/icons/red-leaf.png");

        emptyIcon = ImageUtils.createImageIcon(StepControlPanel.class, "/icons/empty.png");
        errorIcon = ImageUtils.createImageIcon(StepControlPanel.class, "/icons/error-leaf.png");
    }

    private void firePerformAspect(final StepControlAspect stepControlAspect) {
        LOGGER.info("Aspect is performed on stepControlAspect: {}", stepControlAspect);

        int aspectNumber = -1;

        // get the aspect number from the position of the stepControl aspect
        for (int rowIndex = 0; rowIndex < aspectTable.getRowCount(); rowIndex++) {
            StepControlAspect currentAspect =
                (StepControlAspect) getSortableTableModel().getValueAt(rowIndex, AspectTableModel.COLUMN_POSITION);

            if (Objects.equals(currentAspect.getPosition(), stepControlAspect.getPosition())) {
                aspectNumber = rowIndex;
                LOGGER.info("Found the aspect to activate at row: {}, aspectNumber: {}", rowIndex, aspectNumber);
                break;
            }
        }

        firePerformAspect(aspectNumber);
    }

    private void firePerformOppositeAspect(final StepControlAspect stepControlAspect) {
        LOGGER.info("Opposite aspect is performed on stepControlAspect: {}", stepControlAspect);

        int aspectNumber = -1;

        // get the aspect number from the position of the stepControl aspect
        int totalRows = aspectTable.getRowCount();
        for (int rowIndex = 0; rowIndex < totalRows; rowIndex++) {
            StepControlAspect currentAspect =
                (StepControlAspect) getSortableTableModel().getValueAt(rowIndex, AspectTableModel.COLUMN_POSITION);

            if (Objects.equals(currentAspect.getOppositePosition(), stepControlAspect.getOppositePosition())) {
                aspectNumber = rowIndex + totalRows;
                LOGGER
                    .info("Found the opposite aspect to activate at row: {}, aspectNumber: {}", rowIndex, aspectNumber);
                break;
            }
        }

        firePerformAspect(aspectNumber);
    }

    private void firePerformAspect(int aspectNumber) {
        LOGGER.info("The controlling aspect is performed, current aspectNumber: {}", aspectNumber);

        final Accessory accessory = new Accessory();
        accessory.setId(StepControlControllerInterface.ACCESSORY_ID_CONTROLLING);

        if (aspectNumber < 0 || aspectNumber > StepControlControllerInterface.MAX_CONFIGURED_ASPECTS) {
            LOGGER
                .warn("The current aspect has no valid aspectNumber assigned (0..{}). Abort perform aspect: {}",
                    StepControlControllerInterface.MAX_CONFIGURED_ASPECTS, aspectNumber);
            return;
        }

        stepControlController.activateAspect(accessory, aspectNumber);

        // update the aspectId in the aspect execution model
        synchronized (aspectExecutionModel) {
            aspectExecutionModel.setAspectId(aspectNumber);
        }
    }

    private void firePerformSpeed() {
        int speed = stepControlModel.getSpeed();
        LOGGER.info("Write the speed value: {}", speed);
        writeCvValue(StepControlKeywords.KEYWORD_SPEED, speed);
    }

    private void firePerformAccel() {
        int accel = stepControlModel.getAccel();
        writeCvValue(StepControlKeywords.KEYWORD_ACCEL, accel);
    }

    private void firePerformDecel() {
        int decel = stepControlModel.getDecel();
        writeCvValue(StepControlKeywords.KEYWORD_DECEL, decel);
    }

    private void writeCvValue(String keyword, int modelValue) {
        try {
            final List cvList = new ArrayList<>();

            // check the value
            CvNode speedNode = StepControlCvUtils.getNode(mapKeywordToNode, keyword);

            CvValueUtils
                .compareAndAddNewValue(speedNode, Integer.toString(modelValue), cvList,
                    getCvNumberToNodeMap(selectedNode));

            LOGGER.info("Write CV value, keyword: {}, modelValue: {}, cvList: {}", keyword, modelValue, cvList);

            if (CollectionUtils.isNotEmpty(cvList)) {
                // write the config variables to the node
                fireWriteConfigVariables(cvList);
            }
            else {
                LOGGER.warn("No CV values to write found.");
            }
        }
        catch (Exception ex) {
            LOGGER.warn("Set the new {} value failed.", keyword, ex);
        }
    }

    private void readCurrentValuesFromCV(final NodeInterface node) {
        LOGGER.info("Read the values from CV for node: {}", node);

        if (node == null || !ProductUtils.isStepControl(node.getUniqueId())) {
            LOGGER.warn("No node available or not a StepControl: {}", node);

            if (mapKeywordToNode != null) {
                mapKeywordToNode.clear();
            }

            return;
        }
        LOGGER.info("Get the cvDefinitionTreeTableModel from the node: {}", node);

        CvDefinitionPanelController cvDefinitionPanelController =
            DefaultApplicationContext
                .getInstance()
                .get(DefaultApplicationContext.KEY_CVDEFINITIONPANEL_CONTROLLER, CvDefinitionPanelController.class);

        CvDefinitionTreeTableModel cvDefinitionTreeTableModel =
            cvDefinitionPanelController.getCvDefinitionTreeTableModel(node);

        LOGGER.info("Current cvDefinitionTreeTableModel: {}", cvDefinitionTreeTableModel);

        if (cvDefinitionTreeTableModel != null) {
            // search the keywords
            mapKeywordToNode = new LinkedHashMap<>();

            DefaultExpandableRow rootNode = (DefaultExpandableRow) cvDefinitionTreeTableModel.getRoot();
            if (rootNode != null) {
                CvNodeUtils.harvestKeywordNodes(rootNode, mapKeywordToNode);
            }

            LOGGER.info("Found keywords in nodes: {}", mapKeywordToNode.keySet());

            // load the speed range
            CvNode cvNodeSpeed = StepControlCvUtils.getNode(mapKeywordToNode, StepControlKeywords.KEYWORD_SPEED);
            int maxSpeed = 2001;
            int minSpeed = 1;
            if (cvNodeSpeed != null) {
                try {
                    maxSpeed = Integer.parseInt(cvNodeSpeed.getCV().getMax()) + 1;
                }
                catch (Exception ex) {
                    LOGGER.warn("Get the max speed from CV definition failed.", ex);
                }

                try {
                    minSpeed = Integer.parseInt(cvNodeSpeed.getCV().getMin());
                }
                catch (Exception ex) {
                    LOGGER.warn("Get the min speed from CV definition failed.", ex);
                }
            }
            boundedRangeAdapterSpeed.setMinimum(minSpeed);
            boundedRangeAdapterSpeed.setMaximum(maxSpeed);
        }
        else {
            LOGGER.warn("No cvDefinitionTreeTableModel available for node: {}", node);
        }

        // TODO make this more clever? only load values that are not loaded already? is this clever? how about changed
        // values?

        // read the values from the node
        if (MapUtils.isNotEmpty(mapKeywordToNode)) {
            final List configVariables = new ArrayList<>();

            final Map cvNumberToNodeMap = getCvNumberToNodeMap(selectedNode);

            for (CvNode cvNode : mapKeywordToNode.values()) {
                // LOGGER.info("Process cvNode: {}", cvNode);

                prepareConfigVariables(cvNode, configVariables, cvNumberToNodeMap);
            }

            // keep the list of config variables
            this.requiredConfigVariables.clear();
            if (CollectionUtils.isNotEmpty(configVariables)) {
                this.requiredConfigVariables.addAll(configVariables);
            }

            // add the current position with the special CV
            prepareGetCurrentPosition(configVariables);

            fireLoadConfigVariables(configVariables);
        }
        else {
            LOGGER.warn("No values available in mapKeywordToNode!");
        }
    }

    private void prepareConfigVariables(
        final CvNode cvNode, final List configVariables,
        final Map cvNumberToNodeMap) {

        try {
            switch (cvNode.getCV().getType()) {
                case LONG:
                    // LONG nodes are processed
                    LongCvNode masterNode = ((LongCvNode) cvNode).getMasterNode();
                    addConfigVariableDistinct(configVariables, masterNode.getConfigVar());
                    for (CvNode slaveNode : masterNode.getSlaveNodes()) {
                        addConfigVariableDistinct(configVariables, slaveNode.getConfigVar());
                    }
                    break;
                case INT:
                    // INT nodes are processed
                    addConfigVariableDistinct(configVariables, cvNode.getConfigVar());
                    int highCvNum = Integer.parseInt(cvNode.getCV().getHigh());
                    int cvNumber = Integer.parseInt(cvNode.getCV().getNumber());

                    if (highCvNum == cvNumber) {
                        // search the low CV
                        CvNode lowCvNode = cvNumberToNodeMap.get(cvNode.getCV().getLow());
                        addConfigVariableDistinct(configVariables, lowCvNode.getConfigVar());
                    }
                    else {
                        // search the high CV
                        CvNode highCvNode = cvNumberToNodeMap.get(cvNode.getCV().getHigh());
                        if (highCvNode == null) {
                            LOGGER.warn("The highCvNode is not available: {}", cvNode.getCV());
                        }
                        addConfigVariableDistinct(configVariables, highCvNode.getConfigVar());
                    }
                    break;
                default:
                    addConfigVariableDistinct(configVariables, cvNode.getConfigVar());
                    break;
            }
        }
        catch (Exception ex) {
            LOGGER.warn("Prepare config variables to read from node failed.", ex);
        }
    }

    /**
     * Add the config variable to the list if it is not in list already.
     * 
     * @param configVariables
     *            the list of config variables
     */
    private void addConfigVariableDistinct(
        final List configVariables, final ConfigurationVariable cv) {

        if (!configVariables.contains(cv)) {
            configVariables.add(cv);
        }
        else {
            LOGGER.info("Skip add CV because it's in the list already: {}", cv);
        }
    }

    public void addCvDefinitionRequestListener(CvDefinitionRequestListener l) {
        cvDefinitionRequestListeners.add(l);
    }

    private List fireLoadConfigVariables(List configVariables) {
        // TODO decouple the AWT-thread from this work?
        LOGGER.info("Load the config variables.");
        for (CvDefinitionRequestListener l : cvDefinitionRequestListeners) {
            return l.loadCvValues(configVariables);
        }

        return Collections.emptyList();
    }

    private JComponent buildLeftAlignedButtonBar(JPanel container, JComponent... button) {
        return new ButtonBarBuilder(container).addButton(button).addGlue().build();
    }

    private JComponent buildLeftAlignedButtonBar(JComponent... button) {
        return new ButtonBarBuilder().addButton(button).addGlue().build();
    }

    @Override
    public JPanel getComponent() {
        return component;
    }

    public String getName() {
        return Resources.getString(getClass(), "name");
    }

    private final AtomicBoolean cvValuesLoaded = new AtomicBoolean();

    @Override
    public void tabSelected(boolean selected) {
        LOGGER.info("Tab is selected: {}, initialized: {}", selected, initialized);
        // show the toolbar
        toolbarCvDefinition.setVisible(selected);

        // if the tab is selected and the initialized flag is false we must load the CV definitions
        if (selected) {
            triggerLoadCvValues();
        }

        if (!selected && mainModel.getSelectedNode() == null) {
            LOGGER.info("The tab is no longer selected and the selected node is null, reset the selected node!");

            selectedNode = null;

            LOGGER.info("Clear the mode and set operational mode to unknown.");
            stepControlModel.setOperationalMode(OperationModeEnum.unknown);
        }
    }

    public void triggerLoadCvValues() {
        final NodeInterface node = selectedNode;
        LOGGER
            .info("Load the CV values for node: {}, initialized: {}, cvValuesLoaded: {}", node, initialized,
                cvValuesLoaded);

        if (!cvValuesLoaded.get() && !initialized && selectedNode != null) {
            LOGGER.info("The CV values will be read from node.");
            statusBar.setStatusText(Resources.getString(StepControlPanel.class, "loading_cvvalues"));

            cvValuesLoaded.set(true);

            // call this method after node was selected
            SwingUtilities.invokeLater(() -> readCurrentValuesFromCV(node));
        }
        else {
            LOGGER.info("Skip loading CV values because the initialized is true.");
        }
    }

    private final class AspectTablePopupMenu extends BasicPopupMenu {
        private static final long serialVersionUID = 1L;

        private JMenuItem newLabel;

        private JMenuItem deleteLabel;

        private JMenuItem deleteAllLabel;

        private JMenuItem importAspectsFromExcelMenuItem;

        private JMenuItem exportAspectsToExcelMenuItem;

        public AspectTablePopupMenu() {
            newLabel = new JMenuItem(Resources.getString(StepControlPanel.class, "newAspect") + " ...");
            newLabel.addActionListener(new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    LOGGER.info("Add new aspect.");

                    fireCreateNewAspect();
                }
            });
            add(newLabel);

            deleteLabel = new JMenuItem(Resources.getString(StepControlPanel.class, "deleteAspect") + " ...");
            deleteLabel.addActionListener(new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    fireRemoveAspect();
                }
            });
            add(deleteLabel);

            deleteAllLabel = new JMenuItem(Resources.getString(StepControlPanel.class, "deleteAllAspects") + " ...");
            deleteAllLabel.setToolTipText(Resources.getString(StepControlPanel.class, "deleteAllAspects.tooltip"));
            deleteAllLabel.addActionListener(new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    fireRemoveAllAspects();
                }
            });
            add(deleteAllLabel);

            addSeparator();

            // import aspects from Excel file
            importAspectsFromExcelMenuItem =
                new JMenuItem(Resources.getString(StepControlPanel.class, "importAspectsFromExcel") + " ...");
            importAspectsFromExcelMenuItem
                .setToolTipText(Resources.getString(StepControlPanel.class, "importAspectsFromExcel.tooltip"));
            importAspectsFromExcelMenuItem.addActionListener(new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    fireImportAspectsFromExcel();
                }
            });
            add(importAspectsFromExcelMenuItem);

            // export aspects to Excel file
            exportAspectsToExcelMenuItem =
                new JMenuItem(Resources.getString(StepControlPanel.class, "exportAspectsToExcel") + " ...");
            exportAspectsToExcelMenuItem
                .setToolTipText(Resources.getString(StepControlPanel.class, "exportAspectsToExcel.tooltip"));
            exportAspectsToExcelMenuItem.addActionListener(new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    fireExportAspectsToExcel();
                }
            });
            add(exportAspectsToExcelMenuItem);

        }

        public void setDeleteEnabled(boolean enabled) {
            deleteLabel.setEnabled(enabled);
        }

        public void setDeleteAllEnabled(boolean enabled) {
            deleteAllLabel.setEnabled(enabled);
        }
    }

    private final class AddAspectDialog extends EscapeDialog {

        private static final long serialVersionUID = 1L;

        private int result = JOptionPane.CANCEL_OPTION;

        private final AspectEditorPanel aspectEditorPanel;

        public AddAspectDialog(Frame parent, String title, boolean modal, final AspectEditorPanel aspectEditorPanel) {

            super(parent, title, modal);

            this.aspectEditorPanel = aspectEditorPanel;

            getContentPane().setLayout(new BorderLayout());

            getContentPane().add(aspectEditorPanel);

            aspectEditorPanel.setDefaultButton();

            pack();

            setLocationRelativeTo(parent);
            setMinimumSize(getSize());
            // setVisible(true);

        };

        protected void setResult(int result) {
            this.result = result;
        }

        public int getResult() {
            return result;
        }

        public void cleanup() {

            if (aspectEditorPanel != null) {
                aspectEditorPanel.cleanup();
            }
        }
    }

    private final class AddAspectCallbackListener implements AspectCallbackListener {

        private AddAspectDialog aspectDialog;

        private void setAddAspectDialog(final AddAspectDialog aspectDialog) {
            LOGGER.info("Set the aspect dialog: {}", aspectDialog);
            this.aspectDialog = aspectDialog;
        }

        @Override
        public void activateAspect(final StepControlAspect stepControlAspect) {
            LOGGER.info("Activate the aspect is called for new aspect: {}", stepControlAspect);

            // make sure the aspect is valid and saved
            if (stepControlAspect.isValid() && (stepControlModel.getTurnTableType() == TurnTableType.round
                ? stepControlAspect.isOppositeValid() : true)
                && stepControlAspect.getStatus() == AspectPersistanceStatus.statusPersistent) {
                firePerformAspect(stepControlAspect);
            }
            else {
                LOGGER
                    .warn("Do not perform the aspect because the stepControlAspect is not stored: {}",
                        stepControlAspect);
            }
        }

        @Override
        public void activateOppositeAspect(final StepControlAspect stepControlAspect) {
            LOGGER.info("Activate the aspect is called for new aspect: {}", stepControlAspect);

            // make sure the aspect is valid and saved
            if (stepControlAspect.isValid() && stepControlAspect.isOppositeValid()
                && stepControlAspect.getStatus() == AspectPersistanceStatus.statusPersistent) {
                firePerformOppositeAspect(stepControlAspect);
            }
            else {
                LOGGER
                    .warn("Do not perform the aspect because the stepControlAspect is not stored: {}",
                        stepControlAspect);
            }
        }

        @Override
        public void saveChanges(
            final StepControlAspect originalAspect, Function func)
            throws Exception {

            StepControlAspect changedAspect = func.apply(originalAspect);

            // the aspectDialog is only set if a new aspect is created
            if (aspectDialog == null) {
                LOGGER.info("Save the changed aspect: {}", changedAspect);

                stepControlModel.updateStepControlAspect(originalAspect, changedAspect);

                // expand the updated aspect
                selectAspectByPosition(originalAspect);
            }
            else {
                // new aspect is added
                aspectDialog.setResult(JOptionPane.OK_OPTION);

                LOGGER.info("Add the new aspect to the model: {}", changedAspect);

                stepControlModel.addStepControlAspect(changedAspect);

                // expand the inserted aspect
                selectAspectByPosition(changedAspect);

                aspectDialog.setVisible(false);
            }

            LOGGER.info("Set the pending changes flag.");
            tabStatusListener.updatePendingChanges(component, true);

        }

        @Override
        public void discardChanges(Callable func) throws Exception {

            LOGGER.info("User decided to discard changes. Close the aspect dialog.");

            func.call();

            if (aspectDialog != null) {
                aspectDialog.setResult(JOptionPane.CANCEL_OPTION);
                aspectDialog.setVisible(false);
            }
        }

        @Override
        public void verifyUniquePosition(final StepControlAspect originalAspect, Long position, Long positionOpposite) {

            // check if a stepControl aspect for the same position is configured already
            verifyUniqueAspectPosition(originalAspect, position, positionOpposite);
        }

        private void verifyUniqueAspectPosition(
            final StepControlAspect originalAspect, Long position, Long positionOpposite) {

            if (position == null) {
                throw new IllegalArgumentException("Provided position value is not available.");
            }

            if (TurnTableType.round == stepControlModel.getTurnTableType()) {
                // check if a stepControl aspect for the same position is configured already
                for (StepControlAspect currentAspect : stepControlModel.getStepControlAspects()) {
                    if ((Objects.equals(currentAspect.getPosition(), position) && currentAspect != originalAspect)
                        || (Objects.equals(currentAspect.getOppositePosition(), positionOpposite)
                            && currentAspect != originalAspect)) {
                        LOGGER
                            .warn("Found configured aspect with the same position or opposite position: {}",
                                currentAspect);

                        throw new IllegalArgumentException("Found configured aspect with the same position.");
                    }
                }

                for (StepControlAspect currentAspect : stepControlModel.getStepControlAspects()) {
                    if ((Objects.equals(currentAspect.getOppositePosition(), position)
                        && currentAspect != originalAspect)
                        || (Objects.equals(currentAspect.getPosition(), positionOpposite)
                            && currentAspect != originalAspect)) {
                        LOGGER
                            .warn("Found configured aspect with the same position or opposite position: {}",
                                currentAspect);

                        throw new IllegalArgumentException("Found configured aspect with the same position.");
                    }
                }
            }
            else {
                // linear
                for (StepControlAspect currentAspect : stepControlModel.getStepControlAspects()) {
                    if (Objects.equals(currentAspect.getPosition(), position) && currentAspect != originalAspect) {
                        LOGGER.warn("Found configured aspect with the same position: {}", currentAspect);

                        throw new IllegalArgumentException("Found configured aspect with the same position.");
                    }
                }
            }
        }

        @Override
        public Long getCurrentTurntablePosition() {

            return stepControlModel.getCurrentPosition();
        }
    }

    private void fireCreateNewAspect() {

        if (stepControlModel.getStepControlAspects().size() < stepControlModel.getMaxConfiguredAspects()) {

            // check if the current selected row is dirty
            int selectedRow = aspectTable.getSelectedRow();
            if (selectedRow > -1) {
                if (!aspectTable.checkAspectEditorIsDirty(selectedRow)) {
                    LOGGER.info("Check if aspect editor is dirty failed. Do not show the add aspect editor.");
                    return;
                }
            }

            LOGGER.info("Create the editorPanel to create a new aspect.");

            final StepControlAspect stepControlAspect = new StepControlAspect(null, null, Polarity.normal);

            // create the callback listener for the new aspect
            final AddAspectCallbackListener aspectCallbackListener = new AddAspectCallbackListener();

            final AspectEditorPanel aspectEditorPanel =
                aspectTable
                    .createAspectEditorPanel(aspectCallbackListener, stepControlAspect, stepControlModel,
                        EditorType.editorNew);

            final AddAspectDialog aspectDialog =
                new AddAspectDialog(JOptionPane.getFrameForComponent(component),
                    Resources.getString(StepControlPanel.class, "dialog_add_aspect"), true, aspectEditorPanel);

            aspectCallbackListener.setAddAspectDialog(aspectDialog);
            aspectDialog.setVisible(true);

            if (aspectDialog.getResult() == JOptionPane.OK_OPTION) {
                LOGGER.info("User added new aspect: {}", stepControlAspect);
            }
            else {
                LOGGER.info("User cancelled add new aspect.");
            }

            aspectDialog.cleanup();
        }
        else {
            LOGGER.warn("Maximum number of aspects reached.");
            JOptionPane
                .showMessageDialog(component, Resources.getString(StepControlPanel.class, "max_aspect_message"),
                    Resources.getString(StepControlPanel.class, "max_aspect_title"), JOptionPane.ERROR_MESSAGE);
        }
    }

    /**
     * Select the aspect by the position.
     * 
     * @param stepControlAspect
     *            the aspect
     */
    private void selectAspectByPosition(final StepControlAspect stepControlAspect) {

        // select and expand the aspect
        for (int rowIndex = 0; rowIndex < aspectTable.getRowCount(); rowIndex++) {
            Object position = getSortableTableModel().getValueAt(rowIndex, AspectTableModel.COLUMN_POSITION);
            LOGGER.info("Current position: {}", position);

            Assert.isInstanceOf(StepControlAspect.class, position);

            if (Objects.equals(position, stepControlAspect)) {
                LOGGER
                    .info("Found configured aspect to select with the same position: {} at rowIndex: {}", position,
                        rowIndex);

                aspectTable.setRowSelectionInterval(rowIndex, rowIndex);

                LOGGER.info("Expand row: {}", rowIndex);
                aspectTable.expandRow(rowIndex);

                aspectTable.scrollRowToVisible(rowIndex);
                break;
            }
        }

        LOGGER.info("Select aspect by position has finished.");
    }

    private void fireRemoveAspect() {
        LOGGER.info("Remove aspect.");

        int selectedRow = aspectTable.getSelectedRow();
        if (selectedRow > -1) {

            int sortedRow = getSortableTableModel().getSortedRowAt(selectedRow);
            AspectTableModel aspectTableModel = (AspectTableModel) getSortableTableModel().getActualModel();
            Object aspect = aspectTableModel.getChildValueAt(sortedRow);

            if (aspect instanceof StepControlAspect) {
                StepControlAspect stepControlAspect = (StepControlAspect) aspect;
                LOGGER.info("Remove aspect from stepControlModel: {}", stepControlAspect);

                stepControlModel.removeStepControlAspect(stepControlAspect);
            }

        }
    }

    private void fireRemoveAllAspects() {
        LOGGER.info("Remove all aspects.");

        int totalRows = aspectTable.getRowCount();
        if (totalRows > 0) {

            // show a messagebox to check if the user really wants to delete all aspects
            boolean continueDeleteAspects =
                TaskDialogs
                    .build(JOptionPane.getFrameForComponent(component),
                        Resources.getString(StepControlPanel.class, "deleteAllAspects-confirm.instruction"),
                        Resources.getString(StepControlPanel.class, "deleteAllAspects-confirm"))
                    .title(Resources.getString(StepControlPanel.class, "deleteAllAspects-confirm.title")).isConfirmed();
            LOGGER.info("The user confirmed to continue delete all aspects: {}", continueDeleteAspects);

            if (continueDeleteAspects) {
                removeAllAspects();
            }
        }
    }

    private void removeAllAspects() {

        aspectTable.collapseAllRows();

        stepControlModel.setStepControlAspects(null);
    }

    private void fireImportAspectsFromExcel() {
        LOGGER.info("Import all aspects from excel.");
        final WizardSettingsInterface wizardSettings = this.settingsService.getWizardSettings();
        String storedWorkingDirectory = wizardSettings.getWorkingDirectory(WORKING_DIR_STEPCONTROL_EXPORT_KEY);

        final FileNameExtensionFilter ff =
            new FileNameExtensionFilter(
                Resources.getString(StepControlPanel.class, "filter") + " (*."
                    + org.bidib.wizard.utils.FileUtils.EXCEL_EXTENSION + ")",
                org.bidib.wizard.utils.FileUtils.EXCEL_EXTENSION);

        final FileDialog dialog = new FileDialog(component, FileDialog.OPEN, storedWorkingDirectory, null, ff) {
            @Override
            public void approve(String selectedFile) {
                File file = new File(selectedFile);

                selectedFile = file.getName();

                try {
                    loadAspectsFromExcel(file);

                    final String exportDir = file.getParent();
                    LOGGER.info("Save current exportDir: {}", exportDir);

                    wizardSettings.setWorkingDirectory(WORKING_DIR_STEPCONTROL_EXPORT_KEY, exportDir);
                }
                catch (PositionOutOfRangeException ex) {
                    LOGGER.warn("Load aspects from excel file failed because invalid data was detected.", ex);

                    TaskDialogs
                        .build(JOptionPane.getFrameForComponent(getComponent()), Resources
                            .getString(StepControlPanel.class, "import-aspects-from-excel-invalid-data.instruction"),
                            Resources.getString(StepControlPanel.class, "import-aspects-from-excel-invalid-data.text")
                                + Resources
                                    .getString(StepControlPanel.class, "position-out-of-range.text",
                                        ex.getAspectNumber(), ex.getPosition(), ex.getMin(), ex.getMax()))
                        .title(Resources.getString(StepControlPanel.class, "import-aspects-from-excel-failed.title"))
                        .showException(ex);
                }
                catch (IOException | DataExchangeException ex) {
                    LOGGER.warn("Load aspects from excel file failed.", ex);

                    TaskDialogs
                        .build(JOptionPane.getFrameForComponent(getComponent()),
                            Resources.getString(StepControlPanel.class, "import-aspects-from-excel-failed.instruction"),
                            Resources.getString(StepControlPanel.class, "import-aspects-from-excel-failed.text"))
                        .title(Resources.getString(StepControlPanel.class, "import-aspects-from-excel-failed.title"))
                        .showException(ex);
                }
            }
        };
        dialog.showDialog();
    }

    private static final String WORKING_DIR_STEPCONTROL_EXPORT_KEY = "stepControlExchange";

    private void fireExportAspectsToExcel() {
        LOGGER.info("Export all aspects to excel.");

        final WizardSettingsInterface wizardSettings = this.settingsService.getWizardSettings();
        String storedWorkingDirectory = wizardSettings.getWorkingDirectory(WORKING_DIR_STEPCONTROL_EXPORT_KEY);
        String filename = "StepControl-export-" + NodeUtils.getNodeName(selectedNode) + ".xlsx";
        if (!FileUtils.isFilenameValid(filename)) {
            filename = FileUtils.escapeInvalidFilenameCharacters(filename, "_");
        }

        final FileNameExtensionFilter ff =
            new FileNameExtensionFilter(
                Resources.getString(StepControlPanel.class, "filter") + " (*."
                    + org.bidib.wizard.utils.FileUtils.EXCEL_EXTENSION + ")",
                org.bidib.wizard.utils.FileUtils.EXCEL_EXTENSION);

        final FileDialog dialog = new FileDialog(component, FileDialog.SAVE, storedWorkingDirectory, filename, ff) {
            @Override
            public void approve(String selectedFile) {
                File file = new File(selectedFile);

                selectedFile = file.getName();

                try {
                    writeAspectsToExcel(file);

                    final String exportDir = file.getParent();
                    LOGGER.info("Save current exportDir: {}", exportDir);

                    wizardSettings.setWorkingDirectory(WORKING_DIR_STEPCONTROL_EXPORT_KEY, exportDir);
                }
                catch (Exception ex) {
                    LOGGER.warn("Save aspects to excel file failed.", ex);

                    TaskDialogs
                        .build(JOptionPane.getFrameForComponent(getComponent()),
                            Resources.getString(StepControlPanel.class, "export-aspects-to-excel-failed.instruction"),
                            Resources.getString(StepControlPanel.class, "export-aspects-to-excel-failed.text"))
                        .title(Resources.getString(StepControlPanel.class, "export-aspects-to-excel-failed.title"))
                        .showException(ex);
                }
            }
        };
        dialog.showDialog();
    }

    private void loadAspectsFromExcel(File excelFile) throws IOException, PositionOutOfRangeException {
        LOGGER.info("Load aspects from excel file: {}", excelFile);

        List importAspects = null;

        try (InputStream is = new BufferedInputStream(new FileInputStream(excelFile))) {
            final ExcelAspectReader reader = new ExcelAspectReader();
            importAspects = reader.readAspects(is);
        }

        if (CollectionUtils.isNotEmpty(importAspects)) {
            LOGGER.info("Import the aspects.");

            final List importAspectsOpposite = new ArrayList<>();
            if (TurnTableType.round == stepControlModel.getTurnTableType()) {

                int totalSteps = stepControlModel.getTotalSteps().intValue();
                int oppositePositionStartPosition = totalSteps / 2;

                // split the aspects
                int size = importAspects.size();
                LOGGER.info("Number of import aspects: {}", size);
                importAspectsOpposite.addAll(importAspects.subList((size / 2), size));
                importAspects.removeAll(importAspectsOpposite);

                int aspectNumber = 0;

                // check if we have invalid configuration, e.g. position value to big
                // position values must be between 0 and half of motor steps
                for (ImportAspect importAspect : importAspects) {
                    if (importAspect.getPosition() < 0 || importAspect.getPosition() >= oppositePositionStartPosition) {
                        LOGGER.warn("Invalid values detected: {}", importAspect);

                        throw new PositionOutOfRangeException(importAspect.getPosition(), 0,
                            oppositePositionStartPosition - 1, aspectNumber);
                    }

                    aspectNumber++;
                }

                for (ImportAspect importAspect : importAspectsOpposite) {
                    if (importAspect.getPosition() < oppositePositionStartPosition
                        || importAspect.getPosition() >= totalSteps) {
                        LOGGER.warn("Invalid values detected: {}", importAspect);

                        throw new PositionOutOfRangeException(importAspect.getPosition(), oppositePositionStartPosition,
                            totalSteps - 1, aspectNumber);
                    }
                    aspectNumber++;
                }
            }

            LOGGER.info("Current importAspects: {}", importAspects);
            LOGGER.info("Current importAspectsOpposite: {}", importAspectsOpposite);

            removeAllAspects();

            final List stepControlAspects = new ArrayList<>();
            int index = 0;
            // add the imported aspects
            for (ImportAspect importAspect : importAspects) {
                StepControlAspect stepControlAspect =
                    new StepControlAspect(null, Long.valueOf(importAspect.getPosition()),
                        importAspect.isInverse() ? Polarity.inverted : Polarity.normal);

                if (TurnTableType.round == stepControlModel.getTurnTableType()) {
                    ImportAspect importAspectOpposite = importAspectsOpposite.get(index);
                    stepControlAspect.setOppositePosition((long) importAspectOpposite.getPosition());
                    stepControlAspect
                        .setOppositePolarity(importAspectOpposite.isInverse() ? Polarity.inverted : Polarity.normal);
                }

                stepControlAspects.add(stepControlAspect);

                index++;
            }
            stepControlModel.setStepControlAspects(stepControlAspects);

            getSortableTableModel().fireTableDataChanged();

            this.aspectTable.expandAllRows();
        }
        else {
            LOGGER.info("No aspects loaded.");
        }
    }

    private void writeAspectsToExcel(File excelFile) {
        LOGGER.info("Write aspects to excel file: {}", excelFile);

        final List importAspects = new ArrayList<>();
        int aspectNumber = 0;
        for (StepControlAspect aspect : stepControlModel.getStepControlAspects()) {
            importAspects
                .add(new ImportAspect(aspectNumber, aspect.getPosition().intValue(),
                    aspect.getPolarity() == Polarity.inverted));

            aspectNumber++;
        }

        if (TurnTableType.round == stepControlModel.getTurnTableType()) {
            // prepare the opposite positions
            for (StepControlAspect aspect : stepControlModel.getStepControlAspects()) {
                importAspects
                    .add(new ImportAspect(aspectNumber, aspect.getOppositePosition().intValue(),
                        aspect.getOppositePolarity() == Polarity.inverted));

                aspectNumber++;
            }
        }

        // provide the motor data
        final Map cvNumberToNodeMap = getCvNumberToNodeMap(selectedNode);

        Integer stepCount =
            StepControlCvUtils
                .getConfigVarIntValue(mapKeywordToNode, cvNumberToNodeMap, StepControlKeywords.KEYWORD_STEPCOUNT);
        MicroStepsEnum microStepsEnum = StepControlCvUtils.getValidMicroStepping(mapKeywordToNode, cvNumberToNodeMap);
        final MotorData motorData =
            new MotorData(stepCount, microStepsEnum.getSteps(), stepControlModel.getTotalSteps().intValue(),
                stepControlModel.getTurnTableType());

        final ExcelAspectReader reader = new ExcelAspectReader();
        reader.writeAspects(selectedNode, excelFile, importAspects, motorData);
    }

    private void fireConfigurationWizard() {
        LOGGER.info("Open the configuration wizard.");

        try {
            Map cvNumberToNodeMap = getCvNumberToNodeMap(selectedNode);

            final ConfigurationWizardModel configurationWizardModel =
                ConfigurationWizard
                    .prepareConfigurationModel(selectedNode, stepControlModel.getTurnTableType(), cvNumberToNodeMap,
                        mapKeywordToNode);

            final ConfigurationWizard wizard = new ConfigurationWizard(configurationWizardModel);
            wizard.showWizard(component);

            if (configurationWizardModel.getWizardStatus() == WizardStatus.finished) {

                // transfer the new values to the node
                writeConfigurationValues(configurationWizardModel, cvNumberToNodeMap, mapKeywordToNode);
            }
            else {
                LOGGER.info("The configurationWizardModel is not available.");
            }
        }
        catch (Exception ex) {
            LOGGER.warn("Show configuration wizard failed.", ex);

            TaskDialogs
                .build(JOptionPane.getFrameForComponent(getComponent()),
                    Resources.getString(StepControlPanel.class, "show-config-wizard-failed.instruction"),
                    Resources.getString(StepControlPanel.class, "show-config-wizard-failed.text"))
                .title(Resources.getString(StepControlPanel.class, "show-config-wizard-failed.title"))
                .showException(ex);
        }
    }

    /**
     * The CV definition of the node has changed.
     */
    public void cvDefinitionChanged() {

        // the cvDefinitionChanged is misused to change the selected node

        LOGGER.info("The cv definition has changed, selected node: {}", selectedNode);

        if ((selectedNode != null && selectedNode.equals(mainModel.getSelectedNode()))
            || (selectedNode == null && (mainModel.getSelectedNode() == null
                || !ProductUtils.isStepControl(mainModel.getSelectedNode().getUniqueId())))) {
            LOGGER.info("The node in the model has not changed or is not a StepControl.");
            return;
        }

        if (selectedNode != null) {
            LOGGER.info("Remove the port value listener from the previous selected node.");
            selectedNode.removePortValueListener(MotorPort.class, this);
        }

        LOGGER.info("Reset the cv values loaded flag.");
        cvValuesLoaded.set(false);

        // force reload of CV values
        initialized = false;

        if (readCvButton != null) {
            readCvButton.setEnabled(false);
        }
        if (writeCvButton != null) {
            writeCvButton.setEnabled(false);
        }

        this.selectedNode = null;

        // set the selected node
        final NodeInterface currentSelectedNode = mainModel.getSelectedNode();

        if (ProductUtils.isStepControl(currentSelectedNode.getUniqueId())) {
            this.selectedNode = currentSelectedNode;
        }

        // clear the aspects
        // set the aspects silently
        try {
            aspectTable.getModel().removeTableModelListener(aspectTableModelListener);
            stepControlModel.clearModel();
        }
        finally {
            aspectTable.getModel().addTableModelListener(aspectTableModelListener);
        }
        requiredConfigVariables.clear();

        if (mapKeywordToNode != null) {
            LOGGER.info("Clear the mapKeywordToNode.");
            mapKeywordToNode.clear();
        }

        // reset the pending changes flag on the tab
        resetPendingChanges();

        updateToolbarButtons();

        // load the aspects initially
        if (selectedNode != null && ProductUtils.isStepControl(selectedNode.getUniqueId())) {
            LOGGER.info("The currently selected node is a StepControl.");

            selectedNode.addPortValueListener(MotorPort.class, this);

            fieldsToUpdate.add(StepControlKeywords.KEYWORD_CONFIGURED_ASPECTS);
        }
    }

    /**
     * The values of the current CV definition have changed. Get the cv values and update the stepControlModel.
     */
    public void cvDefinitionValuesChanged(final boolean read, final List changedNames) {
        LOGGER.info("The cv definition values have changed, read: {}, changedNames: {}", read, changedNames);

        cvDefinitionChanged();

        if (selectedNode == null || !ProductUtils.isStepControl(selectedNode.getUniqueId())) {
            LOGGER.info("The currently selected node is not a StepControl.");
            return;
        }

        final Map cvNumberToNodeMap = getCvNumberToNodeMap(selectedNode);
        final AtomicBoolean errorDetected = new AtomicBoolean();

        if (changedNames.contains(StepControlKeywords.CVKEY_CURRENT_POSITION)) {
            try {
                long currentPosition =
                    StepControlCvUtils
                        .getConfigVarLongValueFromStringByCvKey(cvNumberToNodeMap,
                            StepControlKeywords.CVKEY_CURRENT_POSITION);
                stepControlModel.setCurrentPosition(currentPosition);

                LOGGER
                    .info(
                        "The current position was read and no more CV values are updated! The current position is: {}",
                        currentPosition);

            }
            catch (Exception ex) {
                LOGGER.warn("Update the current position failed.", ex);
            }
            // return;
        }

        if (MapUtils.isEmpty(mapKeywordToNode)) {
            LOGGER.info("No mapKeywordToNode value available.");
            return;
        }

        if (changedNames.contains(getCvName(StepControlKeywords.KEYWORD_TOTALSTEPS))) {
            try {
                Long totalSteps =
                    StepControlCvUtils.getConfigVarLongValue(mapKeywordToNode, StepControlKeywords.KEYWORD_TOTALSTEPS);
                LOGGER.info("Current total steps: {}", totalSteps);
                stepControlModel.setTotalSteps(totalSteps);
                if (angleRenderer != null) {
                    angleRenderer.setTotalSteps(totalSteps);
                }
            }
            catch (Exception ex) {
                LOGGER.warn("Set the total step count failed.", ex);
                errorDetected.set(true);
            }
        }

        if (changedNames.contains(getCvName(StepControlKeywords.KEYWORD_TABLETYPE))) {
            try {
                Integer value =
                    StepControlCvUtils
                        .getConfigVarIntValue(mapKeywordToNode, cvNumberToNodeMap,
                            StepControlKeywords.KEYWORD_TABLETYPE);
                TurnTableType turnTableType = TurnTableType.fromValue(ByteUtils.getLowByte(value));
                stepControlModel.setTurnTableType(turnTableType);

                int maxConfiguredAspects = StepControlControllerInterface.MAX_CONFIGURED_ASPECTS;
                switch (turnTableType) {
                    case round:
                        maxConfiguredAspects = StepControlControllerInterface.MAX_CONFIGURED_ASPECTS / 2;
                        break;
                    case pendular:
                        maxConfiguredAspects = 2;
                        break;
                    default:
                        break;
                }
                stepControlModel.setMaxConfiguredAspects(maxConfiguredAspects);
                LOGGER
                    .info("The turnTableType was set to: {}, maxConfiguredAspects: {}", turnTableType,
                        maxConfiguredAspects);
            }
            catch (Exception ex) {
                LOGGER.warn("Set the turnTableType failed.", ex);
                errorDetected.set(true);
            }
        }

        if (changedNames.contains(getCvName(StepControlKeywords.KEYWORD_SPEED))) {
            try {
                Integer speed =
                    StepControlCvUtils
                        .getConfigVarIntValue(mapKeywordToNode, cvNumberToNodeMap, StepControlKeywords.KEYWORD_SPEED);
                stepControlModel.setSpeed(speed);
            }
            catch (Exception ex) {
                LOGGER.warn("Set the speed failed.", ex);
                errorDetected.set(true);
            }
        }

        if (changedNames.contains(getCvName(StepControlKeywords.KEYWORD_ACCEL))) {
            try {
                Integer accel =
                    StepControlCvUtils
                        .getConfigVarIntValue(mapKeywordToNode, cvNumberToNodeMap, StepControlKeywords.KEYWORD_ACCEL);
                stepControlModel.setAccel(accel);
            }
            catch (Exception ex) {
                LOGGER.warn("Set the accel failed.", ex);
                errorDetected.set(true);
            }
        }

        if (changedNames.contains(getCvName(StepControlKeywords.KEYWORD_DECEL))) {
            try {
                Integer decel =
                    StepControlCvUtils
                        .getConfigVarIntValue(mapKeywordToNode, cvNumberToNodeMap, StepControlKeywords.KEYWORD_DECEL);
                stepControlModel.setDecel(decel);
            }
            catch (Exception ex) {
                LOGGER.warn("Set the decel failed.", ex);
                errorDetected.set(true);
            }
        }

        if (changedNames.contains(getCvName(StepControlKeywords.KEYWORD_STEPCOUNT))
            || changedNames.contains(getCvName(StepControlKeywords.KEYWORD_UNIT_SYSTEM))) {

            final Integer stepCount =
                StepControlCvUtils
                    .getConfigVarIntValue(mapKeywordToNode, cvNumberToNodeMap, StepControlKeywords.KEYWORD_STEPCOUNT);
            final MicroStepsEnum microStepsEnum =
                StepControlCvUtils.getValidMicroStepping(mapKeywordToNode, cvNumberToNodeMap);

            // unit system
            Integer unitSystemValue =
                StepControlCvUtils
                    .getConfigVarByteValue(mapKeywordToNode, cvNumberToNodeMap,
                        StepControlKeywords.KEYWORD_UNIT_SYSTEM);
            MovementScaleEnum movementScale = MovementScaleEnum.scale1;
            if (unitSystemValue != null) {
                int bitSpeed = ByteUtils.getBit(unitSystemValue, 0);
                movementScale = (bitSpeed > 0 ? MovementScaleEnum.scale0_1 : MovementScaleEnum.scale1);
            }

            // check max valid speed
            long maxSpeedValid =
                SpeedRangeValidationUtils.calculateValidMaxSpeed(microStepsEnum.getSteps(), stepCount, movementScale);

            int currentMaxSpeed = boundedRangeAdapterSpeed.getMaximum();
            LOGGER.info("Current speed range maximum in bounded range adapter is: {}", currentMaxSpeed);
            if (currentMaxSpeed > maxSpeedValid) {
                LOGGER
                    .info(
                        "The configured maxSpeed ({}) in bounded range adapter exceeds the limit: {}. Update the maxSpeed in bounded range adapter to limit.",
                        currentMaxSpeed, maxSpeedValid);
                boundedRangeAdapterSpeed.setMaximum((int) maxSpeedValid);

                int currentSpeed = stepControlModel.getSpeed();
                if (currentSpeed > maxSpeedValid) {
                    LOGGER.warn("Set the current speed to the maximum valid speed: {}", maxSpeedValid);
                    stepControlModel.setSpeed((int) maxSpeedValid);
                }

                if (speedSlider != null) {
                    speedSlider.updateUI();
                }
            }

            // check max valid accel and decel values
            try {
                long accelDecelLimit = SpeedRangeValidationUtils.F_STEP_ACCEL_LIMITx10;
                AccelarationScaleEnum accelarationScale = AccelarationScaleEnum.scale1;
                if (unitSystemValue != null) {
                    int bitAccelaration = ByteUtils.getBit(unitSystemValue, 4);
                    accelarationScale =
                        (bitAccelaration > 0 ? AccelarationScaleEnum.scale0_1 : AccelarationScaleEnum.scale1);
                }

                if (accelarationScale == AccelarationScaleEnum.scale0_1) {
                    accelDecelLimit = SpeedRangeValidationUtils.F_STEP_ACCEL_LIMIT;
                }

                long accelarationFactor =
                    SpeedRangeValidationUtils.calculateValidAccelarationFactor(microStepsEnum.getSteps(), stepCount);

                // check and adjust accel
                long minAccel = accelarationFactor * stepControlModel.getAccel();
                LOGGER.info("The calculated minAccel: {}, current accelDecelLimit: {}", minAccel, accelDecelLimit);
                if (minAccel < accelDecelLimit) {
                    long adjustedAccelValue = accelDecelLimit / accelarationFactor;
                    LOGGER.warn("Set the current accel value to minimum: {}", adjustedAccelValue);
                    stepControlModel.setAccel((int) adjustedAccelValue);
                }

                // check and adjust decel
                long minDecel = accelarationFactor * stepControlModel.getDecel();
                LOGGER.info("The calculated minDecel: {}, current accelDecelLimit: {}", minDecel, accelDecelLimit);
                if (minDecel < accelDecelLimit) {
                    long adjustedDecelValue = accelDecelLimit / accelarationFactor;
                    LOGGER.warn("Set the current decel value to minimum: {}", adjustedDecelValue);
                    stepControlModel.setDecel((int) adjustedDecelValue);
                }
            }
            catch (Exception ex) {
                LOGGER.warn("Set or adjust the decel value failed.", ex);
                errorDetected.set(true);
            }
        }

        if (changedNames.contains(getCvName(StepControlKeywords.KEYWORD_PUSH_INTERVAL))) {
            try {
                Integer pushInterval =
                    StepControlCvUtils
                        .getConfigVarByteValue(mapKeywordToNode, cvNumberToNodeMap,
                            StepControlKeywords.KEYWORD_PUSH_INTERVAL);
                stepControlModel.setPushInterval(pushInterval);
            }
            catch (Exception ex) {
                LOGGER.warn("Set the push interval failed.", ex);
                errorDetected.set(true);
            }
        }

        if (changedNames.contains(getCvName(StepControlKeywords.KEYWORD_BRIDGE))) {

            Integer cvNodeBridge =
                StepControlCvUtils
                    .getConfigVarByteValue(mapKeywordToNode, cvNumberToNodeMap, StepControlKeywords.KEYWORD_BRIDGE);
            LOGGER.info("Current cvNodeBridge: {}", cvNodeBridge);

            if (cvNodeBridge != null) {
                int bridge = 0;
                try {
                    bridge = cvNodeBridge.intValue();

                    LOGGER.info("Current bridge value: {}", bridge);
                }
                catch (Exception ex) {
                    LOGGER.warn("Get the bridge value from CVs failed.", ex);
                }

                // if (bridge == 1 /* none */) {
                // // hide the servos and light ports
                // LOGGER.info("Remove the servos and light ports because no bridge is configured.");
                // selectedNode.setServoPorts(Collections.emptyList());
                // mainModel.setLightPorts(Collections.emptyList());
                // }
            }
        }

        // do not always update the configured aspects
        if (!initialized && read) {
            LOGGER.info("The values were read, update the aspects in the table.");
            fieldsToUpdate.add(StepControlKeywords.KEYWORD_CONFIGURED_ASPECTS);
        }

        // prepare the aspects
        if (fieldsToUpdate.contains(StepControlKeywords.KEYWORD_CONFIGURED_ASPECTS)) {
            LOGGER.info("Prepare the configured aspects.");

            // create the list of aspects with the values from the config variables
            final List configuredAspects =
                prepareConfiguredAspects(cvNumberToNodeMap, errorDetected);

            // set the aspects silently
            try {
                aspectTable.getModel().removeTableModelListener(aspectTableModelListener);

                stepControlModel.setStepControlAspects(configuredAspects);
            }
            finally {
                aspectTable.getModel().addTableModelListener(aspectTableModelListener);
            }

            fieldsToUpdate.remove(StepControlKeywords.KEYWORD_CONFIGURED_ASPECTS);

            getSortableTableModel().fireTableDataChanged();

            this.aspectTable.expandAllRows();
        }

        if (!initialized && !errorDetected.get()) {

            initialized = true;

            LOGGER.info("The panel was initialized. Reset the pending changes flag on the tab.");
            tabStatusListener.updatePendingChanges(component, false);
        }
        else if (read) {
            LOGGER.info("The CV values were read from the node. Reset the pending changes flag on the tab.");
            tabStatusListener.updatePendingChanges(component, false);
        }
        else {
            // check if we have pending changes on the aspects
            if (!checkHasPendingAspects(cvNumberToNodeMap)) {
                LOGGER.info("No pending aspect changes detected. Reset the pending changes flag on the tab.");
                tabStatusListener.updatePendingChanges(component, false);
            }
        }

        // update the buttons in the toolbar
        updateToolbarButtons();
    }

    private void updateToolbarButtons() {
        if (selectedNode != null) {
            if (readCvButton != null) {
                // enable the read button to get the values of the configuration variables for this node on
                // request
                readCvButton.setEnabled(MapUtils.isNotEmpty(selectedNode.getConfigVariables()));
            }
            if (writeCvButton != null) {
                // enable the write button to write the values of the configuration variables for this node on
                // request
                writeCvButton.setEnabled(MapUtils.isNotEmpty(selectedNode.getConfigVariables()));
            }
        }
    }

    private String getCvName(String keyword) {

        final CvNode cvNode = StepControlCvUtils.getNode(mapKeywordToNode, keyword);
        if (cvNode != null) {
            return cvNode.getConfigVar().getName();
        }
        return null;
    }

    /**
     * Prepare the configured aspects of the turntable from the CV values.
     * 
     * @param cvNumberToJideNodeMap
     *            the cv number to node map
     * @param errorDetected
     *            holder for error detected flag
     * @return the list of aspects
     */
    private List prepareConfiguredAspects(
        final Map cvNumberToJideNodeMap, final AtomicBoolean errorDetected) {

        // If round turntable is configured then the aspects are symetric. This means that only half of the aspects
        // are usable for tracks because the other half is used for the opposite position.
        boolean roundTurnTable = TurnTableType.round == stepControlModel.getTurnTableType();

        long oppositePositionStartPosition = -1;

        Long totalSteps = stepControlModel.getTotalSteps();
        if (totalSteps != null && totalSteps.longValue() > 0) {
            oppositePositionStartPosition = totalSteps.longValue() / 2;
        }

        LOGGER
            .info("Prepare the aspects for turntable type: {}, totalSteps: {}, oppositePositionStartPosition: {}",
                stepControlModel.getTurnTableType(), totalSteps, oppositePositionStartPosition);

        // keep the first aspect with opposite position
        StepControlAspect firstOppositeStepControlAspect = null;
        int maxAspectCount = 0;

        // create the list of aspects with the values from the config variables
        final List configuredAspects = new ArrayList<>();
        int maxIndex = StepControlControllerInterface.MAX_CONFIGURED_ASPECTS;

        for (int index = 0; index < maxIndex; index++) {
            try {
                Integer targetPolarity =
                    StepControlCvUtils
                        .getConfigVarIntValue(mapKeywordToNode, cvNumberToJideNodeMap,
                            String.format(StepControlKeywords.KEYWORD_PATTERN_POLARITY, index));
                Long targetPosition =
                    StepControlCvUtils
                        .getConfigVarLongValue(mapKeywordToNode,
                            String.format(StepControlKeywords.KEYWORD_PATTERN_POSITION, index));

                if (targetPosition < INACTIVE_POSITION_VALUE) {

                    StepControlAspect stepControlAspect =
                        new StepControlAspect(null, targetPosition.longValue(),
                            Polarity.valueOf(targetPolarity.intValue()));
                    stepControlAspect.setStatus(AspectPersistanceStatus.statusPersistent);

                    LOGGER.info("Current index: {}, adding new stepControlAspect: {}", index, stepControlAspect);
                    configuredAspects.add(stepControlAspect);

                    if (roundTurnTable && firstOppositeStepControlAspect == null
                        && stepControlAspect.getPosition().longValue() > oppositePositionStartPosition) {
                        firstOppositeStepControlAspect = stepControlAspect;
                        LOGGER
                            .info(
                                "Found the first opposite position aspect based on the position and totalSteps at position: {}, firstOppositeStepControlAspect: {}",
                                stepControlAspect.getPosition().longValue(), firstOppositeStepControlAspect);

                        maxAspectCount = configuredAspects.size() - 1;
                    }
                }
                else {
                    LOGGER
                        .info("No active position at index {}, skip further creation of aspects, position: {}", index,
                            targetPosition);
                    break;
                }
            }
            catch (IllegalArgumentException ex) {
                LOGGER.warn("Prepare configured step control aspect failed: {}", ex.getMessage());
                errorDetected.set(true);
            }
            catch (Exception ex) {
                LOGGER.warn("Prepare configured step control aspect failed.", ex);
                errorDetected.set(true);
            }
        }

        // process the configured aspects
        if (TurnTableType.round == stepControlModel.getTurnTableType()) {

            // The problem with round type is that we must first detect the total configured number of aspects
            // and then assume that the second half is the opposite. Maybe we can check if the position value is
            // more than the half number of steps.

            int maxTrackPositions = configuredAspects.size();
            LOGGER.info("Number of aspects read from cv values: {}", maxTrackPositions);

            if (maxAspectCount == 0 && maxTrackPositions > 0) {
                maxAspectCount = maxTrackPositions / 2;
            }

            final List mergedConfiguredAspects = new ArrayList<>();

            // when we get the opposite aspect we must check if the position is more
            // than the half of total steps.

            boolean foundFirstOppositePosition = false;

            for (int index = 0; index < maxAspectCount; index++) {

                StepControlAspect stepControlAspect = configuredAspects.get(index);
                StepControlAspect stepControlAspectOpposite = null;
                if ((index + maxAspectCount) < maxTrackPositions) {
                    stepControlAspectOpposite = configuredAspects.get(index + maxAspectCount);
                }
                LOGGER
                    .info("Fetched stepControlAspect: {}, stepControlAspectOpposite: {}", stepControlAspect,
                        stepControlAspectOpposite);

                if (stepControlAspectOpposite != null && stepControlAspectOpposite.getPosition() != null
                    && stepControlAspectOpposite.getPosition().longValue() >= oppositePositionStartPosition) {

                    if (!foundFirstOppositePosition) {
                        LOGGER.info("The first opposite position was found: {}", stepControlAspectOpposite);

                        foundFirstOppositePosition = true;
                    }

                    LOGGER
                        .info("Merge the values from opposite aspect: {}, stepControlAspect: {}",
                            stepControlAspectOpposite, stepControlAspect);
                    stepControlAspect.setOppositePosition(stepControlAspectOpposite.getPosition());
                    stepControlAspect.setOppositePolarity(stepControlAspectOpposite.getPolarity());

                    // keep a reference to the opposite aspect
                    LOGGER.info("Set the opposite aspect on aspect: {}", stepControlAspect);
                    stepControlAspect.setOppositeAspect(stepControlAspectOpposite);
                }

                LOGGER.info("Adding merged stepControlAspect: {}", stepControlAspect);
                mergedConfiguredAspects.add(stepControlAspect);
            }

            configuredAspects.clear();
            configuredAspects.addAll(mergedConfiguredAspects);
        }
        else if (TurnTableType.linear == stepControlModel.getTurnTableType()) {
            // linear turntable
        }

        else if (TurnTableType.pendular == stepControlModel.getTurnTableType()) {
            // pendular turntable

            // remove the surplus aspects, only 2 aspects allowed
            if (configuredAspects.size() > 2) {
                // get only the first 2 aspects from the model
                // configuredAspects.clear();
                final List subListAspects = new ArrayList<>(configuredAspects.subList(0, 2));
                configuredAspects.clear();
                configuredAspects.addAll(subListAspects);
            }
        }

        return configuredAspects;
    }

    public void executionStateChanged(
        AccessoryExecutionState executionState, Integer accessoryId, Integer aspect, AccessoryState accessoryState) {
        LOGGER
            .info("The execution state has changed: {}, accessoryId: {}, aspect: {}", executionState, accessoryId,
                aspect);
        if (aspect == null || executionState == null) {
            executionState = AccessoryExecutionState.IDLE;
        }

        executionStateIconLabel.setToolTipText(null);
        switch (executionState) {
            case ERROR:
                executionStateIconLabel.setIcon(accessoryErrorIcon);
                executionStateIconLabel
                    .setText(Resources.getString(StepControlPanel.class, "unknown_error_aspect", aspect));
                if (accessoryState != null) {
                    executionStateIconLabel.setToolTipText(accessoryState.getErrorInformation());
                }
                break;
            case RUNNING:
                executionStateIconLabel.setIcon(accessoryWaitIcon);
                executionStateIconLabel
                    .setText(Resources.getString(StepControlPanel.class, "executing_aspect", aspect));
                break;
            case SUCCESSFUL:
                executionStateIconLabel.setIcon(accessorySuccessfulIcon);
                executionStateIconLabel.setText(Resources.getString(StepControlPanel.class, "active_aspect", aspect));
                break;
            case UNKNOWN:
                executionStateIconLabel.setIcon(accessoryUnknownIcon);
                executionStateIconLabel.setText(null);
                break;
            default:
                executionStateIconLabel.setIcon(null);
                executionStateIconLabel.setText(null);
                break;
        }

        synchronized (aspectExecutionModel) {
            if (aspectExecutionModel.getAccessoryId() == accessoryId && aspectExecutionModel.getAspectId() == aspect) {
                LOGGER.info("Update the aspectExecutionModel.");
                aspectExecutionModel.setExecutionState(executionState);
                aspectExecutionModel.setAccessoryState(accessoryState);
            }
        }
    }

    private void addToolBarButtons(VLToolBar toolBar) {

        // read CV button
        readCvButton =
            makeNavigationButton("loadfromnode", "/32x32", READ, Resources.getString(getClass(), "toolbar.readallcv"),
                Resources.getString(getClass(), "toolbar.readallcv.alttext"));
        readCvButton.setEnabled(false);
        toolBar.add(readCvButton);

        readCvButton.addActionListener(new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent e) {
                readCurrentValuesFromCV(selectedNode);
            }
        });

        // write CV button
        writeCvButton =
            makeNavigationButton("savetonode", "/32x32", WRITE, Resources.getString(getClass(), "toolbar.writeallcv"),
                Resources.getString(getClass(), "toolbar.writeallcv.alttext"));
        toolBar.add(writeCvButton);
        writeCvButton.setEnabled(false);

        writeCvButton.addActionListener(new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent e) {

                try {
                    prepareAndWriteCvValuesToNode(false);
                }
                catch (InvalidAspectException ex) {
                    LOGGER.warn("Write CV value to node failed because an invalid aspect was detected.", ex);

                    int currentAspectIndex = ex.getAspectIndex();
                    if (currentAspectIndex > -1) {
                        LOGGER.info("Invalid data in currentAspectIndex: {}", currentAspectIndex);
                        aspectTable.expandRow(currentAspectIndex);
                        aspectTable.scrollRowToVisible(currentAspectIndex);
                    }

                    TaskDialogs
                        .build(JOptionPane.getFrameForComponent(getComponent()),
                            Resources.getString(StepControlPanel.class, "write-aspect-failed.instruction"),
                            Resources.getString(StepControlPanel.class, "write-aspect-failed.text", currentAspectIndex))
                        .title(Resources.getString(StepControlPanel.class, "write-aspect-failed.title"))
                        .showException(ex);
                }
                catch (Exception ex) {
                    LOGGER.warn("Write CV value to node failed.", ex);

                    TaskDialogs
                        .build(JOptionPane.getFrameForComponent(getComponent()),
                            Resources.getString(StepControlPanel.class, "write-cv-failed.instruction"),
                            Resources.getString(StepControlPanel.class, "write-cv-failed.text"))
                        .title(Resources.getString(StepControlPanel.class, "write-cv-failed.title")).showException(ex);
                }
            }
        });
    }

    private JButton makeNavigationButton(
        String imageName, String pathExt, String actionCommand, String toolTipText, String altText) {
        // Look for the image.
        String imgLocation = "/icons/" + imageName + ".png";
        if (pathExt != null) {
            imgLocation = "/icons" + pathExt + "/" + imageName + ".png";
        }
        URL imageURL = StepControlPanel.class.getResource(imgLocation);

        // Create and initialize the button.
        JButton button = new JButton();
        button.setActionCommand(actionCommand);
        button.setToolTipText(toolTipText);

        if (imageURL != null) { // image found
            button.setIcon(new ImageIcon(imageURL, altText));
        }
        else { // no image found
            button.setText(altText);
            LOGGER.warn("Resource not found: {}", imgLocation);
        }

        return button;
    }

    private void addToolBar(final VLToolBar toolBar, ToolBarConstraints constraints) {
        ToolBarPanel topToolBarPanel =
            (ToolBarPanel) DefaultApplicationContext.getInstance().get(DefaultApplicationContext.KEY_TOPTOOLBARPANEL);
        topToolBarPanel.add(toolBar, constraints);
    }

    private void prepareAndWriteCvValuesToNode(boolean writeOnlyAspects) {

        LOGGER.info("Write the CV values, writeOnlyAspects: {}", writeOnlyAspects);

        final Map cvNumberToNodeMap = getCvNumberToNodeMap(selectedNode);

        // prepare the cv list to write
        final List cvList = new ArrayList<>();

        // collect the new values
        int aspectIndex = 0;

        // get the aspects from the controller
        final List aspects = this.stepControlController.getConfigureAspectsListModel();

        // check the aspects
        for (StepControlAspect currentAspect : aspects) {
            LOGGER.info("Prepare aspect to save: {}", currentAspect);

            // prepare the CV values of all aspects
            prepareCvValues(aspectIndex, currentAspect, cvList, cvNumberToNodeMap);

            aspectIndex++;
        }

        // add the terminating aspects by setting 0xFFFF as position
        if (aspectIndex < StepControlControllerInterface.MAX_CONFIGURED_ASPECTS) {
            StepControlAspect currentAspect = new StepControlAspect(null, INACTIVE_POSITION_VALUE, Polarity.normal);
            LOGGER.info("Add the terminating aspect: {}", currentAspect);

            prepareCvValues(aspectIndex, currentAspect, cvList, cvNumberToNodeMap);

            aspectIndex++;
        }

        if (!writeOnlyAspects) {
            // check the turntable type
            final CvNode tableTypeNode =
                StepControlCvUtils.getNode(mapKeywordToNode, StepControlKeywords.KEYWORD_TABLETYPE);
            TurnTableType turnTableType = stepControlModel.getTurnTableType();
            CvValueUtils
                .compareAndAddNewValue(tableTypeNode, Integer.toString(turnTableType.getCvValue()), cvList,
                    cvNumberToNodeMap);

            // check the speed
            CvNode speedNode = StepControlCvUtils.getNode(mapKeywordToNode, StepControlKeywords.KEYWORD_SPEED);
            int speed = stepControlModel.getSpeed();
            CvValueUtils.compareAndAddNewValue(speedNode, Integer.toString(speed), cvList, cvNumberToNodeMap);

            // check the accel
            CvNode accelNode = StepControlCvUtils.getNode(mapKeywordToNode, StepControlKeywords.KEYWORD_ACCEL);
            int accel = stepControlModel.getAccel();
            CvValueUtils.compareAndAddNewValue(accelNode, Integer.toString(accel), cvList, cvNumberToNodeMap);

            // check the decel
            CvNode decelNode = StepControlCvUtils.getNode(mapKeywordToNode, StepControlKeywords.KEYWORD_DECEL);
            int decel = stepControlModel.getDecel();
            CvValueUtils.compareAndAddNewValue(decelNode, Integer.toString(decel), cvList, cvNumberToNodeMap);
        }
        else {
            LOGGER.info("Only write aspects to node.");
        }

        final CvDefinitionPanelProvider cvDefinitionPanelProvider = StepControlPanel.this;
        // write the values on the node
        CvValueUtils.writeCvValues(selectedNode, cvList, cvNumberToNodeMap, cvDefinitionPanelProvider);

        if (!hasPendingChanges()) {
            // set the persistence status of all aspects to persistent
            LOGGER.info("Set the persistence status of all aspects to persistent.");

            for (StepControlAspect currentAspect : stepControlModel.getStepControlAspects()) {
                currentAspect.setStatus(AspectPersistanceStatus.statusPersistent);
            }
        }
        else {
            LOGGER.info("Pending changes detected after write CV values to node.");
        }
    }

    private boolean checkHasPendingAspects(final Map cvNumberToNodeMap) {

        // collect the new values
        int aspectIndex = 0;

        // get the aspects from the controller
        final List aspects = this.stepControlController.getConfigureAspectsListModel();

        // check the aspects
        for (StepControlAspect currentAspect : aspects) {
            LOGGER.info("Prepare aspect to save: {}", currentAspect);

            // prepare the CV values of all aspects
            boolean changeDetected = checkChangedCvValues(aspectIndex, currentAspect, cvNumberToNodeMap);
            if (changeDetected) {
                return true;
            }

            aspectIndex++;
        }

        // add the terminating aspects by setting 0xFFFF as position
        if (aspectIndex < StepControlControllerInterface.MAX_CONFIGURED_ASPECTS) {
            StepControlAspect currentAspect = new StepControlAspect(null, INACTIVE_POSITION_VALUE, Polarity.normal);
            LOGGER.info("Add the terminating aspect: {}", currentAspect);

            boolean changeDetected = checkChangedCvValues(aspectIndex, currentAspect, cvNumberToNodeMap);
            if (changeDetected) {
                return true;
            }

            aspectIndex++;
        }

        return false;
    }

    private Map getCvNumberToNodeMap(final NodeInterface node) {
        // get the prepared CvDefinitionTreeTableModel from the CvDefinitionPanelController
        CvDefinitionPanelController cvDefinitionPanelController =
            DefaultApplicationContext
                .getInstance()
                .get(DefaultApplicationContext.KEY_CVDEFINITIONPANEL_CONTROLLER, CvDefinitionPanelController.class);

        final Map cvNumberToNodeMap = cvDefinitionPanelController.getCvNumberToNodeMap(node);
        return cvNumberToNodeMap;
    }

    /**
     * Write the CV values provided by the configurationWizardModel.
     * 
     * @param configurationWizardModel
     *            the configuration wizard model
     */
    private void writeConfigurationValues(
        final ConfigurationWizardModel configurationWizardModel, final Map cvNumberToNodeMap,
        final Map mapKeywordToNode) {
        LOGGER.info("Write the values from the configuration wizard model to the node.");

        // prepare the cv list to write
        final List cvList = new ArrayList<>();

        // check the turntable type
        CvNode tableTypeNode = StepControlCvUtils.getNode(mapKeywordToNode, StepControlKeywords.KEYWORD_TABLETYPE);
        TurnTableType turnTableType = configurationWizardModel.getTurnTableType();
        stepControlModel.setTurnTableType(turnTableType);
        CvValueUtils
            .compareAndAddNewValue(tableTypeNode, Integer.toString(turnTableType.getCvValue()), cvList,
                cvNumberToNodeMap);

        // check the motor type
        CvNode nemaTypeNode = StepControlCvUtils.getNode(mapKeywordToNode, StepControlKeywords.KEYWORD_NEMATYPE);
        MotorSizeType motorSizeType = configurationWizardModel.getMotorSizeType();
        CvValueUtils
            .compareAndAddNewValue(nemaTypeNode, Integer.toString(motorSizeType.getCvValue()), cvList,
                cvNumberToNodeMap);

        // // prepare the current moving and stopped values
        // CvNode currentMovingNode = getNode(StepControlKeywords.KEYWORD_CURRENT_MOVING);
        // // the values are stored in 10mA units
        // int currentMoving = motorSizeType.getCurrentMoving();
        // CvValueUtils
        // .compareAndAddNewValue(currentMovingNode, Integer.toString(currentMoving / 10), cvList, cvNumberToNodeMap);
        // // prepare the current moving and stopped values
        // CvNode currentStoppedNode = getNode(StepControlKeywords.KEYWORD_CURRENT_STOPPED);
        // // the values are stored in 10mA units
        // int currentStopped = motorSizeType.getCurrentStopped();
        // CvValueUtils
        // .compareAndAddNewValue(currentStoppedNode, Integer.toString(currentStopped / 10), cvList,
        // cvNumberToNodeMap);

        // check the step count
        CvNode stepCountNode = StepControlCvUtils.getNode(mapKeywordToNode, StepControlKeywords.KEYWORD_STEPCOUNT);
        Integer stepCount = configurationWizardModel.getStepCount();

        // make sure step count is available and not zero
        if (stepCount == null || stepCount == 0) {
            LOGGER.warn("Adjusting invalid stepCount to default value 1.");
            stepCount = 1;
        }

        CvValueUtils.compareAndAddNewValue(stepCountNode, Integer.toString(stepCount), cvList, cvNumberToNodeMap);

        // check the gear primary
        CvNode gearPrimaryNode = StepControlCvUtils.getNode(mapKeywordToNode, StepControlKeywords.KEYWORD_GEAR_PRIMARY);
        Integer gearRatioPrimary = configurationWizardModel.getGearing().getGearRatioPrimary();
        CvValueUtils
            .compareAndAddNewValue(gearPrimaryNode, Integer.toString(gearRatioPrimary), cvList, cvNumberToNodeMap);

        // check the gear secondary
        CvNode gearSecondaryNode =
            StepControlCvUtils.getNode(mapKeywordToNode, StepControlKeywords.KEYWORD_GEAR_SECONDARY);
        Integer gearRatioSecondary = configurationWizardModel.getGearing().getGearRatioSecondary();
        CvValueUtils
            .compareAndAddNewValue(gearSecondaryNode, Integer.toString(gearRatioSecondary), cvList, cvNumberToNodeMap);

        // check the total steps
        LongCvNode totalStepsNode =
            (LongCvNode) StepControlCvUtils.getNode(mapKeywordToNode, StepControlKeywords.KEYWORD_TOTALSTEPS);
        Integer totalStepCount = configurationWizardModel.getTotalStepCount();
        CvValueUtils.compareAndAddNewValue(totalStepsNode, totalStepCount, cvList);

        // prepare the gearing
        if (Gearing.YES.equals(configurationWizardModel.getGearing().getKey())) {
            CvNode backlashNode = StepControlCvUtils.getNode(mapKeywordToNode, StepControlKeywords.KEYWORD_BACKLASH);
            Integer backlash = configurationWizardModel.getGearing().getBackLash();
            CvValueUtils.compareAndAddNewValue(backlashNode, Integer.toString(backlash), cvList, cvNumberToNodeMap);
        }
        else {
            LOGGER.info("No gearing selected.");

            CvNode backlashNode = StepControlCvUtils.getNode(mapKeywordToNode, StepControlKeywords.KEYWORD_BACKLASH);
            Integer backlash = 0;
            CvValueUtils.compareAndAddNewValue(backlashNode, Integer.toString(backlash), cvList, cvNumberToNodeMap);
        }

        // check the microstepping
        CvNode microSteppingNode =
            StepControlCvUtils.getNode(mapKeywordToNode, StepControlKeywords.KEYWORD_MICROSTEPPING);
        Integer microStepping =
            (configurationWizardModel.getMicroStepping() != null
                ? configurationWizardModel.getMicroStepping().getSteps() : null);

        // make sure microStepping is available and not zero
        if (microStepping == null || microStepping == 0) {
            LOGGER.warn("Adjusting invalid microstepping to default value 64.");
            microStepping = 64;
        }

        CvValueUtils
            .compareAndAddNewValue(microSteppingNode, Integer.toString(microStepping), cvList, cvNumberToNodeMap);

        // write the values on the node
        CvValueUtils.writeCvValues(selectedNode, cvList, cvNumberToNodeMap, StepControlPanel.this);

        // update the renderer
        try {
            Long totalSteps =
                StepControlCvUtils.getConfigVarLongValue(mapKeywordToNode, StepControlKeywords.KEYWORD_TOTALSTEPS);
            if (angleRenderer != null) {
                angleRenderer.setTotalSteps(totalSteps);
            }
        }
        catch (Exception ex) {
            LOGGER.warn("Set the total step count failed.", ex);
        }

    }

    private void prepareCvValues(
        int aspectIndex, StepControlAspect currentAspect, final List cvList,
        final Map cvNumberToNodeMap) {

        // prepare the keyword to search
        String keywordPolarity = String.format(StepControlKeywords.KEYWORD_PATTERN_POLARITY, aspectIndex);
        CvNode polarityNode = StepControlCvUtils.getNode(mapKeywordToNode, keywordPolarity);

        Polarity polarity = currentAspect.getPolarity();
        CvValueUtils
            .compareAndAddNewValue(polarityNode, Integer.toString(polarity.getCvValue()), cvList, cvNumberToNodeMap);

        // the target position is a long type
        String keywordPosition = String.format(StepControlKeywords.KEYWORD_PATTERN_POSITION, aspectIndex);
        // get the master
        LongCvNode positionNode = (LongCvNode) StepControlCvUtils.getNode(mapKeywordToNode, keywordPosition);

        Long position = currentAspect.getPosition();
        // prepare the master value
        LOGGER.debug("The new position is: {}", position);

        if (position != null) {
            CvValueUtils.compareAndAddNewValue(positionNode, Long.toString(position), cvList, cvNumberToNodeMap);
        }
        else {
            LOGGER.warn("No position of aspect available. Current aspect: {}", currentAspect);
        }
    }

    /**
     * Check for changed cv values but do not touch the cv nodes.
     */
    private boolean checkChangedCvValues(
        int aspectIndex, StepControlAspect currentAspect, final Map cvNumberToNodeMap) {

        // prepare the keyword to search
        String keywordPolarity = String.format(StepControlKeywords.KEYWORD_PATTERN_POLARITY, aspectIndex);
        CvNode polarityNode = StepControlCvUtils.getNode(mapKeywordToNode, keywordPolarity);

        Polarity polarity = currentAspect.getPolarity();
        boolean changeDetected =
            CvValueUtils.hasChangedNewValue(polarityNode, Integer.toString(polarity.getCvValue()), cvNumberToNodeMap);
        if (changeDetected) {
            return true;
        }

        // the target position is a long type
        String keywordPosition = String.format(StepControlKeywords.KEYWORD_PATTERN_POSITION, aspectIndex);
        // get the master
        LongCvNode positionNode = (LongCvNode) StepControlCvUtils.getNode(mapKeywordToNode, keywordPosition);

        Long position = currentAspect.getPosition();
        // prepare the master value
        LOGGER.debug("The new position is: {}", position);

        if (position != null) {
            changeDetected = CvValueUtils.hasChangedNewValue(positionNode, Long.toString(position), cvNumberToNodeMap);
            if (changeDetected) {
                return true;
            }
        }
        else {
            LOGGER.warn("No position of aspect available. Current aspect: {}", currentAspect);
        }

        return false;
    }

    @Override
    public void checkPendingChanges() {
        boolean hasPendingChanges = hasPendingChanges();
        if (!hasPendingChanges) {
            resetPendingChanges();
        }
    }

    private void resetPendingChanges() {
        LOGGER.info("Reset pending changes.");
        tabStatusListener.updatePendingChanges(component, false);
    }

    private boolean hasPendingChanges() {
        // TODO implement hasPendingChanges()
        final Map cvNumberToNodeMap = getCvNumberToNodeMap(selectedNode);
        return checkHasPendingAspects(cvNumberToNodeMap);
    }

    @Override
    public void writeConfigVariables(List cvList) {
        fireWriteConfigVariables(cvList);
    }

    private void fireWriteConfigVariables(List cvList) {
        // TODO decouple the AWT-thread from this work?

        // add line
        CvConsoleModel cvConsoleModel = CvConsoleModel.getConsoleModel();
        for (ConfigurationVariable cv : cvList) {
            cvConsoleModel
                .addConsoleLine(ConsoleColor.black, String.format(">> CV %s : %s", cv.getName(), cv.getValue()));
        }

        for (CvDefinitionRequestListener l : cvDefinitionRequestListeners) {
            l.writeCvValues(cvList);
        }
    }

    private void fireWriteConfigVariablesPosition(List cvList) {
        // TODO decouple the AWT-thread from this work?

        LOGGER.info("Write cvValue for current position: {}", cvList);
        Frame frame = JOptionPane.getFrameForComponent(getComponent());
        try {
            if (frame instanceof BusyFrame) {
                ((BusyFrame) frame).setBusy(true);
            }

            final Map cvNumberToNodeMap = getCvNumberToNodeMap(selectedNode);

            final CvDefinitionPanelProvider cvDefinitionPanelProvider = StepControlPanel.this;
            // write the values on the node
            CvValueUtils.writeCvValues(selectedNode, cvList, cvNumberToNodeMap, cvDefinitionPanelProvider);

            String currentPositionValue = null;

            for (ConfigurationVariable configVar : cvList) {
                LOGGER.info("Update the configVar in the model: {}", configVar);
                selectedNode.getConfigVariables().put(configVar.getName(), configVar);

                currentPositionValue = configVar.getValue();
            }

            try {
                CvNode currentPositionNode =
                    StepControlCvUtils.getNode(cvNumberToNodeMap, StepControlKeywords.CVKEY_CURRENT_POSITION);

                currentPositionNode.getConfigVar().setValue(currentPositionValue);

                Long currentPosition = Long.parseLong(currentPositionValue);
                LOGGER.info("Set the current position: {}", currentPosition);

                stepControlModel.setCurrentPosition(currentPosition);

                LOGGER
                    .info(
                        "The current position was read and no more CV values are updated! The current position is: {}",
                        currentPosition);

            }
            catch (Exception ex) {
                LOGGER.warn("Update the current position failed.", ex);
            }
        }
        finally {
            if (frame instanceof BusyFrame) {
                ((BusyFrame) frame).setBusy(false);
            }
        }
    }

    private List fireLoadConfigVariablesPosition(List configVariables) {
        // TODO decouple the AWT-thread from this work?
        LOGGER.info("Load the config variables for the positions.");
        for (CvDefinitionRequestListener l : cvDefinitionRequestListeners) {
            return l.loadCvValues(configVariables);
        }

        return Collections.emptyList();
    }

    @Override
    public void labelChanged(final MotorPort port, String label) {

    }

    @Override
    public void valueChanged(final NodeInterface node, final MotorPort port) {
        LOGGER.info("The motor port value has changed: {}", port);

        // if (port.getId() == 0) {
        // MotorPort motorPort = (MotorPort) port;
        // motorSliderEditor.setValue(motorPort.getValue());
        // }
    }

    @Override
    public void configChanged(final NodeInterface node, final MotorPort port) {

    }

    @Override
    public Class getPortClass() {
        return MotorPort.class;
    }

    private JPanel createFunctionButtonPanel(final JPanel parentPanel, final List functionButtons) {

        FormBuilder formBuilder =
            FormBuilder
                .create()
                .columns("pref, 8dlu, pref, 8dlu, pref, 8dlu, pref, 8dlu, pref, 8dlu, pref, 8dlu, pref, 8dlu, pref")
                .rows("p, 3dlu, p").panel(parentPanel);

        formBuilder.addSeparator(Resources.getString(getClass(), "additionalFunctions")).xyw(1, 1, 15);

        int totalSoundPorts = 0;
        if (selectedNode != null) {
            Feature feature =
                Feature
                    .findFeature(selectedNode.getNode().getFeatures(),
                        FeatureEnum.FEATURE_CTRL_SOUND_COUNT.getNumber());
            if (feature != null) {
                totalSoundPorts = feature.getValue();
            }
        }
        LOGGER.info("Total number of sound ports: {}", totalSoundPorts);

        // add the master sound button
        final ImageIcon iconSoundMute =
            ImageUtils.createImageIcon(StepControlPanel.class, "/icons/stepcontrol/sound_mute.png");
        final ImageIcon iconSoundOn =
            ImageUtils.createImageIcon(StepControlPanel.class, "/icons/stepcontrol/sound.png");

        soundActiveButton = new JideToggleButton(Resources.getString(getClass(), "soundActive"));
        soundActiveButton.setButtonStyle(JideButton.TOOLBOX_STYLE);
        soundActiveButton.setSelectedIcon(iconSoundMute);
        soundActiveButton.setIcon(iconSoundOn);
        soundActiveButton.addActionListener(evt -> {

            boolean isSoundMute = soundActiveButton.isSelected();

            LOGGER.info("Pressed the sound active button, isSoundMute: {}", isSoundMute);

            // activate / deactivate the sound
            stepControlController.setSoundActive(!isSoundMute);
        });

        boolean soundActive = stepControlModel.isSoundActive();
        soundActiveButton.setSelected(!soundActive);

        formBuilder.add(soundActiveButton).xyw(1, 3, 5);
        formBuilder.appendRows("10dlu, p");

        // add the sound function buttons
        final int columns = 8;
        int currentSoundPortId = 0;
        for (int row = 0; row < 4; row++) {

            for (int column = 0; column < columns; column++) {
                final int functionIndex = row * columns + (column);
                final String buttonText = "F" + functionIndex;
                final JideButton functionButton = new JideButton(buttonText);
                functionButton.setButtonStyle(JideButton.TOOLBOX_STYLE);

                functionButton.addActionListener(new ActionListener() {
                    @Override
                    public void actionPerformed(ActionEvent e) {
                        LOGGER.info("Trigger sound port with index: {}", functionIndex);
                        SoundPortStatus soundPortStatus = SoundPortStatus.PLAY;

                        stepControlController.triggerSoundPort(functionIndex, soundPortStatus);
                    }
                });
                functionButtons.add(functionButton);

                functionButtonMap.put(buttonText, functionButton);

                formBuilder.add(functionButton).xy((column * 2) + 1, (row * 2) + 5);

                currentSoundPortId++;
                if (currentSoundPortId >= totalSoundPorts) {
                    LOGGER.info("Maximum number of sound ports reached.");
                    break;
                }
            }

            formBuilder.appendRows("3dlu, p");

            if (currentSoundPortId >= totalSoundPorts) {
                LOGGER.info("Maximum number of sound ports reached.");
                break;
            }
        }

        return formBuilder.build();
    }

    @Override
    public void setActiveAspect(SwitchingNodeInterface node, int accessoryNumber, int aspectNumber) {
        LOGGER.info("Set the active aspect: {}", aspectNumber);

        if (SwingUtilities.isEventDispatchThread()) {
            firePerformAspect(aspectNumber);
        }
        else {
            try {
                SwingUtilities.invokeAndWait(() -> firePerformAspect(aspectNumber));
            }
            catch (InvocationTargetException | InterruptedException ex) {
                LOGGER
                    .warn("Activate aspect failed, accessoryNumber: {}, aspectNumber: {}", accessoryNumber,
                        aspectNumber, ex);

                throw new RuntimeException(
                    "Activate aspect failed, accessoryNumber: " + accessoryNumber + ", aspect: " + aspectNumber);
            }
        }
    }

    @Override
    public AccessoryExecutionState getAccessoryExecutionState(SwitchingNodeInterface node, int accessoryNumber) {
        synchronized (aspectExecutionModel) {
            return aspectExecutionModel.getExecutionState();
        }
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy