org.noroomattheinn.visibletesla.TripController.java Source code

Java tutorial

Introduction

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

Source

/*
 * TripController.java - Copyright(c) 2013 Joe Pasqua
 * Provided under the MIT License. See the LICENSE file for details.
 * Created: Nov 27, 2013
 */
package org.noroomattheinn.visibletesla;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Dialogs;
import javafx.scene.control.ListView;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.stage.FileChooser;
import javafx.util.Callback;
import jfxtras.labs.scene.control.CalendarPicker;
import org.apache.commons.io.FileUtils;
import org.noroomattheinn.tesla.ChargeState;
import org.noroomattheinn.tesla.StreamState;
import org.noroomattheinn.timeseries.Row;
import org.noroomattheinn.utils.GeoUtils;
import org.noroomattheinn.utils.SimpleTemplate;
import org.noroomattheinn.utils.Utils;
import org.noroomattheinn.visibletesla.data.Trip;
import org.noroomattheinn.visibletesla.data.VTData;
import org.noroomattheinn.visibletesla.data.WayPoint;

import static org.noroomattheinn.tesla.Tesla.logger;

/**
 * TripController: Manage the recording, selection and display of Trips
 * 
 * @author Joe Pasqua <joe at NoRoomAtTheInn dot org>
 */
public class TripController extends BaseController {

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

    private static final String IncludeGraphKey = "TR_INCLUDE_GRAPH";
    private static final String SnapToRoadKey = "TR_SNAP";
    private static final String PathTemplateFileName = "PathTemplate.html";
    private static final long MaxTimeBetweenWayPoints = 15 * 60 * 1000;
    private static final String RangeRowName = "Range";
    private static final String OdoRowName = "Odometer";

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

    private boolean useMiles = true;

    private Map<String, List<Trip>> dateToTrips = new HashMap<>();
    private Map<String, Trip> selectedTrips = new HashMap<>();

    // Each of the following items defines a row of the property table
    private final GenericProperty rangeRow = new GenericProperty(RangeRowName, "0.0", "0.0");
    private final GenericProperty socRow = new GenericProperty("SOC (%)", "0.0", "0.0");
    private final GenericProperty odoRow = new GenericProperty(OdoRowName, "0.0", "0.0");
    private final GenericProperty powerRow = new GenericProperty("Energy (kWh)", "0.0", "0.0");
    private final ObservableList<GenericProperty> data = FXCollections.observableArrayList(rangeRow, socRow, odoRow,
            powerRow);

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

    @FXML
    private CalendarPicker calendarPicker;
    @FXML
    private Button mapItButton;
    @FXML
    private Button exportItButton;
    @FXML
    private ListView<String> availableTripsView;
    @FXML
    private CheckBox includeGraph;
    @FXML
    private CheckBox snapToRoad;

    // The Property TableView and its Columns
    @FXML
    private TableView<GenericProperty> propertyTable;
    @FXML
    private TableColumn<GenericProperty, String> propNameCol;
    @FXML
    private TableColumn<GenericProperty, String> propStartCol;
    @FXML
    private TableColumn<GenericProperty, String> propEndCol;

    /*------------------------------------------------------------------------------
     *
     * UI Action Handlers
     * 
     *----------------------------------------------------------------------------*/

    private List<Trip> getSelectedTrips() {
        ArrayList<Trip> selection = new ArrayList<>();
        for (String item : availableTripsView.getSelectionModel().getSelectedItems()) {
            Trip t = selectedTrips.get(item);
            if (t != null)
                selection.add(t);
        }
        Collections.sort(selection, new Comparator<Trip>() {
            @Override
            public int compare(Trip o1, Trip o2) {
                return Long.signum(o1.firstWayPoint().getTime() - o2.firstWayPoint().getTime());
            }
        });
        return selection;
    }

