org.noroomattheinn.visibletesla.MainController.java Source code

Java tutorial

Introduction

Here is the source code for org.noroomattheinn.visibletesla.MainController.java

Source

/*
 * MainController.java - Copyright(c) 2013 Joe Pasqua
 * Provided under the MIT License. See the LICENSE file for details.
 * Created: Jul 22, 2013
 */

package org.noroomattheinn.visibletesla;

import com.google.common.collect.Range;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.NavigableMap;
import java.util.concurrent.Callable;
import java.util.logging.Level;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import javafx.scene.layout.Pane;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import org.apache.commons.lang3.SystemUtils;
import org.noroomattheinn.tesla.ChargeState;
import org.noroomattheinn.tesla.GUIState;
import org.noroomattheinn.tesla.Result;
import org.noroomattheinn.tesla.Vehicle;
import org.noroomattheinn.tesla.VehicleState;
import org.noroomattheinn.timeseries.Row;
import org.noroomattheinn.utils.ThreadManager;
import org.noroomattheinn.utils.TrackedObject;
import org.noroomattheinn.utils.Utils;
import org.noroomattheinn.visibletesla.data.RestCycle;
import org.noroomattheinn.visibletesla.data.VTData;
import org.noroomattheinn.visibletesla.dialogs.*;
import org.noroomattheinn.visibletesla.vehicle.VTVehicle;

import static org.noroomattheinn.tesla.Tesla.logger;
import static org.noroomattheinn.utils.Utils.timeSince;

/**
 * This is the main application code for VisibleTesla. It does not contain
 * the main function. Main is in VisibleTesla.java which is mostly just a shell.
 * This controller is associated with the Tab panel in which all of the 
 * individual tabs live.
 * 
 * @author Joe Pasqua <joe at NoRoomAtTheInn dot org>
 */
public class MainController extends BaseController {

    /*------------------------------------------------------------------------------
     *
     * Constants and Enums
     * 
     *----------------------------------------------------------------------------*/

    private static final String DocumentationURL = "http://visibletesla.com/Doc_v2/pages/GettingStarted.html";
    private static final String ReleaseNotesURL = "http://visibletesla.com/Doc_v2/ReleaseNotes.html";

    /*------------------------------------------------------------------------------
     *
     * Internal State
     * 
     *----------------------------------------------------------------------------*/

    private final BooleanProperty forceWakeup = new SimpleBooleanProperty(false);

    /*------------------------------------------------------------------------------
     *
     * UI Elements
     * 
     *----------------------------------------------------------------------------*/

    // The top level AnchorPane and the TabPane that sits inside it
    @FXML
    private TabPane tabPane;

    // The individual tabs that comprise the overall UI
    @FXML
    private Tab notifierTab;
    @FXML
    private Tab prefsTab;
    @FXML
    private Tab schedulerTab;
    @FXML
    private Tab graphTab;
    @FXML
    private Tab chargeTab;
    @FXML
    private Tab hvacTab;
    @FXML
    private Tab locationTab;
    @FXML
    private Tab loginTab;
    @FXML
    private Tab overviewTab;
    @FXML
    private Tab tripsTab;
    @FXML
    private Pane wakePane;

    private List<Tab> tabs;

    @FXML
    private MenuItem exportStatsMenuItem, exportLocMenuItem, exportAllMenuItem, exportChargeMenuItem,
            exportRestMenuItem;
    @FXML
    private MenuItem vampireLossMenuItem;
    @FXML
    private MenuItem remoteStartMenuItem;

    // The menu items that are handled in this controller directly
    @FXML
    private RadioMenuItem allowSleepMenuItem;
    @FXML
    private RadioMenuItem stayAwakeMenuItem;

    /*==============================================================================
     * -------                                                               -------
     * -------              Public Interface To This Class                   ------- 
     * -------                                                               -------
     *============================================================================*/

