/*
* GenericTrigger.java - Copyright(c) 2013 Joe Pasqua
* Provided under the MIT License. See the LICENSE file for details.
* Created: Dec 14, 2013
*/
package org.noroomattheinn.visibletesla.trigger;
import java.util.Comparator;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import static org.noroomattheinn.tesla.Tesla.logger;
/**
* GenericTrigger: A generic trigger mechanism that handles a number of different
* predicates on an input type.
*
* @author Joe Pasqua <joe at NoRoomAtTheInn dot org>
*/
public class GenericTrigger<T> {
/*------------------------------------------------------------------------------
*
* Cosntants and Enums
*
*----------------------------------------------------------------------------*/
public enum Predicate {FallsBelow, HitsOrExceeds, Becomes, AnyChange, GT, LT, EQ};
public interface RW<T> extends Comparator<T> {
public String toExternal(T value);
public T fromExternal(String external);
public String formatted(T value);
public boolean isAny(T value);
public void persist(String key, String value);
public String load(String key, String dflt);
}
/*------------------------------------------------------------------------------
*
* Internal State
*
*----------------------------------------------------------------------------*/
private final String triggerName;
private final String key;
private final BooleanProperty isEnabled;
private final ObjectProperty<T> targetProperty;
private final T targetDefault;
private final Predicate predicate;
private final long bounceInterval;
private final RW<T> th;
private T curVal, lastVal;
private long lastTimeSatisfied;
/*==============================================================================
* ------- -------
* ------- Public Interface To This Class -------
* ------- -------
*============================================================================*/
public GenericTrigger(BooleanProperty isEnabled, RW<T> th,
String name, String key, Predicate predicate,
ObjectProperty<T> targetProperty, T targetDefault,
long bounceInterval) {
this.triggerName = name;
this.key = key;
this.isEnabled = isEnabled;
this.targetProperty = targetProperty;
this.targetDefault = targetDefault;
this.predicate = predicate;
this.bounceInterval = bounceInterval;
this.th = th;
this.lastTimeSatisfied = 0;
this.curVal = this.lastVal = null;
targetProperty.addListener(new ChangeListener<T>() {
@Override public void changed(ObservableValue<? extends T> ov, T t, T t1)
{ externalize(); }
});
isEnabled.addListener(new ChangeListener<Boolean>() {
@Override public void changed(ObservableValue<? extends Boolean> ov, Boolean t, Boolean t1)
{ externalize(); }
});
internalize();
}
public String getTriggerName() { return triggerName; }
public Predicate getPredicateType() { return predicate; }
public String getPredicateName() {
switch (predicate) {
case FallsBelow: return "fell below";
case HitsOrExceeds: return "hit or exceeded";
case Becomes: return "became";
case AnyChange: return "occurred";
case GT: return "is greater than";
case LT: return "is less than";
case EQ: return "is equal to";
}
return predicate.name();
}
public String getKey() { return key; }
public String getCurrentVal() {
return curVal == null ? "" : th.formatted(curVal);
}
public String getTargetVal() {
T triggerVal = targetProperty.get();
return triggerVal == null ? "" : th.formatted(triggerVal);
}
public boolean evalPredicate(T newVal) {
curVal = newVal;
if (isEnabled.get() && !bouncing()) {
if (satisfied(newVal)) {
lastTimeSatisfied = System.currentTimeMillis();
return true;
}
}
return false;
}
public String defaultMessage() {
String val = getCurrentVal();
String targetVal = getCurrentVal();
String pName = getPredicateName();
switch (predicate) {
case HitsOrExceeds:
case FallsBelow:
return String.format("%s %s %s (%s)",
triggerName, pName, targetVal, val);
case Becomes:
return String.format("%s became: %s", triggerName, val);
case AnyChange:
return String.format("%s Activity: %s", triggerName, val);
case EQ:
case LT:
case GT:
return String.format("%s %s %s", triggerName, pName, targetVal);
}
// If we ever get here it is a bug in the code - I added a type
// and didn't account for it in the switch. Do something useful...
logger.severe("Unexpected Predicate type: " + pName);
return String.format(
"%s %s %s (%s)", triggerName, pName, targetVal, curVal);
}
/*------------------------------------------------------------------------------
*
* Private Methods to evaluate the predicate
*
*----------------------------------------------------------------------------*/
private boolean satisfied(T current) {
boolean satisfied = false;
T targetVal = targetProperty.get();
if (predicate == Predicate.AnyChange) {
satisfied = true;
} else if (predicate == Predicate.GT) {
satisfied = th.compare(current, targetVal) > 0;
} else if (predicate == Predicate.LT) {
satisfied = th.compare(current, targetVal) < 0;
} else if (predicate == Predicate.EQ) {
satisfied = th.compare(current, targetVal) == 0;
} else if (lastVal != null) {
if (predicate == Predicate.FallsBelow) {
satisfied = th.compare(lastVal, targetVal) >= 0 &&
th.compare(current, targetVal) < 0;
} else if (predicate == Predicate.HitsOrExceeds) {
satisfied = th.compare(lastVal, targetVal) < 0 &&
th.compare(current, targetVal) >= 0;
} else if (predicate == Predicate.Becomes) {
if (th.isAny(targetVal)) {
satisfied = (th.compare(lastVal, current) != 0);
} else {
satisfied = th.compare(lastVal, current) != 0 &&
th.compare(current, targetVal) == 0;
}
}
}
lastVal = current;
return satisfied;
}
private boolean bouncing() {
if (bounceInterval == 0) return false;
return (System.currentTimeMillis() - lastTimeSatisfied < bounceInterval);
}
/*------------------------------------------------------------------------------
*
* Private Methods for internalizing and externalizing a Trigger
*
*----------------------------------------------------------------------------*/
private String onOff(boolean b) { return b ? "1" : "0"; }
private void externalize() {
String encoded = String.format("%s_%s", onOff(isEnabled.get()), th.toExternal(targetProperty.get()));
th.persist(key, encoded);
}
private void internalize() {
String dfltEncoded = th.toExternal(targetDefault);
String encoded = th.load(key, dfltEncoded);
String[] elements = encoded.split("_");
if (elements.length >= 2) {
isEnabled.set(elements[0].equals("1"));
targetProperty.set(th.fromExternal(elements[1]));
} else {
isEnabled.set(false);
targetProperty.set(th.fromExternal(dfltEncoded));
logger.warning("Malformed externalized trigger: " + encoded);
}
}
}