/*
* BaseController.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.Map;
import java.util.ResourceBundle;
import java.util.TimerTask;
import java.util.concurrent.Callable;
import java.util.logging.Level;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.image.ImageView;
import javafx.scene.layout.AnchorPane;
import org.noroomattheinn.tesla.Result;
import org.noroomattheinn.tesla.Vehicle;
import org.noroomattheinn.utils.ThreadManager;
import org.noroomattheinn.visibletesla.data.VTData;
import org.noroomattheinn.visibletesla.vehicle.VTVehicle;
import static org.noroomattheinn.tesla.Tesla.logger;
/**
* BaseController: This superclass implements most of the common mechanisms used
* by all Controller (sub)classes. The methods in this class are divided into 5
* categories:<ol>
* <li>Methods completely implemented by this class and called by external
* objects (not the subclasses)</li>
* <li>Abstract methods that must be implemented by subclasses to provide
* subclass-specific functionality</li>
* <li>Protected methods with default implementations that *may* be overriden
* by subclasses</li>
* <li>Utility methods that may be used (but not overriden) by subclasses
* <li>Private/internal methods</li>
* </ol>
*
* @author Joe Pasqua <joe at NoRoomAtTheInn dot org>
*/
abstract class BaseController {
/*------------------------------------------------------------------------------
*
* Constants and Enums
*
*----------------------------------------------------------------------------*/
protected static final long MinRefreshInterval = 2 * 1000;
/*------------------------------------------------------------------------------
*
* Internal State
*
*----------------------------------------------------------------------------*/
private static BaseController activeController = null;
private static long lastRefreshTime;
protected static CommandIssuer issuer = null;
protected boolean initialized = false; // Has this controller been init'd?
protected App app; // The overall app context
protected VTVehicle vtVehicle = null; // The current VTVehicle
protected VTData vtData = null; // The data service
protected Prefs prefs = null; // The Prefs object
/*------------------------------------------------------------------------------
*
* UI Elements that are common to all subclasses (Controllers)
*
*----------------------------------------------------------------------------*/
@FXML protected ResourceBundle resources;
@FXML protected URL location;
@FXML protected AnchorPane root;
@FXML protected Button refreshButton;
@FXML protected ProgressIndicator progressIndicator;
/*==============================================================================
* ------- -------
* ------- Public Interface To This Class -------
* ------- -------
*============================================================================*/
@FXML final void initialize() {
root.setUserData(this);
prepCommonElements();
fxInitialize(); // This is an initialization hook for subclasses
}
/**
* This is called ONE TIME at startup to establish the context for each
* controller. The context includes the App object, the vehicle we're
* monitoring, the data service, and the Preferences.
*/
final void setAppContext(
App ctxt, VTVehicle v, VTData data, Prefs p) {
this.app = ctxt;
this.vtVehicle = v;
this.vtData = data;
this.prefs = p;
if (issuer == null) issuer = new CommandIssuer(app.progressListener);
}
/**
* Called whenever the tab associated with this controller is activated.
*/
final void activate() {
activeController = this;
if (!initialized) { initializeState(); initialized = true; }
activateTab();
if (app.api.isIdle()) {
if (prefs.wakeOnTabChange.get()) {
app.api.setActive();
doRefresh();
}
} else {
doRefresh();
}
}
/*------------------------------------------------------------------------------
*
* UI Action Handlers
*
*----------------------------------------------------------------------------*/
@FXML protected void refreshButtonHandler(ActionEvent event) {
if (System.currentTimeMillis() - lastRefreshTime < MinRefreshInterval) {
logger.log(Level.INFO, "Ignoring refresh button - we just refreshed!");
return;
}
doRefresh();
}
/*------------------------------------------------------------------------------
*
* The following methods must be implemented by subclasses though the
* implementation may be empty.
*
*----------------------------------------------------------------------------*/
/**
* The controller is being initialized along with the associated UI. This
* is a chance for the BaseController subclass to do any necessary setup.
*/
abstract protected void fxInitialize();
/**
* Subclasses can do whatever is needed to prepare for operations on the
* given vehicle. E.g., they may wish to create a tesla.XYZController object.
*/
abstract protected void initializeState();
/**
* Called every time the associated tab is selected.
*/
abstract protected void activateTab();
/**
* Do whatever is required to refresh the state the controller uses
* to display its UI. This doesn't actually update the UI, it just gets
* the state needed to do so.
*/
abstract protected void refresh();
/*------------------------------------------------------------------------------
*
* Convenience methods used to issue commands and update state
*
*----------------------------------------------------------------------------*/
protected final void issueCommand(Callable<Result> c, String commandName) {
issuer.issueCommand(c, true, progressIndicator, commandName);
}
protected final void updateState(Vehicle.StateType whichState) {
vtData.produceState(whichState, progressIndicator);
}
protected final void updateStateLater(final Vehicle.StateType whichState, long delay) {
ThreadManager.get().addTimedTask(new TimerTask() {
@Override public void run() { updateState(whichState); } }, delay);
}
/*------------------------------------------------------------------------------
*
* Utility Methods for use by subclasses
*
*----------------------------------------------------------------------------*/
protected final void setOptionState(boolean selected, ImageView selImg, ImageView deselImg) {
if (deselImg != null) deselImg.setVisible(!selected);
if (selImg != null) selImg.setVisible(selected);
}
protected <K> void updateImages(K key, Map<K,ImageView[]> imageMap, Map<K,K> equivs) {
// Turn off all images
for (ImageView[] images: imageMap.values()) {
for (ImageView image: images) { image.setVisible(false); }
}
// Turn appropriate images on
K mapped = equivs.get(key);
if (mapped != null) key = mapped;
ImageView[] images = imageMap.get(key);
for (ImageView image: images) { image.setVisible(true); }
}
protected final boolean active() { return activeController == this; }
/**
* Convenience function to return a Prefs key that is prefixed by the VIN
* of the current vehicle.
* @param key The raw Prefs key
* @return The Prefs key prefixed by the VIN
*/
final String vinKey(String key) {
return vtVehicle.getVehicle().getVIN() + "_" + key;
}
/*------------------------------------------------------------------------------
*
* Private Utility Methods
*
*----------------------------------------------------------------------------*/
/**
* Wrapper around refresh() that keeps track of the time to facilitate AutoRefresh
*/
private void doRefresh() {
lastRefreshTime = System.currentTimeMillis(); // Used by the AutoRefresh mechanism
refresh();
}
private static final String ProgressIndicatorColor = "darkgray";
private static final double ProgressIndicatorSize = 24.0;
private static final double ProgressIndicatorOffset = 14.0;
private static final double RefreshButtonOffset = 14.0;
private void prepCommonElements() {
if (progressIndicator != null) {
// Set the size, color, and location/anchor of the progressIndicator
progressIndicator.setStyle("-fx-progress-color: " + ProgressIndicatorColor + ";");
progressIndicator.setMaxWidth(ProgressIndicatorSize);
progressIndicator.setMaxHeight(ProgressIndicatorSize);
// Place the indicator at a fixed offset from the lower left corner
AnchorPane.setLeftAnchor(progressIndicator, ProgressIndicatorOffset);
AnchorPane.setBottomAnchor(progressIndicator, ProgressIndicatorOffset);
}
// Set the location/anchor of the refresh button
if (refreshButton != null) { // Some controllers don't have a refresh button
AnchorPane.setRightAnchor(refreshButton, RefreshButtonOffset);
AnchorPane.setBottomAnchor(refreshButton, RefreshButtonOffset);
}
}
}