    /**
     * Called by the main application to allow us to store away the fxApp context
     * and perform any other fxApp startup tasks. In particular, we (1) distribute
     * fxApp context to all of the controllers, and (2) we set a listener for login
     * completion and try and automatic login.
     */
    void start(App theApp, VTVehicle v, VTData data, Prefs prefs) {
        this.app = theApp;
        this.vtVehicle = v; // This is defined in BaseController
        this.vtData = data; // This is defined in BaseController
        this.prefs = prefs; // This is defined in BaseController

        logAppInfo();
        addSystemSpecificHandlers(app.stage);

        refreshTitle();
        app.stage.getIcons().add(new Image(getClass().getClassLoader()
                .getResourceAsStream("org/noroomattheinn/TeslaResources/Icon-72@2x.png")));

        tabPane.getSelectionModel().selectedItemProperty().addListener(new ChangeListener<Tab>() {
            @Override
            public void changed(ObservableValue<? extends Tab> ov, Tab t, Tab t1) {
                BaseController c = controllerFromTab(t1);
                if (c != null) {
                    c.activate();
                }
            }
        });

        tabs = Arrays.asList(prefsTab, loginTab, schedulerTab, graphTab, chargeTab, hvacTab, locationTab,
                overviewTab, tripsTab, notifierTab);
        for (Tab t : tabs) {
            controllerFromTab(t).setAppContext(theApp, v, data, prefs);
        }

        // Handle font scaling
        int fontScale = prefs.fontScale.get();
        if (fontScale != 100) {
            for (Tab t : tabs) {
                Node n = t.getContent();
                n.setStyle(String.format("-fx-font-size: %d%%;", fontScale));
            }
        }

        showDisclaimer();

        // Watch for changes to the inactivity mode and state in order to update the UI
        App.addTracker(app.api.mode, new Runnable() {
            @Override
            public void run() {
                setAppModeMenu();
            }
        });
        App.addTracker(app.api.state, new Runnable() {
            @Override
            public void run() {
                refreshTitle();
            }
        });

        // Kick off the login process
        LoginController lc = Utils.cast(controllerFromTab(loginTab));
        App.addTracker(lc.loggedIn, new LoginStateChange(lc.loggedIn, false));
        lc.activate();
    }

    /*------------------------------------------------------------------------------
     *
     * Methods overridden from BaseController. We implement BaseController so that
     * we can perform issueCommand operations.
     * 
     *----------------------------------------------------------------------------*/

    @Override
    protected void fxInitialize() {
    }

    @Override
    protected void initializeState() {
    }

    @Override
    protected void activateTab() {
    }

    @Override
    protected void refresh() {
    }

    /*------------------------------------------------------------------------------
     *
     * Dealing with a Login Event
     * 
     *----------------------------------------------------------------------------*/

    private void fetchInitialCarState() {
        issueCommand(new Callable<Result>() {
            @Override
            public Result call() {
                Result r = cacheBasics();
                if (!r.success) {
                    if (r.explanation.equals("mobile_access_disabled"))
                        exitWithMobileAccessError();
                    else
                        exitWithCachingError();
                    return Result.Failed;
                }
                Platform.runLater(finishAppStartup);
                return Result.Succeeded;
            }
        }, "Cache Basics");
    }

    private class LoginStateChange implements Runnable {
        private final TrackedObject<Boolean> loggedIn;
        private final boolean assumeAwake;

        LoginStateChange(TrackedObject<Boolean> loggedIn, boolean assumeAwake) {
            this.loggedIn = loggedIn;
            this.assumeAwake = assumeAwake;
        }

        @Override
        public void run() {
            if (!loggedIn.get()) {
                vtVehicle.setVehicle(null);
                setTabsEnabled(false);
                return;
            }

            if (assumeAwake) {
                wakePane.setVisible(false);
            } else {
                Vehicle v = SelectVehicleDialog.select(app.stage, app.tesla.getVehicles());
                vtVehicle.setVehicle(v);
                try {
                    upgradeDataStoreIfNeeded(v);
                    vtData.setVehicle(v);
                    vtData.setWakeEarly(new WakeEarlyPredicate());
                    vtData.setPassiveCollection(new PassiveCollectionPredicate());
                    vtData.setCollectNow(new CollectNowPredicate());
                } catch (IOException e) {
                    logger.severe("Unable to establish VTData: " + e.getMessage());
                    Dialogs.showErrorDialog(app.stage, "VisibleTesla has encountered a severe error\n"
                            + "while trying to access its data files. Another\n"
                            + "copy of VisibleTesla may already be writing to them\n"
                            + "or they may be missing.\n\n" + "VisibleTesla will close when you close this window.",
                            "Problem accessing data files", "Problem launching application");
                    Platform.exit();
                }
                if (!app.lock(v.getVIN())) {
                    showLockError();
                    Platform.exit();
                }
                logger.info("Vehicle Info: " + vtVehicle.getVehicle().getUnderlyingValues());

                if (vtVehicle.getVehicle().status().equals("asleep")) {
                    if (letItSleep()) {
                        logger.info("Allowing vehicle to remain in sleep mode");
                        wakePane.setVisible(true);
                        vtVehicle.waitForWakeup(new LoginStateChange(loggedIn, true), forceWakeup);
                        return;
                    } else {
                        logger.log(Level.INFO, "Waking up your vehicle");
                    }
                }
            }

            conditionalCheckVersion();
            app.restoreMode();
            fetchInitialCarState();
        }

