/*
 * Copyright (C) 2007 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.ddmuilib;

import com.android.ddmlib.EmulatorConsole;
import com.android.ddmlib.EmulatorConsole.GsmMode;
import com.android.ddmlib.EmulatorConsole.GsmStatus;
import com.android.ddmlib.EmulatorConsole.NetworkStatus;
import com.android.ddmlib.IDevice;
import com.android.ddmuilib.location.CoordinateControls;
import com.android.ddmuilib.location.GpxParser;
import com.android.ddmuilib.location.GpxParser.Track;
import com.android.ddmuilib.location.KmlParser;
import com.android.ddmuilib.location.TrackContentProvider;
import com.android.ddmuilib.location.TrackLabelProvider;
import com.android.ddmuilib.location.TrackPoint;
import com.android.ddmuilib.location.WayPoint;
import com.android.ddmuilib.location.WayPointContentProvider;
import com.android.ddmuilib.location.WayPointLabelProvider;

import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.TableViewer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.SWTException;
import org.eclipse.swt.custom.ScrolledComposite;
import org.eclipse.swt.custom.StackLayout;
import org.eclipse.swt.events.ControlAdapter;
import org.eclipse.swt.events.ControlEvent;
import org.eclipse.swt.events.ModifyEvent;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.FileDialog;
import org.eclipse.swt.widgets.Group;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.TabFolder;
import org.eclipse.swt.widgets.TabItem;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.Text;

/**
 * Panel to control the emulator using EmulatorConsole objects.
 */
public class EmulatorControlPanel extends SelectionDependentPanel {

    // default location: Patio outside Charlie's
    private final static double DEFAULT_LONGITUDE = -122.084095;
    private final static double DEFAULT_LATITUDE = 37.422006;

    private final static String SPEED_FORMAT = "Speed: %1$dX";


    /**
     * Map between the display gsm mode and the internal tag used by the display.
     */
    private final static String[][] GSM_MODES = new String[][] {
        { "unregistered", GsmMode.UNREGISTERED.getTag() },
        { "home", GsmMode.HOME.getTag() },
        { "roaming", GsmMode.ROAMING.getTag() },
        { "searching", GsmMode.SEARCHING.getTag() },
        { "denied", GsmMode.DENIED.getTag() },
    };

    private final static String[] NETWORK_SPEEDS = new String[] {
        "Full",
        "GSM",
        "HSCSD",
        "GPRS",
        "EDGE",
        "UMTS",
        "HSDPA",
    };

    private final static String[] NETWORK_LATENCIES = new String[] {
        "None",
        "GPRS",
        "EDGE",
        "UMTS",
    };

    private final static int[] PLAY_SPEEDS = new int[] { 1, 2, 5, 10, 20, 50 };

    private final static String RE_PHONE_NUMBER = "^[+#0-9]+$"; //$NON-NLS-1$
    private final static String PREFS_WAYPOINT_COL_NAME = "emulatorControl.waypoint.name"; //$NON-NLS-1$
    private final static String PREFS_WAYPOINT_COL_LONGITUDE = "emulatorControl.waypoint.longitude"; //$NON-NLS-1$
    private final static String PREFS_WAYPOINT_COL_LATITUDE = "emulatorControl.waypoint.latitude"; //$NON-NLS-1$
    private final static String PREFS_WAYPOINT_COL_ELEVATION = "emulatorControl.waypoint.elevation"; //$NON-NLS-1$
    private final static String PREFS_WAYPOINT_COL_DESCRIPTION = "emulatorControl.waypoint.desc"; //$NON-NLS-1$
    private final static String PREFS_TRACK_COL_NAME = "emulatorControl.track.name"; //$NON-NLS-1$
    private final static String PREFS_TRACK_COL_COUNT = "emulatorControl.track.count"; //$NON-NLS-1$
    private final static String PREFS_TRACK_COL_FIRST = "emulatorControl.track.first"; //$NON-NLS-1$
    private final static String PREFS_TRACK_COL_LAST = "emulatorControl.track.last"; //$NON-NLS-1$
    private final static String PREFS_TRACK_COL_COMMENT = "emulatorControl.track.comment"; //$NON-NLS-1$

    private EmulatorConsole mEmulatorConsole;

    private Composite mParent;

    private Label mVoiceLabel;
    private Combo mVoiceMode;
    private Label mDataLabel;
    private Combo mDataMode;
    private Label mSpeedLabel;
    private Combo mNetworkSpeed;
    private Label mLatencyLabel;
    private Combo mNetworkLatency;

    private Label mNumberLabel;
    private Text mPhoneNumber;

    private Button mVoiceButton;
    private Button mSmsButton;

    private Label mMessageLabel;
    private Text mSmsMessage;

    private Button mCallButton;
    private Button mCancelButton;

    private TabFolder mLocationFolders;

    private Button mDecimalButton;
    private Button mSexagesimalButton;
    private CoordinateControls mLongitudeControls;
    private CoordinateControls mLatitudeControls;
    private Button mGpxUploadButton;
    private Table mGpxWayPointTable;
    private Table mGpxTrackTable;
    private Button mKmlUploadButton;
    private Table mKmlWayPointTable;

    private Button mPlayGpxButton;
    private Button mGpxBackwardButton;
    private Button mGpxForwardButton;
    private Button mGpxSpeedButton;
    private Button mPlayKmlButton;
    private Button mKmlBackwardButton;
    private Button mKmlForwardButton;
    private Button mKmlSpeedButton;

    private Image mPlayImage;
    private Image mPauseImage;

    private Thread mPlayingThread;
    private boolean mPlayingTrack;
    private int mPlayDirection = 1;
    private int mSpeed;
    private int mSpeedIndex;

    private final SelectionAdapter mDirectionButtonAdapter = new SelectionAdapter() {
        @Override
        public void widgetSelected(SelectionEvent e) {
            Button b = (Button)e.getSource();
            if (b.getSelection() == false) {
                // basically the button was unselected, which we don't allow.
                // so we reselect it.
                b.setSelection(true);
                return;
            }

            // now handle selection change.
            if (b == mGpxForwardButton || b == mKmlForwardButton) {
                mGpxBackwardButton.setSelection(false);
                mGpxForwardButton.setSelection(true);
                mKmlBackwardButton.setSelection(false);
                mKmlForwardButton.setSelection(true);
                mPlayDirection = 1;

            } else {
                mGpxBackwardButton.setSelection(true);
                mGpxForwardButton.setSelection(false);
                mKmlBackwardButton.setSelection(true);
                mKmlForwardButton.setSelection(false);
                mPlayDirection = -1;
            }
        }
    };