    @FXML
    void exportItHandler(ActionEvent event) {
        String initialDir = prefs.storage().get(App.LastExportDirKey, System.getProperty("user.home"));
        FileChooser fileChooser = new FileChooser();
        fileChooser.setTitle("Export Trip as KMZ");
        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);
            if (vtData.exportTripsAsKML(getSelectedTrips(), file)) {
                Dialogs.showInformationDialog(app.stage, "Your data has been exported", "Data Export Process",
                        "Export Complete");
            } else {
                Dialogs.showWarningDialog(app.stage, "There was a problem exporting your trip data to KMZ",
                        "Data Export Process", "Export Failed");
            }
        }
    }

    @FXML
    void mapItHandler(ActionEvent event) {
        List<Trip> trips = getSelectedTrips();

        String map = getMapFromTemplate(trips);
        try {
            File tempFile = File.createTempFile("VTTrip", ".html");
            FileUtils.write(tempFile, map);
            app.showDocument(tempFile.toURI().toString());
        } catch (IOException ex) {
            logger.warning("Unable to create temp file");
            // TO DO: Pop up a dialog!
        }
    }

    @FXML
    void todayHandler(ActionEvent event) {
        calendarPicker.calendars().clear();
        calendarPicker.calendars().add(new GregorianCalendar());
    }

    @FXML
    void endTripHandler(ActionEvent event) {
        endCurrentTrip();
    }

    @FXML
    void clearSelection(ActionEvent event) {
        mapItButton.setDisable(true);
        exportItButton.setDisable(true);
        calendarPicker.calendars().clear();
        availableTripsView.getItems().clear();
        selectedTrips.clear();
    }

    /*------------------------------------------------------------------------------
     *
     * Methods overridden from BaseController
     * 
     *----------------------------------------------------------------------------*/

    @Override
    protected void fxInitialize() {
        mapItButton.setDisable(true);
        exportItButton.setDisable(true);

        availableTripsView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
        availableTripsView.getSelectionModel().getSelectedItems().addListener(new ListChangeListener<String>() {
            @Override
            public void onChanged(ListChangeListener.Change<? extends String> change) {
                reflectSelection();
            }
        });

        calendarPicker.calendars().addListener(new ListChangeListener<Calendar>() {
            @Override
            public void onChanged(ListChangeListener.Change<? extends Calendar> change) {
                updateTripSelections();
            }
        });

        calendarPicker.setCalendarRangeCallback(new Callback<CalendarPicker.CalendarRange, java.lang.Void>() {
            @Override
            public Void call(CalendarPicker.CalendarRange p) {
                highlightDaysWithTrips(p.getStartCalendar());
                return null;
            }
        });

        includeGraph.selectedProperty().addListener(new ChangeListener<Boolean>() {
            @Override
            public void changed(ObservableValue<? extends Boolean> ov, Boolean t, Boolean t1) {
                prefs.storage().putBoolean(IncludeGraphKey, t1);
            }
        });

        snapToRoad.selectedProperty().addListener(new ChangeListener<Boolean>() {
            @Override
            public void changed(ObservableValue<? extends Boolean> ov, Boolean t, Boolean t1) {
                prefs.storage().putBoolean(SnapToRoadKey, t1);
            }
        });

        // Prepare the property table
        propNameCol.setCellValueFactory(new PropertyValueFactory<GenericProperty, String>("name"));
        propStartCol.setCellValueFactory(new PropertyValueFactory<GenericProperty, String>("value"));
        propEndCol.setCellValueFactory(new PropertyValueFactory<GenericProperty, String>("units"));
        propertyTable.setItems(data);

    }

    @Override
    protected void activateTab() {
        useMiles = vtVehicle.unitType() == Utils.UnitType.Imperial;
        String units = useMiles ? " (mi)" : " (km)";
        rangeRow.setName(RangeRowName + units);
        odoRow.setName(OdoRowName + units);
    }

    @Override
    protected void initializeState() {
        includeGraph.setSelected(prefs.storage().getBoolean(IncludeGraphKey, false));
        snapToRoad.setSelected(prefs.storage().getBoolean(SnapToRoadKey, false));
        readTrips();
    }

    @Override
    protected void refresh() {
    }

    /*------------------------------------------------------------------------------
     *
     * PRIVATE - Utility Methods and Classes
     * 
     *----------------------------------------------------------------------------*/

    private void reflectTripInfo() {
        List<Trip> trips = getSelectedTrips();
        if (trips == null || trips.isEmpty())
            return;
        WayPoint start = trips.get(0).firstWayPoint();
        WayPoint end = trips.get(trips.size() - 1).lastWayPoint();

        double cvt = useMiles ? 1.0 : Utils.KilometersPerMile;
        updateStartEndProps(VTData.EstRangeKey, start.getTime(), end.getTime(), rangeRow, cvt);
        updateStartEndProps(VTData.SOCKey, start.getTime(), end.getTime(), socRow, 1.0);

        updateStartEndProps(odoRow, start.getOdo(), end.getOdo(), cvt);

        double power = 0.0;
        for (Trip t : trips) {
            power += t.estimateEnergy();
        }
        updateStartEndProps(powerRow, 0.0, power, 1.0);
    }

    private void updateStartEndProps(String statType, long startTime, long endTime, GenericProperty prop,
            double conversionFactor) {
        NavigableMap<Long, Row> rows = vtData.getRangeOfLoadedRows(startTime, endTime);

        double startValue = rows.firstEntry().getValue().get(VTData.schema, statType);
        double endValue = rows.lastEntry().getValue().get(VTData.schema, statType);
        prop.setValue(String.format("%.1f", startValue * conversionFactor));
        prop.setUnits(String.format("%.1f", endValue * conversionFactor));
    }

    private void updateStartEndProps(GenericProperty prop, double startVal, double endVal,
            double conversionFactor) {
        prop.setValue(String.format("%.1f", startVal * conversionFactor));
        prop.setUnits(String.format("%.1f", endVal * conversionFactor));
    }

    private void reflectSelection() {
        reflectTripInfo();
        mapItButton.setDisable(selectedTrips.isEmpty());
        exportItButton.setDisable(selectedTrips.isEmpty());
    }

    /**
     * Must be run on the FX Application Thread
     */
    private void updateTripSelections() {
        mapItButton.setDisable(true);
        exportItButton.setDisable(true);
        selectedTrips.clear();
        availableTripsView.getItems().clear();
        if (calendarPicker.calendars().size() == 0) {
            return;
        }
        double cvt = useMiles ? 1.0 : Utils.KilometersPerMile;
        for (Calendar c : calendarPicker.calendars()) {
            String dateKey = keyFromDate(c.getTime());
            List<Trip> trips = dateToTrips.get(dateKey);
            if (trips != null) {
                for (Trip t : trips) {
                    String id = String.format("%s @ %s, %.1f %s", dateKey,
                            hourAndMinutes(t.firstWayPoint().getTime()), t.distance() * cvt,
                            useMiles ? "mi" : "km");
                    selectedTrips.put(id, t);
                    availableTripsView.getItems().add(id);
                }
            }
        }
    }

    private String keyFromDate(Date d) {
        return String.format("%1$tY-%1$tm-%1$td", d);
    }

    private String hourAndMinutes(long time) {
        return String.format("%1$tH:%1$tM", new Date(time));
    }

    private String getMapFromTemplate(List<Trip> trips) {
        SimpleTemplate template = new SimpleTemplate(getClass().getResourceAsStream(PathTemplateFileName));

        StringBuilder sb = new StringBuilder();
        sb.append("[\n");
        boolean first = true;
        for (Trip t : trips) {
            if (includeGraph.isSelected()) {
                t.addElevationData();
            }
            if (!first)
                sb.append(",\n");
            sb.append(t.asJSON(useMiles));
            first = false;
        }
        sb.append("]\n");
        Date date = new Date(trips.get(0).firstWayPoint().getTime());
        return template.fillIn("TRIPS", sb.toString(), "TITLE", "Tesla Path on " + date, "EL_UNITS",
                useMiles ? "feet" : "meters", "SP_UNITS", useMiles ? "mph" : "km/h", "INCLUDE_GRAPH",
                includeGraph.isSelected() ? "true" : "false", "SNAP", snapToRoad.isSelected() ? "true" : "false",
                "GMAP_API_KEY",
                prefs.useCustomGoogleAPIKey.get() ? prefs.googleAPIKey.get() : Prefs.GoogleMapsAPIKey);
    }

    private boolean sameMonth(Calendar month, Calendar day) {
        return (month.get(Calendar.YEAR) == day.get(Calendar.YEAR)
                && month.get(Calendar.MONTH) == day.get(Calendar.MONTH));
    }

    private void highlightDaysWithTrips(Calendar month) {
        List<Calendar> daysToHighlight = new ArrayList<>();
        for (List<Trip> trips : dateToTrips.values()) {
            Calendar day = Calendar.getInstance();
            day.setTimeInMillis(trips.get(0).firstWayPoint().getTime());
            if (sameMonth(month, day))
                daysToHighlight.add(day);
        }
        calendarPicker.highlightedCalendars().clear();
        calendarPicker.highlightedCalendars().addAll(daysToHighlight);
    }

    /*------------------------------------------------------------------------------
     *
     * PRIVATE - Methods and Classes for handling Trips and WayPoints
     * 
     *----------------------------------------------------------------------------*/

    private void readTrips() {
        Map<Long, Row> rows = vtData.getAllLoadedRows();
        for (Row r : rows.values()) {
            double lat = r.get(VTData.schema, VTData.LatitudeKey);
            double lng = r.get(VTData.schema, VTData.LongitudeKey);
            double odo = r.get(VTData.schema, VTData.OdometerKey);
            if (lat == 0.0 && lng == 0.0 || odo == 0.0)
                continue;
            WayPoint wp = new WayPoint(r.timestamp, odo, r.get(VTData.schema, VTData.SpeedKey),
                    r.get(VTData.schema, VTData.HeadingKey), lat, lng, Double.NaN,
                    r.get(VTData.schema, VTData.PowerKey), r.get(VTData.schema, VTData.SOCKey));
            handleNewWayPoint(wp);
        }
        endCurrentTrip();

        // Start listening for new WayPoints
        vtData.lastStoredStreamState.addTracker(new Runnable() {
            @Override
            public void run() {
                StreamState ss = vtData.lastStoredStreamState.get();
                ChargeState cs = vtData.lastStoredChargeState.get();
                handleNewWayPoint(new WayPoint(ss.timestamp, ss.odometer, ss.speed, ss.heading, ss.estLat,
                        ss.estLng, Double.NaN, ss.power, cs.batteryPercent));
            }
        });

        highlightDaysWithTrips(Calendar.getInstance());
    }

    private void endCurrentTrip() {
        if (tripInProgress == null)
            return;

        if (tripInProgress.distance() > 0.1) {
            updateTripData(tripInProgress);
        }

        tripInProgress = null;
    }

    private Trip tripInProgress = null;

    private void handleNewWayPoint(WayPoint wp) {
        if (tripInProgress == null) {
            // No Trip in progress, start one
            startNewTrip(wp);
            return;
        }

        WayPoint last = tripInProgress.lastWayPoint();
        if ((wp.getTime() - last.getTime() > MaxTimeBetweenWayPoints)) {
            // Finish the old trip and start a new one
            endCurrentTrip();
            startNewTrip(wp);
            return;
        }

        if (thereWasMotion(wp, last)) {
            // Add to the current Trip
            tripInProgress.addWayPoint(wp);
        }
    }

    private void startNewTrip(WayPoint wp) {
        tripInProgress = new Trip();
        tripInProgress.addWayPoint(wp);
    }

    private void updateTripData(Trip t) {
        WayPoint wp = t.firstWayPoint();
        Date d = new Date(wp.getTime());
        String dateKey = keyFromDate(d);
        List<Trip> tripsForDay = dateToTrips.get(dateKey);
        if (tripsForDay == null) {
            tripsForDay = new ArrayList<>();
            dateToTrips.put(dateKey, tripsForDay);
        }
        tripsForDay.add(t);

        if (dateIsSelected(d)) {
            Platform.runLater(new Runnable() {
                @Override
                public void run() {
                    updateTripSelections();
                }
            });
        }

    }

    private boolean dateIsSelected(Date d) {
        Calendar tripDay = Calendar.getInstance();
        tripDay.setTime(d);
        int yr = tripDay.get(Calendar.YEAR);
        int doy = tripDay.get(Calendar.DAY_OF_YEAR);

        for (Calendar c : calendarPicker.calendars()) {
            if (c.get(Calendar.YEAR) == yr && c.get(Calendar.DAY_OF_YEAR) == doy) {
                return true;
            }
        }
        return false;
    }

    /*------------------------------------------------------------------------------
     *
     * PRIVATE - Methods to check proximity between points
     * 
     *----------------------------------------------------------------------------*/

    private boolean thereWasMotion(WayPoint wp1, WayPoint wp2) {
        double turn = 180.0 - Math.abs((Math.abs(wp1.getHeading() - wp2.getHeading()) % 360.0) - 180.0);
        double meters = GeoUtils.distance(wp1.getLat(), wp1.getLng(), wp2.getLat(), wp2.getLng());

        return (meters >= 5 || (turn > 10 && meters > 3.0));
    }

}