        private void upgradeDataStoreIfNeeded(Vehicle v) {
            if (vtData.upgradeRequired(v)) {
                Dialogs.showInformationDialog(app.stage,
                        "Your data files must be upgraded\nPress OK to begin the process.", "Data Upgrade Process",
                        "Data File Upgrade");
                vtData.doUpgrade(v);
                Dialogs.showInformationDialog(app.stage,
                        "Your data files have been upgraded\nPress OK to continue.", "Data Upgrade Process",
                        "Process Complete");
            }
        }
    }

    private void conditionalCheckVersion() {
        String key = vinKey("LastVersionCheck");
        long lastVersionCheck = prefs.storage().getLong(key, 0);
        long now = System.currentTimeMillis();
        if (now - lastVersionCheck > (7 * 24 * 60 * 60 * 1000)) {
            VersionUpdater.checkForNewerVersion(App.productVersion(), app.stage, app.getHostServices(),
                    prefs.offerExperimental.get());
            prefs.storage().putLong(key, now);
        }
    }

    private Runnable finishAppStartup = new Runnable() {
        @Override
        public void run() {
            boolean remoteStartEnabled = vtVehicle.getVehicle().remoteStartEnabled();
            remoteStartMenuItem.setDisable(!remoteStartEnabled);

            app.watchForUserActivity(Arrays.asList(overviewTab, hvacTab, locationTab, chargeTab));

            // TO DO: Isn't the following line redundant?
            vtVehicle.setVehicle(vtVehicle.getVehicle());

            refreshTitle();

            // Start the Scheduler and the Notifier
            controllerFromTab(schedulerTab).activate();
            controllerFromTab(notifierTab).activate();

            setTabsEnabled(true);
            jumpToTab(overviewTab);
        }
    };

    /*------------------------------------------------------------------------------
     *
     * Private Utility Methods waking the car and initiating contact
     * 
     *----------------------------------------------------------------------------*/

    /**
     * Make contact with the car for the first time. It may need to be woken up
     * in the process. Since we need to do some command as part of this process,
     * we grab the GUIState and store it away.
     * @return 
     */
    private Result establishContact() {
        Vehicle v = vtVehicle.getVehicle();

        long MaxWaitTime = 70 * 1000;
        long now = System.currentTimeMillis();
        while (timeSince(now) < MaxWaitTime) {
            if (ThreadManager.get().shuttingDown()) {
                return new Result(false, "shutting down");
            }
            GUIState gs = v.queryGUI();
            if (gs.valid) {
                if (gs.rawState.optString("reason").equals("mobile_access_disabled")) {
                    return new Result(false, "mobile_access_disabled");
                }
                vtVehicle.noteUpdatedState(gs);
                return Result.Succeeded;
            } else {
                String error = gs.rawState.optString("error");
                if (error.equals("vehicle unavailable"))
                    v.wakeUp();
                ThreadManager.get().sleep(10 * 1000);
            }
        }
        return Result.Failed;
    }

