/*
* GraphController.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 java.net.URL;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.scene.chart.XYChart;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Label;
import javafx.scene.control.RadioMenuItem;
import javafx.scene.control.ToggleGroup;
import javafx.scene.layout.AnchorPane;
import org.noroomattheinn.fxextensions.TimeBasedChart;
import org.noroomattheinn.fxextensions.VTLineChart;
import org.noroomattheinn.fxextensions.VTSeries;
import org.noroomattheinn.tesla.ChargeState;
import org.noroomattheinn.tesla.StreamState;
import org.noroomattheinn.timeseries.Row;
import org.noroomattheinn.utils.DefaultedHashMap;
import org.noroomattheinn.utils.Utils;
import org.noroomattheinn.visibletesla.data.VTData;
/**
* GraphController: Handles the capture and display of vehicle statistics
*
* NOTES:
* To add a new Series:
* 1. If the series requires a new state object:
* 1.1 Add the declaration of the object
* 1.2 Initialize the object in prepForVehicle
* 1.3 In getAndRecordStats: refresh the object and addElement on each series
* 2. Add the corresponding checkbox
* 2.1 Add a decalration for the checkbox and compile this source
* 2.2 Open GraphUI.fxml and add a checkbox to the dropdown list
* 3. Register the new series in prepSeries()
*
* @author Joe Pasqua <joe at NoRoomAtTheInn dot org>
*/
public class GraphController extends BaseController {
/*------------------------------------------------------------------------------
*
* Internal State
*
*----------------------------------------------------------------------------*/
private boolean displayLines = true;
private boolean displayMarkers = true;
/*------------------------------------------------------------------------------
*
* UI Elements
*
*----------------------------------------------------------------------------*/
@FXML private Label readout;
@FXML private CheckBox voltageCheckbox;
@FXML private CheckBox currentCheckbox;
@FXML private CheckBox rangeCheckbox;
@FXML private CheckBox socCheckbox;
@FXML private CheckBox rocCheckbox;
@FXML private CheckBox powerCheckbox;
@FXML private CheckBox batteryCurrentCheckbox;
@FXML private CheckBox speedCheckbox;
@FXML private AnchorPane itemListContent;
@FXML private Button nowButton;
@FXML private AnchorPane arrow;
private RadioMenuItem displayLinesMI;
private RadioMenuItem displayMarkersMI;
private RadioMenuItem displayBothMI;
private TimeBasedChart chart;
private VTLineChart lineChart = null;
/*------------------------------------------------------------------------------
*
* This section implements UI Actionhandlers
*
*----------------------------------------------------------------------------*/
private void showItemList(boolean visible) {
itemListContent.setVisible(visible);
itemListContent.setMouseTransparent(!visible);
arrow.setStyle(visible ? "-fx-rotate: 0;" : "-fx-rotate: -90;");
}
@FXML void nowHandler(ActionEvent event) {
chart.centerTime(System.currentTimeMillis());
}
@FXML void showItemsHandler(ActionEvent event) {
boolean isVisible = itemListContent.isVisible();
showItemList(!isVisible); // Flip whether it's visible
}
@FXML void optionCheckboxHandler(ActionEvent event) {
CheckBox cb = (CheckBox) event.getSource();
boolean visible = cb.isSelected();
VTSeries series = cbToSeries.get(cb);
series.setVisible(visible);
lineChart.refreshChart();
// Remember the value for next time we start up
prefs.storage().putBoolean(vinKey(series.getName()), visible);
}
/*------------------------------------------------------------------------------
*
* VTSeries Handling
*
*----------------------------------------------------------------------------*/
private Map<CheckBox,VTSeries> cbToSeries = new LinkedHashMap<>(); // Preserves insertion order
private Map<String,VTSeries> typeToSeries = new HashMap<>();
private void prepSeries() {
VTSeries.Transform<Number> distTransform =
vtVehicle.unitType() == Utils.UnitType.Imperial
? VTSeries.idTransform : VTSeries.mToKTransform;
lineChart.clearSeries();
cbToSeries.put(voltageCheckbox, lineChart.register(
new VTSeries(VTData.VoltageKey, VTSeries.millisToSeconds, VTSeries.idTransform)));
cbToSeries.put(currentCheckbox, lineChart.register(
new VTSeries(VTData.CurrentKey, VTSeries.millisToSeconds, VTSeries.idTransform)));
cbToSeries.put(rangeCheckbox, lineChart.register(
new VTSeries(VTData.EstRangeKey, VTSeries.millisToSeconds, distTransform)));
cbToSeries.put(socCheckbox, lineChart.register(
new VTSeries(VTData.SOCKey, VTSeries.millisToSeconds, VTSeries.idTransform)));
cbToSeries.put(rocCheckbox, lineChart.register(
new VTSeries(VTData.ROCKey, VTSeries.millisToSeconds, distTransform)));
cbToSeries.put(powerCheckbox, lineChart.register(
new VTSeries(VTData.PowerKey, VTSeries.millisToSeconds, VTSeries.idTransform)));
cbToSeries.put(speedCheckbox, lineChart.register(
new VTSeries(VTData.SpeedKey, VTSeries.millisToSeconds, distTransform)));
cbToSeries.put(batteryCurrentCheckbox, lineChart.register(
new VTSeries(VTData.BatteryAmpsKey, VTSeries.millisToSeconds, VTSeries.idTransform)));
// Make the checkbox colors match the series colors
int seriesNumber = 0;
for (Map.Entry<CheckBox,VTSeries> me: cbToSeries.entrySet()) {
CheckBox cb = me.getKey();
VTSeries s = me.getValue();
cb.getStyleClass().add("cb"+seriesNumber++);
typeToSeries.put(s.getName(), s);
}
}
private void restoreLastSettings() {
// Restore the last settings of the checkboxes
for (CheckBox cb : cbToSeries.keySet()) {
VTSeries s = cbToSeries.get(cb);
boolean selected = prefs.storage().getBoolean(vinKey(s.getName()), true);
cb.setSelected(selected);
s.setVisible(selected);
}
// Restore the last display settings (display lines, markers, or both)
displayLines = prefs.storage().getBoolean(vinKey("DISPLAY_LINES"), true);
displayMarkers = prefs.storage().getBoolean(vinKey("DISPLAY_MARKERS"), true);
reflectDisplayOptions();
}
/*------------------------------------------------------------------------------
*
* Methods overridden from BaseController
*
*----------------------------------------------------------------------------*/
@Override protected void initializeState() {
// This is a hack!! For some reason this is the only way I can get styles
// to work for ToolTips. I should be able to just choose the appropriate
// css class decalratively, but that doesn't work and no one seems to
// know why. This is a workaround.
URL url = getClass().getClassLoader().getResource("org/noroomattheinn/styles/tooltip.css");
app.stage.getScene().getStylesheets().add(url.toExternalForm());
prepSeries();
loadExistingData();
// Register for additions to the list - Handle the new list on the JFX
// thread to avoid ConcurrentModificationExceptions in the series list
App.addTracker(vtData.lastStoredChargeState, chargeHandler);
App.addTracker(vtData.lastStoredStreamState, streamHandler);
setGap();
prefs.ignoreGraphGaps.addListener(new ChangeListener<Boolean>() {
@Override public void changed(ObservableValue<? extends Boolean> ov, Boolean t, Boolean t1) {
setGap(); lineChart.refreshChart();
}
});
prefs.graphGapTime.addListener(new ChangeListener<Number>() {
@Override public void changed(ObservableValue<? extends Number> ov, Number t, Number t1) {
setGap(); lineChart.refreshChart();
}
});
}
@Override protected void activateTab() { }
@Override protected void refresh() { }
@Override protected void fxInitialize() {
nowButton.setVisible(false);
refreshButton.setDisable(true);
refreshButton.setVisible(false);
progressIndicator.setVisible(false);
chart = new TimeBasedChart(root, readout);
lineChart = chart.getChart();
createContextMenu();
showItemList(false);
root.getChildren().add(0, lineChart);
nowButton.setVisible(true);
}
/*------------------------------------------------------------------------------
*
* PRIVATE - Utility Methods for attaching a ContextMenu to the LineChart
*
*----------------------------------------------------------------------------*/
private void createContextMenu() {
final ContextMenu contextMenu = new ContextMenu();
final ToggleGroup toggleGroup = new ToggleGroup();
displayLinesMI = new RadioMenuItem("Display Only Lines");
displayLinesMI.setOnAction(displayMIHandler);
displayLinesMI.setSelected(displayLines);
displayLinesMI.setToggleGroup(toggleGroup);
displayMarkersMI = new RadioMenuItem("Display Only Markers");
displayMarkersMI.setOnAction(displayMIHandler);
displayMarkersMI.setSelected(displayMarkers);
displayMarkersMI.setToggleGroup(toggleGroup);
displayBothMI = new RadioMenuItem("Display Both");
displayBothMI.setOnAction(displayMIHandler);
displayBothMI.setSelected(displayMarkers);
displayBothMI.setToggleGroup(toggleGroup);
if (displayLines && displayMarkers) { displayBothMI.setSelected(true);
} else if (displayLines) { displayLinesMI.setSelected(true);
} else if (displayMarkers) { displayMarkersMI.setSelected(true);
}
contextMenu.getItems().addAll(displayLinesMI, displayMarkersMI, displayBothMI);
chart.addContextMenu(contextMenu);
}
private EventHandler<ActionEvent> displayMIHandler = new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent event) {
RadioMenuItem target = (RadioMenuItem) event.getTarget();
if (target == displayLinesMI) {
displayLines = true;
displayMarkers = false;
} else if (target == displayMarkersMI) {
displayLines = false;
displayMarkers = true;
} else {
displayLines = true;
displayMarkers = true;
}
reflectDisplayOptions();
prefs.storage().putBoolean(vinKey("DISPLAY_LINES"), displayLines);
prefs.storage().putBoolean(vinKey("DISPLAY_MARKERS"), displayMarkers);
}
};
private void reflectDisplayOptions() {
if (displayMarkers && displayLines) {
displayBothMI.setSelected(true);
lineChart.setDisplayMode(VTLineChart.DisplayMode.Both);
} else if (displayLines) {
displayLinesMI.setSelected(true);
displayBothMI.setSelected(false);
lineChart.setDisplayMode(VTLineChart.DisplayMode.LinesOnly);
} else {
displayMarkersMI.setSelected(true);
displayBothMI.setSelected(false);
lineChart.setDisplayMode(VTLineChart.DisplayMode.MarkersOnly);
}
}
private void setGap() {
lineChart.setIgnoreGap(prefs.ignoreGraphGaps.get(),
prefs.graphGapTime.get());
}
/*------------------------------------------------------------------------------
*
* PRIVATE - Loading existing list into the Series
*
*----------------------------------------------------------------------------*/
private void loadExistingData() {
Map<Long,Row> rows = vtData.getAllLoadedRows();
Map<String,ObservableList<XYChart.Data<Number,Number>>> typeToList = new HashMap<>();
for (String type : typeToSeries.keySet()) {
ObservableList<XYChart.Data<Number,Number>> data =
FXCollections.<XYChart.Data<Number,Number>>observableArrayList();
typeToList.put(type, data);
}
DefaultedHashMap<String,Long> lastTimeForType = new DefaultedHashMap<>(0L);
DefaultedHashMap<String,Double> lastValForType = new DefaultedHashMap<>(0.0);
for (Row row : rows.values()) {
long time = row.timestamp;
long bit = 1;
for (int i = 0; i < row.values.length; i++) {
if (row.includes(bit)) {
String type = VTData.schema.columnNames[i];
VTSeries vts = typeToSeries.get(type);
if (vts != null) { // It's a column that we're graphing
double value = row.values[i];
ObservableList<XYChart.Data<Number,Number>> list = typeToList.get(type);
// Don't overload the graph. Make sure that samples are
// At least 5 seconds apart unless they represent a huge
// swing in values: greater than 50%
if (time - lastTimeForType.get(type) >= 5 * 1000 ||
Utils.percentChange(value, lastValForType.get(type)) > 0.5) {
if (type.equals(VTData.SpeedKey) && add0Speed(time, value)) {
vts.addToData(list, time - (5 * 1000), 0);
}
vts.addToData(list, time, value);
lastTimeForType.put(type, time);
lastValForType.put(type, value);
}
}
}
bit = bit << 1;
}
}
for (Map.Entry<String,VTSeries> entry : typeToSeries.entrySet()) {
VTSeries vts = entry.getValue();
String type = entry.getKey();
vts.setData(typeToList.get(type));
}
lineChart.applySeriesToChart();
restoreLastSettings();
}
/*------------------------------------------------------------------------------
*
* PRIVATE - Listen for and add new list points to the graph
*
*----------------------------------------------------------------------------*/
private void addElement(final VTSeries series, final long time, double value) {
if (Double.isNaN(value) || Double.isInfinite(value)) value = 0;
series.addToSeries(time, value);
}
private final Runnable chargeHandler = new Runnable() {
@Override public void run() {
ChargeState cs = vtData.lastStoredChargeState.get();
addElement(typeToSeries.get(VTData.VoltageKey), cs.timestamp, cs.chargerVoltage);
addElement(typeToSeries.get(VTData.CurrentKey), cs.timestamp, cs.chargerActualCurrent);
addElement(typeToSeries.get(VTData.EstRangeKey), cs.timestamp, cs.range);
addElement(typeToSeries.get(VTData.SOCKey), cs.timestamp, cs.batteryPercent);
addElement(typeToSeries.get(VTData.ROCKey), cs.timestamp, cs.chargeRate);
addElement(typeToSeries.get(VTData.BatteryAmpsKey), cs.timestamp, cs.batteryCurrent);
}
};
private final Runnable streamHandler = new Runnable() {
@Override public void run() {
StreamState ss = vtData.lastStoredStreamState.get();
addElement(typeToSeries.get(VTData.PowerKey), ss.timestamp, ss.power);
if (add0Speed(ss.timestamp, ss.speed)) {
addElement(typeToSeries.get(VTData.SpeedKey), ss.timestamp - (5 * 1000), 0);
}
addElement(typeToSeries.get(VTData.SpeedKey), ss.timestamp, ss.speed);
}
};
private long lastTime = 0;
private double lastSpeed = -1.0;
private boolean add0Speed(long curTime, double curSpeed) {
boolean add0 = false;
if (lastSpeed == 0.0 && curSpeed != 0.0) {
if (curTime - lastTime > 60 * 1000L) {
add0 = true;
}
}
lastTime = curTime;
lastSpeed = curSpeed;
return add0;
}
}