    private final SelectionAdapter mSpeedButtonAdapter = new SelectionAdapter() {
        @Override
        public void widgetSelected(SelectionEvent e) {
            mSpeedIndex = (mSpeedIndex+1) % PLAY_SPEEDS.length;
            mSpeed = PLAY_SPEEDS[mSpeedIndex];

            mGpxSpeedButton.setText(String.format(SPEED_FORMAT, mSpeed));
            mGpxPlayControls.pack();
            mKmlSpeedButton.setText(String.format(SPEED_FORMAT, mSpeed));
            mKmlPlayControls.pack();

            if (mPlayingThread != null) {
                mPlayingThread.interrupt();
            }
        }
     };
    private Composite mKmlPlayControls;
    private Composite mGpxPlayControls;


    public EmulatorControlPanel() {
    }

    /**
     * Sent when a new device is selected. The new device can be accessed
     * with {@link #getCurrentDevice()}
     */
    @Override
    public void deviceSelected() {
        handleNewDevice(getCurrentDevice());
    }

    /**
     * Sent when a new client is selected. The new client can be accessed
     * with {@link #getCurrentClient()}
     */
    @Override
    public void clientSelected() {
        // pass
    }

    /**
     * Creates a control capable of displaying some information.  This is
     * called once, when the application is initializing, from the UI thread.
     */
    @Override
    protected Control createControl(Composite parent) {
        mParent = parent;

        final ScrolledComposite scollingParent = new ScrolledComposite(parent, SWT.V_SCROLL);
        scollingParent.setExpandVertical(true);
        scollingParent.setExpandHorizontal(true);
        scollingParent.setLayoutData(new GridData(GridData.FILL_BOTH));

        final Composite top = new Composite(scollingParent, SWT.NONE);
        scollingParent.setContent(top);
        top.setLayout(new GridLayout(1, false));

        // set the resize for the scrolling to work (why isn't that done automatically?!?)
        scollingParent.addControlListener(new ControlAdapter() {
            @Override
            public void controlResized(ControlEvent e) {
                Rectangle r = scollingParent.getClientArea();
                scollingParent.setMinSize(top.computeSize(r.width, SWT.DEFAULT));
            }
        });

        createRadioControls(top);

        createCallControls(top);

        createLocationControls(top);

        doEnable(false);

        top.layout();
        Rectangle r = scollingParent.getClientArea();
        scollingParent.setMinSize(top.computeSize(r.width, SWT.DEFAULT));

        return scollingParent;
    }

    /**
     * Create Radio (on/off/roaming, for voice/data) controls.
     * @param top
     */
    private void createRadioControls(final Composite top) {
        Group g1 = new Group(top, SWT.NONE);
        g1.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
        g1.setLayout(new GridLayout(2, false));
        g1.setText("Telephony Status");

        // the inside of the group is 2 composite so that all the column of the controls (mainly
        // combos) have the same width, while not taking the whole screen width
        Composite insideGroup = new Composite(g1, SWT.NONE);
        GridLayout gl = new GridLayout(4, false);
        gl.marginBottom = gl.marginHeight = gl.marginLeft = gl.marginRight = 0;
        insideGroup.setLayout(gl);

        mVoiceLabel = new Label(insideGroup, SWT.NONE);
        mVoiceLabel.setText("Voice:");
        mVoiceLabel.setAlignment(SWT.RIGHT);

        mVoiceMode = new Combo(insideGroup, SWT.READ_ONLY);
        mVoiceMode.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
        for (String[] mode : GSM_MODES) {
            mVoiceMode.add(mode[0]);
        }
        mVoiceMode.addSelectionListener(new SelectionAdapter() {
            // called when selection changes
            @Override
            public void widgetSelected(SelectionEvent e) {
                setVoiceMode(mVoiceMode.getSelectionIndex());
            }
        });

        mSpeedLabel = new Label(insideGroup, SWT.NONE);
        mSpeedLabel.setText("Speed:");
        mSpeedLabel.setAlignment(SWT.RIGHT);

        mNetworkSpeed = new Combo(insideGroup, SWT.READ_ONLY);
        mNetworkSpeed.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
        for (String mode : NETWORK_SPEEDS) {
            mNetworkSpeed.add(mode);
        }
        mNetworkSpeed.addSelectionListener(new SelectionAdapter() {
            // called when selection changes
            @Override
            public void widgetSelected(SelectionEvent e) {
                setNetworkSpeed(mNetworkSpeed.getSelectionIndex());
            }
        });

        mDataLabel = new Label(insideGroup, SWT.NONE);
        mDataLabel.setText("Data:");
        mDataLabel.setAlignment(SWT.RIGHT);

        mDataMode = new Combo(insideGroup, SWT.READ_ONLY);
        mDataMode.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
        for (String[] mode : GSM_MODES) {
            mDataMode.add(mode[0]);
        }
        mDataMode.addSelectionListener(new SelectionAdapter() {
            // called when selection changes
            @Override
            public void widgetSelected(SelectionEvent e) {
                setDataMode(mDataMode.getSelectionIndex());
            }
        });

        mLatencyLabel = new Label(insideGroup, SWT.NONE);
        mLatencyLabel.setText("Latency:");
        mLatencyLabel.setAlignment(SWT.RIGHT);

        mNetworkLatency = new Combo(insideGroup, SWT.READ_ONLY);
        mNetworkLatency.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
        for (String mode : NETWORK_LATENCIES) {
            mNetworkLatency.add(mode);
        }
        mNetworkLatency.addSelectionListener(new SelectionAdapter() {
            // called when selection changes
            @Override
            public void widgetSelected(SelectionEvent e) {
                setNetworkLatency(mNetworkLatency.getSelectionIndex());
            }
        });

        // now an empty label to take the rest of the width of the group
        Label l = new Label(g1, SWT.NONE);
        l.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
    }