    private Result cacheBasics() {
        final int MaxTriesToStart = 10;
        Result madeContact = establishContact();
        if (!madeContact.success) {
            // Try getting last values from previous run of the app
            logger.warning("Unable to contact vehicle after successful login");
            GUIState gs = vtVehicle.lastSavedGS();
            VehicleState vs = vtVehicle.lastSavedVS();
            ChargeState cs = vtVehicle.lastSavedCS();
            if (gs != null && vs != null && cs != null) {
                vtVehicle.chargeState.reset(cs);
                vtVehicle.vehicleState.reset(vs);
                vtVehicle.guiState.reset(gs);
                logger.info("Able to initial state from old values");
                warnAboutNoCarAccess(madeContact.explanation.equals("mobile_access_disabled"));
                // TO DO: Force Sleep Mode!
                return Result.Succeeded;
            } else {
                logger.info("Unable to initial state from old values");
                return madeContact;
            }
        }

        // As part of establishing contact with the car we cached the GUIState
        Vehicle v = vtVehicle.getVehicle();
        VehicleState vs = v.queryVehicle();
        ChargeState cs = v.queryCharge();

        int tries = 0;
        while (!(vs.valid && cs.valid)) {
            if (tries++ > MaxTriesToStart) {
                return Result.Failed;
            }
            ThreadManager.get().sleep(5 * 1000);
            if (ThreadManager.get().shuttingDown())
                return Result.Failed;
            if (!vs.valid)
                vs = v.queryVehicle();
            if (!cs.valid)
                cs = v.queryCharge();
        }

        vtVehicle.noteUpdatedState(vs);
        vtVehicle.noteUpdatedState(cs);
        return Result.Succeeded;
    }

    /*------------------------------------------------------------------------------
     *
     * Private Utility Methods for Tab handling
     * 
     *----------------------------------------------------------------------------*/

    private void setTabsEnabled(boolean enabled) {
        for (Tab t : tabs) {
            t.setDisable(!enabled);
        }
        loginTab.setDisable(false); // The Login Tab is always enabled
        prefsTab.setDisable(false); // The Prefs Tab is always enabled
    }

    private void jumpToTab(final Tab tab) {
        Platform.runLater(new Runnable() {
            @Override
            public void run() {
                tabPane.getSelectionModel().select(tab);
            }
        });
    }

    /**
     * Utility method that returns the BaseController object associated with
     * a given tab. It does this by extracting the userData object which each
     * BaseController sets to itself.
     * @param   t   The tab for which we want the BaseController
     * @return      The BaseController
     */
    private BaseController controllerFromTab(Tab t) {
        Object userData = t.getContent().getUserData();
        return (userData instanceof BaseController) ? (BaseController) userData : null;
    }

    /*------------------------------------------------------------------------------
     *
     * This section implements UI Actionhandlers for the menu items
     * 
     *----------------------------------------------------------------------------*/

    // File->Close
    @FXML
    void closeHandler(ActionEvent event) {
        Platform.exit();
    }

    // File->Export * Data...
    private static final String[] statsColumns = new String[] { VTData.VoltageKey, VTData.CurrentKey,
            VTData.EstRangeKey, VTData.SOCKey, VTData.ROCKey, VTData.BatteryAmpsKey, VTData.SpeedKey,
            VTData.PowerKey, };
    private static final String[] locColumns = new String[] { VTData.LatitudeKey, VTData.LongitudeKey,
            VTData.HeadingKey, VTData.SpeedKey, VTData.OdometerKey, VTData.PowerKey };

    @FXML
    void exportHandler(ActionEvent event) {
        MenuItem mi = (MenuItem) event.getSource();
        if (mi == exportStatsMenuItem) {
            exportStats(statsColumns);
        } else if (mi == exportLocMenuItem) {
            exportStats(locColumns);
        } else if (mi == exportAllMenuItem) {
            exportStats(VTData.schema.columnNames);
        } else if (mi == exportChargeMenuItem) {
            exportCycles("Charge");
        } else if (mi == exportRestMenuItem) {
            exportCycles("Rest");
        } else if (mi == this.vampireLossMenuItem) {
            showVampireLoss();
        }
    }

    // Options->"Inactivity Mode" menu items
    @FXML
    void inactivityOptionsHandler(ActionEvent event) {
        if (event.getTarget() == allowSleepMenuItem)
            app.api.allowSleeping();
        else
            app.api.stayAwake();
    }

    // Help->Documentation
    @FXML
    private void helpHandler(ActionEvent event) {
        app.showDocument(DocumentationURL);
    }

    // Help->What's New
    @FXML
    private void whatsNewHandler(ActionEvent event) {
        app.showDocument(ReleaseNotesURL);
    }

    // Help->About
    @FXML
    private void aboutHandler(ActionEvent event) {
        Dialogs.showInformationDialog(app.stage,
                "Copyright (c) 2013, Joe Pasqua\n" + "Free for personal and non-commercial use.\n"
                        + "Based on the great API detective work of many members\n"
                        + "of teslamotorsclub.com.  All Tesla imagery derives\n"
                        + "from the official Tesla iPhone app.",
                App.productName() + " " + App.productVersion(), "About " + App.productName());
    }

    // Help->Check for Updates
    @FXML
    private void updatesHandler(ActionEvent event) {
        if (!VersionUpdater.checkForNewerVersion(App.productVersion(), app.stage, app.getHostServices(),
                prefs.offerExperimental.get())) {
            Dialogs.showInformationDialog(app.stage, "You already have the latest release.", "Update Check Results",
                    "Checking for Updates");
        }
    }

    @FXML
    private void remoteStart(ActionEvent e) {
        final String[] unp = PasswordDialog.getCredentials(app.stage, "Authenticate", "Remote Start", false);
        if (unp == null)
            return; // User cancelled
        if (unp[1] == null || unp[1].isEmpty()) {
            Dialogs.showErrorDialog(app.stage, "You must enter a password");
            return;
        }
        issuer.issueCommand(new Callable<Result>() {
            @Override
            public Result call() {
                return vtVehicle.getVehicle().remoteStart(unp[1]);
            }
        }, true, null, "Remote Start");
    }

    // Options->Action_>{Honk,Flsh,Wakeup}

    @FXML
    private void honk(ActionEvent e) {
        issuer.issueCommand(new Callable<Result>() {
            @Override
            public Result call() {
                return vtVehicle.getVehicle().honk();
            }
        }, true, null, "Honk");
    }

    @FXML
    private void flash(ActionEvent e) {
        issuer.issueCommand(new Callable<Result>() {
            @Override
            public Result call() {
                return vtVehicle.getVehicle().flashLights();
            }
        }, true, null, "Flash Lights");
    }

    @FXML
    private void wakeup(ActionEvent e) {
        issuer.issueCommand(new Callable<Result>() {
            @Override
            public Result call() {
                return vtVehicle.getVehicle().wakeUp();
            }
        }, true, null, "Wake up");
    }

    /*------------------------------------------------------------------------------
     *
     * Export Handling Methods
     * 
     *----------------------------------------------------------------------------*/

    private void exportCycles(String cycleType) {
        String initialDir = prefs.storage().get(App.LastExportDirKey, System.getProperty("user.home"));
        FileChooser fileChooser = new FileChooser();
        fileChooser.setTitle("Export " + cycleType + " Data");
        fileChooser.setInitialDirectory(new File(initialDir));

        Stage stage = app.stage;
        File file = fileChooser.showSaveDialog(stage);
        if (file != null) {
            String enclosingDirectory = file.getParent();
            if (enclosingDirectory != null)
                prefs.storage().put(App.LastExportDirKey, enclosingDirectory);
            Range<Long> exportPeriod = DateRangeDialog.getExportPeriod(stage);
            if (exportPeriod == null)
                return;
            boolean exported;
            if (cycleType.equals("Charge")) {
                exported = vtData.exportCharges(file, exportPeriod);
            } else {
                exported = vtData.exportRests(file, exportPeriod);
            }
            if (exported) {
                Dialogs.showInformationDialog(stage, "Your data has been exported", "Data Export Process",
                        "Export Complete");
            } else {
                Dialogs.showErrorDialog(stage, "Unable to save to: " + file, "Data Export Process",
                        "Export Failed");
            }
        }
    }