    /**
     * Create Voice/SMS call/hang up controls
     * @param top
     */
    private void createCallControls(final Composite top) {
        GridLayout gl;
        Group g2 = new Group(top, SWT.NONE);
        g2.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
        g2.setLayout(new GridLayout(1, false));
        g2.setText("Telephony Actions");

        // horizontal composite for label + text field
        Composite phoneComp = new Composite(g2, SWT.NONE);
        phoneComp.setLayoutData(new GridData(GridData.FILL_BOTH));
        gl = new GridLayout(2, false);
        gl.marginBottom = gl.marginHeight = gl.marginLeft = gl.marginRight = 0;
        phoneComp.setLayout(gl);

        mNumberLabel = new Label(phoneComp, SWT.NONE);
        mNumberLabel.setText("Incoming number:");

        mPhoneNumber = new Text(phoneComp, SWT.BORDER | SWT.LEFT | SWT.SINGLE);
        mPhoneNumber.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
        mPhoneNumber.addModifyListener(new ModifyListener() {
            @Override
            public void modifyText(ModifyEvent e) {
                // Reenable the widgets based on the content of the text.
                // doEnable checks the validity of the phone number to enable/disable some
                // widgets.
                // Looks like we're getting a callback at creation time, so we can't
                // suppose that we are enabled when the text is modified...
                doEnable(mEmulatorConsole != null);
            }
        });

        mVoiceButton = new Button(phoneComp, SWT.RADIO);
        GridData gd = new GridData();
        gd.horizontalSpan = 2;
        mVoiceButton.setText("Voice");
        mVoiceButton.setLayoutData(gd);
        mVoiceButton.setEnabled(false);
        mVoiceButton.setSelection(true);
        mVoiceButton.addSelectionListener(new SelectionAdapter() {
            // called when selection changes
            @Override
            public void widgetSelected(SelectionEvent e) {
                doEnable(true);

                if (mVoiceButton.getSelection()) {
                    mCallButton.setText("Call");
                } else {
                    mCallButton.setText("Send");
                }
            }
        });

        mSmsButton = new Button(phoneComp, SWT.RADIO);
        mSmsButton.setText("SMS");
        gd = new GridData();
        gd.horizontalSpan = 2;
        mSmsButton.setLayoutData(gd);
        mSmsButton.setEnabled(false);
        // Since there are only 2 radio buttons, we can put a listener on only one (they
        // are both called on select and unselect event.

        mMessageLabel = new Label(phoneComp, SWT.NONE);
        gd = new GridData();
        gd.verticalAlignment = SWT.TOP;
        mMessageLabel.setLayoutData(gd);
        mMessageLabel.setText("Message:");
        mMessageLabel.setEnabled(false);

        mSmsMessage = new Text(phoneComp, SWT.BORDER | SWT.LEFT | SWT.MULTI | SWT.WRAP | SWT.V_SCROLL);
        mSmsMessage.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
        gd.heightHint = 70;
        mSmsMessage.setEnabled(false);

        // composite to put the 2 buttons horizontally
        Composite g2ButtonComp = new Composite(g2, SWT.NONE);
        g2ButtonComp.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
        gl = new GridLayout(2, false);
        gl.marginWidth = gl.marginHeight = 0;
        g2ButtonComp.setLayout(gl);

        // now a button below the phone number
        mCallButton = new Button(g2ButtonComp, SWT.PUSH);
        mCallButton.setText("Call");
        mCallButton.setEnabled(false);
        mCallButton.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                if (mEmulatorConsole != null) {
                    if (mVoiceButton.getSelection()) {
                        processCommandResult(mEmulatorConsole.call(mPhoneNumber.getText().trim()));
                    } else {
                        // we need to encode the message. We need to replace the carriage return
                        // character by the 2 character string \n.
                        // Because of this the \ character needs to be escaped as well.
                        // ReplaceAll() expects regexp so \ char are escaped twice.
                        String message = mSmsMessage.getText();
                        message = message.replaceAll("\\\\", //$NON-NLS-1$
                                "\\\\\\\\"); //$NON-NLS-1$

                        // While the normal line delimiter is returned by Text.getLineDelimiter()
                        // it seems copy pasting text coming from somewhere else could have another
                        // delimited. For this reason, we'll replace is several steps

                        // replace the dual CR-LF
                        message = message.replaceAll("\r\n", "\\\\n"); //$NON-NLS-1$ //$NON-NLS-2$

                        // replace remaining stand alone \n
                        message = message.replaceAll("\n", "\\\\n"); //$NON-NLS-1$ //$NON-NLS-2$

                        // replace remaining stand alone \r
                        message = message.replaceAll("\r", "\\\\n"); //$NON-NLS-1$ //$NON-NLS-2$

                        processCommandResult(mEmulatorConsole.sendSms(mPhoneNumber.getText().trim(),
                                message));
                    }
                }
            }
        });

        mCancelButton = new Button(g2ButtonComp, SWT.PUSH);
        mCancelButton.setText("Hang Up");
        mCancelButton.setEnabled(false);
        mCancelButton.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                if (mEmulatorConsole != null) {
                    if (mVoiceButton.getSelection()) {
                        processCommandResult(mEmulatorConsole.cancelCall(
                                mPhoneNumber.getText().trim()));
                    }
                }
            }
        });
    }

    /**
     * Create Location controls.
     * @param top
     */
    private void createLocationControls(final Composite top) {
        Label l = new Label(top, SWT.NONE);
        l.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
        l.setText("Location Controls");

        mLocationFolders = new TabFolder(top, SWT.NONE);
        mLocationFolders.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));

        Composite manualLocationComp = new Composite(mLocationFolders, SWT.NONE);
        TabItem item = new TabItem(mLocationFolders, SWT.NONE);
        item.setText("Manual");
        item.setControl(manualLocationComp);

        createManualLocationControl(manualLocationComp);

        ImageLoader loader = ImageLoader.getDdmUiLibLoader();
        mPlayImage = loader.loadImage("play.png", mParent.getDisplay()); //$NON-NLS-1$
        mPauseImage = loader.loadImage("pause.png", mParent.getDisplay()); //$NON-NLS-1$

        Composite gpxLocationComp = new Composite(mLocationFolders, SWT.NONE);
        item = new TabItem(mLocationFolders, SWT.NONE);
        item.setText("GPX");
        item.setControl(gpxLocationComp);

        createGpxLocationControl(gpxLocationComp);

        Composite kmlLocationComp = new Composite(mLocationFolders, SWT.NONE);
        kmlLocationComp.setLayout(new FillLayout());
        item = new TabItem(mLocationFolders, SWT.NONE);
        item.setText("KML");
        item.setControl(kmlLocationComp);

        createKmlLocationControl(kmlLocationComp);
    }

    private void createManualLocationControl(Composite manualLocationComp) {
        final StackLayout sl;
        GridLayout gl;
        Label label;

        manualLocationComp.setLayout(new GridLayout(1, false));
        mDecimalButton = new Button(manualLocationComp, SWT.RADIO);
        mDecimalButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
        mDecimalButton.setText("Decimal");
        mSexagesimalButton = new Button(manualLocationComp, SWT.RADIO);
        mSexagesimalButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
        mSexagesimalButton.setText("Sexagesimal");

        // composite to hold and switching between the 2 modes.
        final Composite content = new Composite(manualLocationComp, SWT.NONE);
        content.setLayout(sl = new StackLayout());

        // decimal display
        final Composite decimalContent = new Composite(content, SWT.NONE);
        decimalContent.setLayout(gl = new GridLayout(2, false));
        gl.marginHeight = gl.marginWidth = 0;

        mLongitudeControls = new CoordinateControls();
        mLatitudeControls = new CoordinateControls();

        label = new Label(decimalContent, SWT.NONE);
        label.setText("Longitude");

        mLongitudeControls.createDecimalText(decimalContent);

        label = new Label(decimalContent, SWT.NONE);
        label.setText("Latitude");

        mLatitudeControls.createDecimalText(decimalContent);

        // sexagesimal content
        final Composite sexagesimalContent = new Composite(content, SWT.NONE);
        sexagesimalContent.setLayout(gl = new GridLayout(7, false));
        gl.marginHeight = gl.marginWidth = 0;

        label = new Label(sexagesimalContent, SWT.NONE);
        label.setText("Longitude");

        mLongitudeControls.createSexagesimalDegreeText(sexagesimalContent);

        label = new Label(sexagesimalContent, SWT.NONE);
        label.setText("\u00B0"); // degree character

        mLongitudeControls.createSexagesimalMinuteText(sexagesimalContent);

        label = new Label(sexagesimalContent, SWT.NONE);
        label.setText("'");

        mLongitudeControls.createSexagesimalSecondText(sexagesimalContent);

        label = new Label(sexagesimalContent, SWT.NONE);
        label.setText("\"");

        label = new Label(sexagesimalContent, SWT.NONE);
        label.setText("Latitude");

        mLatitudeControls.createSexagesimalDegreeText(sexagesimalContent);

        label = new Label(sexagesimalContent, SWT.NONE);
        label.setText("\u00B0");

        mLatitudeControls.createSexagesimalMinuteText(sexagesimalContent);

        label = new Label(sexagesimalContent, SWT.NONE);
        label.setText("'");

        mLatitudeControls.createSexagesimalSecondText(sexagesimalContent);

        label = new Label(sexagesimalContent, SWT.NONE);
        label.setText("\"");

        // set the default display to decimal
        sl.topControl = decimalContent;
        mDecimalButton.setSelection(true);

        mDecimalButton.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                if (mDecimalButton.getSelection()) {
                    sl.topControl = decimalContent;
                } else {
                    sl.topControl = sexagesimalContent;
                }
                content.layout();
            }
        });

        Button sendButton = new Button(manualLocationComp, SWT.PUSH);
        sendButton.setText("Send");
        sendButton.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                if (mEmulatorConsole != null) {
                    processCommandResult(mEmulatorConsole.sendLocation(
                            mLongitudeControls.getValue(), mLatitudeControls.getValue(), 0));
                }
            }
        });

        mLongitudeControls.setValue(DEFAULT_LONGITUDE);
        mLatitudeControls.setValue(DEFAULT_LATITUDE);
    }

    private void createGpxLocationControl(Composite gpxLocationComp) {
        GridData gd;

        IPreferenceStore store = DdmUiPreferences.getStore();

        gpxLocationComp.setLayout(new GridLayout(1, false));

        mGpxUploadButton = new Button(gpxLocationComp, SWT.PUSH);
        mGpxUploadButton.setText("Load GPX...");

        // Table for way point
        mGpxWayPointTable = new Table(gpxLocationComp,
                SWT.V_SCROLL | SWT.H_SCROLL | SWT.FULL_SELECTION);
        mGpxWayPointTable.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
        gd.heightHint = 100;
        mGpxWayPointTable.setHeaderVisible(true);
        mGpxWayPointTable.setLinesVisible(true);

        TableHelper.createTableColumn(mGpxWayPointTable, "Name", SWT.LEFT,
                "Some Name",
                PREFS_WAYPOINT_COL_NAME, store);
        TableHelper.createTableColumn(mGpxWayPointTable, "Longitude", SWT.LEFT,
                "-199.999999",
                PREFS_WAYPOINT_COL_LONGITUDE, store);
        TableHelper.createTableColumn(mGpxWayPointTable, "Latitude", SWT.LEFT,
                "-199.999999",
                PREFS_WAYPOINT_COL_LATITUDE, store);
        TableHelper.createTableColumn(mGpxWayPointTable, "Elevation", SWT.LEFT,
                "99999.9",
                PREFS_WAYPOINT_COL_ELEVATION, store);
        TableHelper.createTableColumn(mGpxWayPointTable, "Description", SWT.LEFT,
                "Some Description",
                PREFS_WAYPOINT_COL_DESCRIPTION, store);

        final TableViewer gpxWayPointViewer = new TableViewer(mGpxWayPointTable);
        gpxWayPointViewer.setContentProvider(new WayPointContentProvider());
        gpxWayPointViewer.setLabelProvider(new WayPointLabelProvider());

        gpxWayPointViewer.addSelectionChangedListener(new ISelectionChangedListener() {
            @Override
            public void selectionChanged(SelectionChangedEvent event) {
                ISelection selection = event.getSelection();
                if (selection instanceof IStructuredSelection) {
                    IStructuredSelection structuredSelection = (IStructuredSelection)selection;
                    Object selectedObject = structuredSelection.getFirstElement();
                    if (selectedObject instanceof WayPoint) {
                        WayPoint wayPoint = (WayPoint)selectedObject;

                        if (mEmulatorConsole != null && mPlayingTrack == false) {
                            processCommandResult(mEmulatorConsole.sendLocation(
                                    wayPoint.getLongitude(), wayPoint.getLatitude(),
                                    wayPoint.getElevation()));
                        }
                    }
                }
            }
        });

        // table for tracks.
        mGpxTrackTable = new Table(gpxLocationComp,
                SWT.V_SCROLL | SWT.H_SCROLL | SWT.FULL_SELECTION);
        mGpxTrackTable.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
        gd.heightHint = 100;
        mGpxTrackTable.setHeaderVisible(true);
        mGpxTrackTable.setLinesVisible(true);

        TableHelper.createTableColumn(mGpxTrackTable, "Name", SWT.LEFT,
                "Some very long name",
                PREFS_TRACK_COL_NAME, store);
        TableHelper.createTableColumn(mGpxTrackTable, "Point Count", SWT.RIGHT,
                "9999",
                PREFS_TRACK_COL_COUNT, store);
        TableHelper.createTableColumn(mGpxTrackTable, "First Point Time", SWT.LEFT,
                "999-99-99T99:99:99Z",
                PREFS_TRACK_COL_FIRST, store);
        TableHelper.createTableColumn(mGpxTrackTable, "Last Point Time", SWT.LEFT,
                "999-99-99T99:99:99Z",
                PREFS_TRACK_COL_LAST, store);
        TableHelper.createTableColumn(mGpxTrackTable, "Comment", SWT.LEFT,
                "-199.999999",
                PREFS_TRACK_COL_COMMENT, store);

        final TableViewer gpxTrackViewer = new TableViewer(mGpxTrackTable);
        gpxTrackViewer.setContentProvider(new TrackContentProvider());
        gpxTrackViewer.setLabelProvider(new TrackLabelProvider());

        gpxTrackViewer.addSelectionChangedListener(new ISelectionChangedListener() {
            @Override
            public void selectionChanged(SelectionChangedEvent event) {
                ISelection selection = event.getSelection();
                if (selection instanceof IStructuredSelection) {
                    IStructuredSelection structuredSelection = (IStructuredSelection)selection;
                    Object selectedObject = structuredSelection.getFirstElement();
                    if (selectedObject instanceof Track) {
                        Track track = (Track)selectedObject;

                        if (mEmulatorConsole != null && mPlayingTrack == false) {
                            TrackPoint[] points = track.getPoints();
                            processCommandResult(mEmulatorConsole.sendLocation(
                                    points[0].getLongitude(), points[0].getLatitude(),
                                    points[0].getElevation()));
                        }

                        mPlayGpxButton.setEnabled(true);
                        mGpxBackwardButton.setEnabled(true);
                        mGpxForwardButton.setEnabled(true);
                        mGpxSpeedButton.setEnabled(true);

                        return;
                    }
                }

                mPlayGpxButton.setEnabled(false);
                mGpxBackwardButton.setEnabled(false);
                mGpxForwardButton.setEnabled(false);
                mGpxSpeedButton.setEnabled(false);
            }
        });

        mGpxUploadButton.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                FileDialog fileDialog = new FileDialog(mParent.getShell(), SWT.OPEN);

                fileDialog.setText("Load GPX File");
                fileDialog.setFilterExtensions(new String[] { "*.gpx" } );

                String fileName = fileDialog.open();
                if (fileName != null) {
                    GpxParser parser = new GpxParser(fileName);
                    if (parser.parse()) {
                        gpxWayPointViewer.setInput(parser.getWayPoints());
                        gpxTrackViewer.setInput(parser.getTracks());
                    }
                }
            }
        });

        mGpxPlayControls = new Composite(gpxLocationComp, SWT.NONE);
        GridLayout gl;
        mGpxPlayControls.setLayout(gl = new GridLayout(5, false));
        gl.marginHeight = gl.marginWidth = 0;
        mGpxPlayControls.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));

        mPlayGpxButton = new Button(mGpxPlayControls, SWT.PUSH | SWT.FLAT);
        mPlayGpxButton.setImage(mPlayImage);
        mPlayGpxButton.addSelectionListener(new SelectionAdapter() {
           @Override
            public void widgetSelected(SelectionEvent e) {
               if (mPlayingTrack == false) {
                   ISelection selection = gpxTrackViewer.getSelection();
                   if (selection.isEmpty() == false && selection instanceof IStructuredSelection) {
                       IStructuredSelection structuredSelection = (IStructuredSelection)selection;
                       Object selectedObject = structuredSelection.getFirstElement();
                       if (selectedObject instanceof Track) {
                           Track track = (Track)selectedObject;
                           playTrack(track);
                       }
                   }
               } else {
                   // if we're playing, then we pause
                   mPlayingTrack = false;
                   if (mPlayingThread != null) {
                       mPlayingThread.interrupt();
                   }
               }
            }
        });

        Label separator = new Label(mGpxPlayControls, SWT.SEPARATOR | SWT.VERTICAL);
        separator.setLayoutData(gd = new GridData(
                GridData.VERTICAL_ALIGN_FILL | GridData.GRAB_VERTICAL));
        gd.heightHint = 0;

        ImageLoader loader = ImageLoader.getDdmUiLibLoader();
        mGpxBackwardButton = new Button(mGpxPlayControls, SWT.TOGGLE | SWT.FLAT);
        mGpxBackwardButton.setImage(loader.loadImage("backward.png", mParent.getDisplay())); //$NON-NLS-1$
        mGpxBackwardButton.setSelection(false);
        mGpxBackwardButton.addSelectionListener(mDirectionButtonAdapter);
        mGpxForwardButton = new Button(mGpxPlayControls, SWT.TOGGLE | SWT.FLAT);
        mGpxForwardButton.setImage(loader.loadImage("forward.png", mParent.getDisplay())); //$NON-NLS-1$
        mGpxForwardButton.setSelection(true);
        mGpxForwardButton.addSelectionListener(mDirectionButtonAdapter);

        mGpxSpeedButton = new Button(mGpxPlayControls, SWT.PUSH | SWT.FLAT);

        mSpeedIndex = 0;
        mSpeed = PLAY_SPEEDS[mSpeedIndex];

        mGpxSpeedButton.setText(String.format(SPEED_FORMAT, mSpeed));
        mGpxSpeedButton.addSelectionListener(mSpeedButtonAdapter);

        mPlayGpxButton.setEnabled(false);
        mGpxBackwardButton.setEnabled(false);
        mGpxForwardButton.setEnabled(false);
        mGpxSpeedButton.setEnabled(false);

    }

    private void createKmlLocationControl(Composite kmlLocationComp) {
        GridData gd;

        IPreferenceStore store = DdmUiPreferences.getStore();

        kmlLocationComp.setLayout(new GridLayout(1, false));

        mKmlUploadButton = new Button(kmlLocationComp, SWT.PUSH);
        mKmlUploadButton.setText("Load KML...");

        // Table for way point
        mKmlWayPointTable = new Table(kmlLocationComp,
                SWT.V_SCROLL | SWT.H_SCROLL | SWT.FULL_SELECTION);
        mKmlWayPointTable.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
        gd.heightHint = 200;
        mKmlWayPointTable.setHeaderVisible(true);
        mKmlWayPointTable.setLinesVisible(true);

        TableHelper.createTableColumn(mKmlWayPointTable, "Name", SWT.LEFT,
                "Some Name",
                PREFS_WAYPOINT_COL_NAME, store);
        TableHelper.createTableColumn(mKmlWayPointTable, "Longitude", SWT.LEFT,
                "-199.999999",
                PREFS_WAYPOINT_COL_LONGITUDE, store);
        TableHelper.createTableColumn(mKmlWayPointTable, "Latitude", SWT.LEFT,
                "-199.999999",
                PREFS_WAYPOINT_COL_LATITUDE, store);
        TableHelper.createTableColumn(mKmlWayPointTable, "Elevation", SWT.LEFT,
                "99999.9",
                PREFS_WAYPOINT_COL_ELEVATION, store);
        TableHelper.createTableColumn(mKmlWayPointTable, "Description", SWT.LEFT,
                "Some Description",
                PREFS_WAYPOINT_COL_DESCRIPTION, store);

        final TableViewer kmlWayPointViewer = new TableViewer(mKmlWayPointTable);
        kmlWayPointViewer.setContentProvider(new WayPointContentProvider());
        kmlWayPointViewer.setLabelProvider(new WayPointLabelProvider());

        mKmlUploadButton.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                FileDialog fileDialog = new FileDialog(mParent.getShell(), SWT.OPEN);

                fileDialog.setText("Load KML File");
                fileDialog.setFilterExtensions(new String[] { "*.kml" } );

                String fileName = fileDialog.open();
                if (fileName != null) {
                    KmlParser parser = new KmlParser(fileName);
                    if (parser.parse()) {
                        kmlWayPointViewer.setInput(parser.getWayPoints());

                        mPlayKmlButton.setEnabled(true);
                        mKmlBackwardButton.setEnabled(true);
                        mKmlForwardButton.setEnabled(true);
                        mKmlSpeedButton.setEnabled(true);
                    }
                }
            }
        });

        kmlWayPointViewer.addSelectionChangedListener(new ISelectionChangedListener() {
            @Override
            public void selectionChanged(SelectionChangedEvent event) {
                ISelection selection = event.getSelection();
                if (selection instanceof IStructuredSelection) {
                    IStructuredSelection structuredSelection = (IStructuredSelection)selection;
                    Object selectedObject = structuredSelection.getFirstElement();
                    if (selectedObject instanceof WayPoint) {
                        WayPoint wayPoint = (WayPoint)selectedObject;

                        if (mEmulatorConsole != null && mPlayingTrack == false) {
                            processCommandResult(mEmulatorConsole.sendLocation(
                                    wayPoint.getLongitude(), wayPoint.getLatitude(),
                                    wayPoint.getElevation()));
                        }
                    }
                }
            }
        });



        mKmlPlayControls = new Composite(kmlLocationComp, SWT.NONE);
        GridLayout gl;
        mKmlPlayControls.setLayout(gl = new GridLayout(5, false));
        gl.marginHeight = gl.marginWidth = 0;
        mKmlPlayControls.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));

        mPlayKmlButton = new Button(mKmlPlayControls, SWT.PUSH | SWT.FLAT);
        mPlayKmlButton.setImage(mPlayImage);
        mPlayKmlButton.addSelectionListener(new SelectionAdapter() {
           @Override
            public void widgetSelected(SelectionEvent e) {
               if (mPlayingTrack == false) {
                   Object input = kmlWayPointViewer.getInput();
                   if (input instanceof WayPoint[]) {
                       playKml((WayPoint[])input);
                   }
               } else {
                   // if we're playing, then we pause
                   mPlayingTrack = false;
                   if (mPlayingThread != null) {
                       mPlayingThread.interrupt();
                   }
               }
            }
        });

        Label separator = new Label(mKmlPlayControls, SWT.SEPARATOR | SWT.VERTICAL);
        separator.setLayoutData(gd = new GridData(
                GridData.VERTICAL_ALIGN_FILL | GridData.GRAB_VERTICAL));
        gd.heightHint = 0;

        ImageLoader loader = ImageLoader.getDdmUiLibLoader();
        mKmlBackwardButton = new Button(mKmlPlayControls, SWT.TOGGLE | SWT.FLAT);
        mKmlBackwardButton.setImage(loader.loadImage("backward.png", mParent.getDisplay())); //$NON-NLS-1$
        mKmlBackwardButton.setSelection(false);
        mKmlBackwardButton.addSelectionListener(mDirectionButtonAdapter);
        mKmlForwardButton = new Button(mKmlPlayControls, SWT.TOGGLE | SWT.FLAT);
        mKmlForwardButton.setImage(loader.loadImage("forward.png", mParent.getDisplay())); //$NON-NLS-1$
        mKmlForwardButton.setSelection(true);
        mKmlForwardButton.addSelectionListener(mDirectionButtonAdapter);

        mKmlSpeedButton = new Button(mKmlPlayControls, SWT.PUSH | SWT.FLAT);

        mSpeedIndex = 0;
        mSpeed = PLAY_SPEEDS[mSpeedIndex];

        mKmlSpeedButton.setText(String.format(SPEED_FORMAT, mSpeed));
        mKmlSpeedButton.addSelectionListener(mSpeedButtonAdapter);

        mPlayKmlButton.setEnabled(false);
        mKmlBackwardButton.setEnabled(false);
        mKmlForwardButton.setEnabled(false);
        mKmlSpeedButton.setEnabled(false);
    }

    /**
     * Sets the focus to the proper control inside the panel.
     */
    @Override
    public void setFocus() {
    }

    @Override
    protected void postCreation() {
        // pass
    }

    private synchronized void setDataMode(int selectionIndex) {
        if (mEmulatorConsole != null) {
            processCommandResult(mEmulatorConsole.setGsmDataMode(
                    GsmMode.getEnum(GSM_MODES[selectionIndex][1])));
        }
    }

    private synchronized void setVoiceMode(int selectionIndex) {
        if (mEmulatorConsole != null) {
            processCommandResult(mEmulatorConsole.setGsmVoiceMode(
                    GsmMode.getEnum(GSM_MODES[selectionIndex][1])));
        }
    }

    private synchronized void setNetworkLatency(int selectionIndex) {
        if (mEmulatorConsole != null) {
            processCommandResult(mEmulatorConsole.setNetworkLatency(selectionIndex));
        }
    }

    private synchronized void setNetworkSpeed(int selectionIndex) {
        if (mEmulatorConsole != null) {
            processCommandResult(mEmulatorConsole.setNetworkSpeed(selectionIndex));
        }
    }


    /**
     * Callback on device selection change.
     * @param device the new selected device
     */
    public void handleNewDevice(IDevice device) {
        if (mParent.isDisposed()) {
            return;
        }
        // unlink to previous console.
        synchronized (this) {
            mEmulatorConsole = null;
        }

        try {
            // get the emulator console for this device
            // First we need the device itself
            if (device != null) {
                GsmStatus gsm = null;
                NetworkStatus netstatus = null;

                synchronized (this) {
                    mEmulatorConsole = EmulatorConsole.getConsole(device);
                    if (mEmulatorConsole != null) {
                        // get the gsm status
                        gsm = mEmulatorConsole.getGsmStatus();
                        netstatus = mEmulatorConsole.getNetworkStatus();

                        if (gsm == null || netstatus == null) {
                            mEmulatorConsole = null;
                        }
                    }
                }

                if (gsm != null && netstatus != null) {
                    Display d = mParent.getDisplay();
                    if (d.isDisposed() == false) {
                        final GsmStatus f_gsm = gsm;
                        final NetworkStatus f_netstatus = netstatus;

                        d.asyncExec(new Runnable() {
                            @Override
                            public void run() {
                                if (f_gsm.voice != GsmMode.UNKNOWN) {
                                    mVoiceMode.select(getGsmComboIndex(f_gsm.voice));
                                } else {
                                    mVoiceMode.clearSelection();
                                }
                                if (f_gsm.data != GsmMode.UNKNOWN) {
                                    mDataMode.select(getGsmComboIndex(f_gsm.data));
                                } else {
                                    mDataMode.clearSelection();
                                }

                                if (f_netstatus.speed != -1) {
                                    mNetworkSpeed.select(f_netstatus.speed);
                                } else {
                                    mNetworkSpeed.clearSelection();
                                }

                                if (f_netstatus.latency != -1) {
                                    mNetworkLatency.select(f_netstatus.latency);
                                } else {
                                    mNetworkLatency.clearSelection();
                                }
                            }
                        });
                    }
                }
            }
        } finally {
            // enable/disable the ui
            boolean enable = false;
            synchronized (this) {
                enable = mEmulatorConsole != null;
            }

            enable(enable);
        }
    }

    /**
     * Enable or disable the ui. Can be called from non ui threads.
     * @param enabled
     */
    private void enable(final boolean enabled) {
        try {
            Display d = mParent.getDisplay();
            d.asyncExec(new Runnable() {
                @Override
                public void run() {
                    if (mParent.isDisposed() == false) {
                        doEnable(enabled);
                    }
                }
            });
        } catch (SWTException e) {
            // disposed. do nothing
        }
    }

    private boolean isValidPhoneNumber() {
        String number = mPhoneNumber.getText().trim();

        return number.matches(RE_PHONE_NUMBER);
    }

    /**
     * Enable or disable the ui. Cannot be called from non ui threads.
     * @param enabled
     */
    protected void doEnable(boolean enabled) {
        mVoiceLabel.setEnabled(enabled);
        mVoiceMode.setEnabled(enabled);

        mDataLabel.setEnabled(enabled);
        mDataMode.setEnabled(enabled);

        mSpeedLabel.setEnabled(enabled);
        mNetworkSpeed.setEnabled(enabled);

        mLatencyLabel.setEnabled(enabled);
        mNetworkLatency.setEnabled(enabled);

        // Calling setEnabled on a text field will trigger a modifyText event, so we don't do it
        // if we don't need to.
        if (mPhoneNumber.isEnabled() != enabled) {
            mNumberLabel.setEnabled(enabled);
            mPhoneNumber.setEnabled(enabled);
        }

        boolean valid = isValidPhoneNumber();

        mVoiceButton.setEnabled(enabled && valid);
        mSmsButton.setEnabled(enabled && valid);

        boolean smsValid = enabled && valid && mSmsButton.getSelection();

        // Calling setEnabled on a text field will trigger a modifyText event, so we don't do it
        // if we don't need to.
        if (mSmsMessage.isEnabled() != smsValid) {
            mMessageLabel.setEnabled(smsValid);
            mSmsMessage.setEnabled(smsValid);
        }
        if (enabled == false) {
            mSmsMessage.setText(""); //$NON-NLs-1$
        }

        mCallButton.setEnabled(enabled && valid);
        mCancelButton.setEnabled(enabled && valid && mVoiceButton.getSelection());

        if (enabled == false) {
            mVoiceMode.clearSelection();
            mDataMode.clearSelection();
            mNetworkSpeed.clearSelection();
            mNetworkLatency.clearSelection();
            if (mPhoneNumber.getText().length() > 0) {
                mPhoneNumber.setText(""); //$NON-NLS-1$
            }
        }

        // location controls
        mLocationFolders.setEnabled(enabled);

        mDecimalButton.setEnabled(enabled);
        mSexagesimalButton.setEnabled(enabled);
        mLongitudeControls.setEnabled(enabled);
        mLatitudeControls.setEnabled(enabled);

        mGpxUploadButton.setEnabled(enabled);
        mGpxWayPointTable.setEnabled(enabled);
        mGpxTrackTable.setEnabled(enabled);
        mKmlUploadButton.setEnabled(enabled);
        mKmlWayPointTable.setEnabled(enabled);
    }

    /**
     * Returns the index of the combo item matching a specific GsmMode.
     * @param mode
     */
    private int getGsmComboIndex(GsmMode mode) {
        for (int i = 0 ; i < GSM_MODES.length; i++) {
            String[] modes = GSM_MODES[i];
            if (mode.getTag().equals(modes[1])) {
                return i;
            }
        }
        return -1;
    }

    /**
     * Processes the result of a command sent to the console.
     * @param result the result of the command.
     */
    private boolean processCommandResult(final String result) {
        if (result != EmulatorConsole.RESULT_OK) {
            try {
                mParent.getDisplay().asyncExec(new Runnable() {
                    @Override
                    public void run() {
                        if (mParent.isDisposed() == false) {
                            MessageDialog.openError(mParent.getShell(), "Emulator Console",
                                    result);
                        }
                    }
                });
            } catch (SWTException e) {
                // we're quitting, just ignore
            }

            return false;
        }

        return true;
    }

    /**
     * @param track
     */
    private void playTrack(final Track track) {
        // no need to synchronize this check, the worst that can happen, is we start the thread
        // for nothing.
        if (mEmulatorConsole != null) {
            mPlayGpxButton.setImage(mPauseImage);
            mPlayKmlButton.setImage(mPauseImage);
            mPlayingTrack = true;

            mPlayingThread = new Thread() {
                @Override
                public void run() {
                    try {
                        TrackPoint[] trackPoints = track.getPoints();
                        int count = trackPoints.length;

                        // get the start index.
                        int start = 0;
                        if (mPlayDirection == -1) {
                            start = count - 1;
                        }

                        for (int p = start; p >= 0 && p < count; p += mPlayDirection) {
                            if (mPlayingTrack == false) {
                                return;
                            }

                            // get the current point and send its location to
                            // the emulator.
                            final TrackPoint trackPoint = trackPoints[p];

                            synchronized (EmulatorControlPanel.this) {
                                if (mEmulatorConsole == null ||
                                        processCommandResult(mEmulatorConsole.sendLocation(
                                                trackPoint.getLongitude(), trackPoint.getLatitude(),
                                                trackPoint.getElevation())) == false) {
                                    return;
                                }
                            }

                            // if this is not the final point, then get the next one and
                            // compute the delta time
                            int nextIndex = p + mPlayDirection;
                            if (nextIndex >=0 && nextIndex < count) {
                                TrackPoint nextPoint = trackPoints[nextIndex];

                                long delta = nextPoint.getTime() - trackPoint.getTime();
                                if (delta < 0) {
                                    delta = -delta;
                                }

                                long startTime = System.currentTimeMillis();

                                try {
                                    sleep(delta / mSpeed);
                                } catch (InterruptedException e) {
                                    if (mPlayingTrack == false) {
                                        return;
                                    }

                                    // we got interrupted, lets make sure we can play
                                    do {
                                        long waited = System.currentTimeMillis() - startTime;
                                        long needToWait = delta / mSpeed;
                                        if (waited < needToWait) {
                                            try {
                                                sleep(needToWait - waited);
                                            } catch (InterruptedException e1) {
                                                // we'll just loop and wait again if needed.
                                                // unless we're supposed to stop
                                                if (mPlayingTrack == false) {
                                                    return;
                                                }
                                            }
                                        } else {
                                            break;
                                        }
                                    } while (true);
                                }
                            }
                        }
                    } finally {
                        mPlayingTrack = false;
                        try {
                            mParent.getDisplay().asyncExec(new Runnable() {
                                @Override
                                public void run() {
                                    if (mPlayGpxButton.isDisposed() == false) {
                                        mPlayGpxButton.setImage(mPlayImage);
                                        mPlayKmlButton.setImage(mPlayImage);
                                    }
                                }
                            });
                        } catch (SWTException e) {
                            // we're quitting, just ignore
                        }
                    }
                }
            };

            mPlayingThread.start();
        }
    }

    private void playKml(final WayPoint[] trackPoints) {
        // no need to synchronize this check, the worst that can happen, is we start the thread
        // for nothing.
        if (mEmulatorConsole != null) {
            mPlayGpxButton.setImage(mPauseImage);
            mPlayKmlButton.setImage(mPauseImage);
            mPlayingTrack = true;

            mPlayingThread = new Thread() {
                @Override
                public void run() {
                    try {
                        int count = trackPoints.length;

                        // get the start index.
                        int start = 0;
                        if (mPlayDirection == -1) {
                            start = count - 1;
                        }

                        for (int p = start; p >= 0 && p < count; p += mPlayDirection) {
                            if (mPlayingTrack == false) {
                                return;
                            }

                            // get the current point and send its location to
                            // the emulator.
                            WayPoint trackPoint = trackPoints[p];

                            synchronized (EmulatorControlPanel.this) {
                                if (mEmulatorConsole == null ||
                                        processCommandResult(mEmulatorConsole.sendLocation(
                                                trackPoint.getLongitude(), trackPoint.getLatitude(),
                                                trackPoint.getElevation())) == false) {
                                    return;
                                }
                            }

                            // if this is not the final point, then get the next one and
                            // compute the delta time
                            int nextIndex = p + mPlayDirection;
                            if (nextIndex >=0 && nextIndex < count) {

                                long delta = 1000; // 1 second
                                if (delta < 0) {
                                    delta = -delta;
                                }

                                long startTime = System.currentTimeMillis();

                                try {
                                    sleep(delta / mSpeed);
                                } catch (InterruptedException e) {
                                    if (mPlayingTrack == false) {
                                        return;
                                    }

                                    // we got interrupted, lets make sure we can play
                                    do {
                                        long waited = System.currentTimeMillis() - startTime;
                                        long needToWait = delta / mSpeed;
                                        if (waited < needToWait) {
                                            try {
                                                sleep(needToWait - waited);
                                            } catch (InterruptedException e1) {
                                                // we'll just loop and wait again if needed.
                                                // unless we're supposed to stop
                                                if (mPlayingTrack == false) {
                                                    return;
                                                }
                                            }
                                        } else {
                                            break;
                                        }
                                    } while (true);
                                }
                            }
                        }
                    } finally {
                        mPlayingTrack = false;
                        try {
                            mParent.getDisplay().asyncExec(new Runnable() {
                                @Override
                                public void run() {
                                    if (mPlayGpxButton.isDisposed() == false) {
                                        mPlayGpxButton.setImage(mPlayImage);
                                        mPlayKmlButton.setImage(mPlayImage);
                                    }
                                }
                            });
                        } catch (SWTException e) {
                            // we're quitting, just ignore
                        }
                    }
                }
            };

            mPlayingThread.start();
        }
    }
}