    private void exportStats(String[] columns) {
        String initialDir = prefs.storage().get(App.LastExportDirKey, System.getProperty("user.home"));
        FileChooser fileChooser = new FileChooser();
        fileChooser.setTitle("Export Data");
        fileChooser.setInitialDirectory(new File(initialDir));

        File file = fileChooser.showSaveDialog(app.stage);
        if (file != null) {
            String enclosingDirectory = file.getParent();
            if (enclosingDirectory != null)
                prefs.storage().put(App.LastExportDirKey, enclosingDirectory);
            Range<Long> exportPeriod = DateRangeDialog.getExportPeriod(app.stage);
            if (exportPeriod == null)
                return;
            if (vtData.export(file, exportPeriod, columns)) {
                Dialogs.showInformationDialog(app.stage, "Your data has been exported", "Data Export Process",
                        "Export Complete");
            } else {
                Dialogs.showErrorDialog(app.stage, "Unable to save to: " + file, "Data Export Process",
                        "Export Failed");
            }
        }
    }

    /*------------------------------------------------------------------------------
     *
     * Other UI Handlers and utilities
     * 
     *----------------------------------------------------------------------------*/

    private void showVampireLoss() {
        Range<Long> exportPeriod = getExportPeriod();
        if (exportPeriod != null) {
            List<RestCycle> rests = vtData.getRestCycles(exportPeriod);
            boolean useMiles = vtVehicle.unitType() == Utils.UnitType.Imperial;

            // Compute some stats and generate detail output
            long totalRestTime = 0;
            double totalLoss = 0;
            for (RestCycle r : rests) {
                totalRestTime += r.endTime - r.startTime;
                totalLoss += r.startRange - r.endRange;
            }

            VampireLossResults.show(app.stage, rests, useMiles ? "mi" : "km", totalLoss / hours(totalRestTime));
        }
    }

    private double hours(long millis) {
        return ((double) (millis)) / (60 * 60 * 1000);
    }

    private void addSystemSpecificHandlers(final Stage theStage) {
        if (SystemUtils.IS_OS_MAC) { // Add a handler for Command-H
            theStage.getScene().getAccelerators()
                    .put(new KeyCodeCombination(KeyCode.H, KeyCombination.SHORTCUT_DOWN), new Runnable() {
                        @Override
                        public void run() {
                            theStage.setIconified(true);
                        }
                    });
        }
    }

    private void refreshTitle() {
        Vehicle v = vtVehicle.getVehicle();
        String carName = (v != null) ? v.getDisplayName() : null;
        String title = App.productName() + " " + App.productVersion();
        if (carName != null)
            title = title + " for " + carName;
        if (app.api.isIdle()) {
            String time = String.format("%1$tH:%1$tM", new Date());
            title = title + " [sleeping at " + time + "]";
        }
        app.stage.setTitle(title);
    }

    private void setAppModeMenu() {
        if (app.api.allowingSleeping())
            allowSleepMenuItem.setSelected(true);
        else
            stayAwakeMenuItem.setSelected(true);
    }

    private void logAppInfo() {
        logger.info(App.productName() + ": " + App.productVersion());

        logger.info(String.format("Max memory: %4dmb", Runtime.getRuntime().maxMemory() / (1024 * 1024)));
        List<String> jvmArgs = Utils.getJVMArgs();
        logger.info("JVM Arguments");
        if (jvmArgs != null) {
            for (String arg : jvmArgs) {
                logger.info("Arg: " + arg);
            }
        }
    }

    private class WakeEarlyPredicate implements Utils.Predicate {
        private long lastEval = System.currentTimeMillis();

        @Override
        public boolean eval() {
            try {
                if (app.api.mode.lastSet() > lastEval && app.api.stayingAwake())
                    return true;
                return ThreadManager.get().shuttingDown();
            } finally {
                lastEval = System.currentTimeMillis();
            }
        }
    }

    private class CollectNowPredicate implements VTData.TimeBasedPredicate {
        private long last = Long.MAX_VALUE;

        @Override
        public void setTime(long time) {
            last = time;
        }

        @Override
        public boolean eval() {
            return (app.api.isActive() && app.api.state.lastSet() > last);
        }
    }

    private class PassiveCollectionPredicate implements Utils.Predicate {
        @Override
        public boolean eval() {
            return (app.api.isIdle() && app.api.allowingSleeping());
        }
    }

    /*------------------------------------------------------------------------------
     *
     * Display various info and warning dialogs
     * 
     *----------------------------------------------------------------------------*/

    private boolean letItSleep() {
        WakeSleepDialog wsd = WakeSleepDialog.show(app.stage);
        return wsd.letItSleep();
    }

    @FXML
    private void wakeButtonHandler(ActionEvent event) {
        forceWakeup.set(true);
    }

    private void warnAboutNoCarAccess(final boolean mobileDisabled) {
        Platform.runLater(new Runnable() {
            @Override
            public void run() {
                String message = "VisibleTesla is unable to access your car even though"
                        + "the login succeeded.\n\n";
                if (mobileDisabled)
                    message = message + "Your Tesla has not been configured to allow mobile "
                            + "access. You have to enable this on your car's touch"
                            + "screen using Controls / Settings / Vehicle.\n\n";
                message = message + "VisibleTesla will still launch, but will not"
                        + "be able to control or monitor your car until access is restored.";

                Dialogs.showErrorDialog(app.stage, message, "Mobile access is not enabled",
                        "Communication Problem");
            }
        });
    }

    private void exitWithMobileAccessError() {
        Platform.runLater(new Runnable() {
            @Override
            public void run() {
                Dialogs.showErrorDialog(app.stage,
                        "Your Tesla has not been configured to allow mobile "
                                + "access. You have to enable this on your car's touch"
                                + "screen using Controls / Settings / Vehicle."
                                + "\n\nChange that setting in your car, then relaunch VisibleTesla.",
                        "Unable to communicate with your Tesla", "Communication Problem");
                logger.log(Level.SEVERE, "Mobile access is not enabled - exiting.");
                Platform.exit();
            }
        });
    }

    private void exitWithCachingError() {
        Platform.runLater(new Runnable() {
            @Override
            public void run() {
                Dialogs.showErrorDialog(app.stage,
                        "Failed to connect to your vehicle even after a successful "
                                + "login. It may be in a deep sleep and can't be woken up.\n"
                                + "\nPlease try to wake your Tesla and then try VisibleTesla again.",
                        "Unable to communicate with your Tesla", "Communication Problem");
                logger.severe("Can't communicate with vehicle - exiting.");
                Platform.exit();
            }
        });
    }

    private void showLockError() {
        Dialogs.showErrorDialog(app.stage, "There appears to be another copy of VisibleTesla\n"
                + "running on this computer and trying to talk\n" + "to the same car. That can cause problems and\n"
                + "is not allowed\n\n" + "VisibleTesla will close when you close this window.",
                "Multiple Copies of VisibleTesla", "Problem launching application");
    }

    private void showDisclaimer() {
        boolean disclaimer = prefs.storage().getBoolean("V2_Disclaimer", false);
        if (!disclaimer) {
            Dialogs.showInformationDialog(app.stage,
                    "Use this application at your own risk. The author\n"
                            + "does not guarantee its proper functioning.\n"
                            + "It is possible that use of this application may cause\n"
                            + "unexpected damage for which nobody but you are\n"
                            + "responsible. Use of this application can change the\n"
                            + "settings on your car and may have negative\n"
                            + "consequences such as (but not limited to):\n"
                            + "unlocking the doors, opening the sun roof, or\n"
                            + "reducing the available charge in the battery.",
                    "Please Read Carefully", "Disclaimer");
            Dialogs.showInformationDialog(app.stage,
                    "Use of this application may result in Tesla Motors\n"
                            + "restricting your access via your official Tesla mobile\n"
                            + "phone application. The author takes no responsibilitity\n"
                            + "for this outcome. By running this application you\n"
                            + "acknowledge this and accept all responsibility for\n" + "any adverse outcomes.",
                    "Please Read Carefully", "Disclaimer");
            prefs.storage().putBoolean("V2_Disclaimer", true);
        }
    }

    private Range<Long> getExportPeriod() {
        NavigableMap<Long, Row> rows = vtData.getAllLoadedRows();
        long timestamp = rows.firstKey();
        Calendar start = Calendar.getInstance();
        start.setTimeInMillis(timestamp);

        timestamp = rows.lastKey();
        Calendar end = Calendar.getInstance();
        end.setTimeInMillis(timestamp);

        Range<Long> exportPeriod = DateRangeDialog.getExportPeriod(app.stage, start, end);
        return exportPeriod;
    }
}