package com.openxc.measurements;
import com.google.common.base.Objects;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;
import android.content.Context;
import android.util.Log;
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
import com.openxc.NoValueException;
import com.openxc.messages.EventedSimpleVehicleMessage;
import com.openxc.messages.MessageKey;
import com.openxc.messages.NamedVehicleMessage;
import com.openxc.messages.SimpleVehicleMessage;
import com.openxc.units.Unit;
import com.openxc.util.AgingData;
import com.openxc.util.Range;
/**
* The BaseMeasurement is the base implementation of the Measurement, and
* wraps an instance of a {@link Unit}, and the value it returns is always
* in terms of this Unit.
*
* The Unit wrapper might seem annoying at first, but it is critical to avoid
* misinterpreting the unit and crashing your lander into Mars
* (http://en.wikipedia.org/wiki/Mars_Climate_Orbiter).
*
* Most applications will not use this class directly, but will import specific
* child classes that correspond to specific types of measurements - i.e. the
* parameterized instances of this class with a Unit. That may seem like a
* "pseudo-typedef" but we're using it to enforce the binding between
* the measurement and its unit type. This unfortunately means we have to add
* constructors to every child class because they aren't inherited from
* Measurement. If you know of a better way, please say so.
*/
public abstract class BaseMeasurement<TheUnit extends Unit>
implements Measurement {
private static final String TAG = BaseMeasurement.class.toString();
protected AgingData<TheUnit> mValue;
private Range<TheUnit> mRange;
private static Map<Class<? extends Measurement>, String>
sCachedPrettyNames = new HashMap<>();
private static BiMap<String, Class<? extends Measurement>>
sMeasurementIdToClass;
static {
sMeasurementIdToClass = HashBiMap.create();
try {
cacheMeasurementId(AcceleratorPedalPosition.class);
cacheMeasurementId(BrakePedalStatus.class);
cacheMeasurementId(EngineSpeed.class);
cacheMeasurementId(FuelConsumed.class);
cacheMeasurementId(FuelLevel.class);
cacheMeasurementId(HeadlampStatus.class);
cacheMeasurementId(HighBeamStatus.class);
cacheMeasurementId(IgnitionStatus.class);
cacheMeasurementId(Latitude.class);
cacheMeasurementId(Longitude.class);
cacheMeasurementId(Odometer.class);
cacheMeasurementId(ParkingBrakeStatus.class);
cacheMeasurementId(SteeringWheelAngle.class);
cacheMeasurementId(TorqueAtTransmission.class);
cacheMeasurementId(TransmissionGearPosition.class);
cacheMeasurementId(TurnSignalStatus.class);
cacheMeasurementId(VehicleButtonEvent.class);
cacheMeasurementId(VehicleDoorStatus.class);
cacheMeasurementId(VehicleSpeed.class);
cacheMeasurementId(WindshieldWiperStatus.class);
} catch(UnrecognizedMeasurementTypeException e) { }
}
public abstract String getGenericName();
/**
* Construct a new Measurement with the given value.
*
* @param value the TheUnit this measurement represents.
*/
public BaseMeasurement(TheUnit value) {
if(!sMeasurementIdToClass.inverse().containsKey(this.getClass())) {
try {
cacheMeasurementId(this.getClass());
} catch(UnrecognizedMeasurementTypeException e) {
Log.w(TAG, "Incomplete BaseMeasurement subclass", e);
}
}
mValue = new AgingData<>(value);
}
/**
* Construct an new Measurement with the given value and valid Range.
*
* There is not currently any automated verification that the value is
* within the range - this is up to the application programmer.
*
* @param value the TheUnit this measurement represents.
* @param range the valid {@link Range} of values for this measurement.
*/
public BaseMeasurement(TheUnit value, Range<TheUnit> range) {
this(value);
mRange = range;
}
@Override
public void setTimestamp(long timestamp) {
mValue.setTimestamp(timestamp);
}
@Override
public long getAge() {
return mValue.getAge();
}
@Override
public long getBirthtime() {
return mValue.getTimestamp();
}
@Override
public boolean hasRange() {
return mRange != null;
}
@Override
public Range<TheUnit> getRange() {
return mRange;
}
@Override
public TheUnit getValue() {
return mValue.getValue();
}
@Override
public Object getSerializedValue() {
return getValue().getSerializedValue();
}
@Override
public SimpleVehicleMessage toVehicleMessage() {
return new SimpleVehicleMessage(mValue.getTimestamp(),
getGenericName(), getSerializedValue());
}
public String getName(Context context) {
String name = getGenericName();
if(!sCachedPrettyNames.containsKey(getClass())) {
// Make sure to not use the package name here, we have to find the
// resource using the package name of the app using the library instead.
int identifier = context.getResources().getIdentifier(
getGenericName() + "_label", "string", context.getPackageName());
if(identifier != 0) {
name = context.getString(identifier);
sCachedPrettyNames.put(getClass(), name);
}
} else if(sCachedPrettyNames.get(getClass()) != null) {
name = sCachedPrettyNames.get(getClass());
}
return name;
}
private static void cacheMeasurementId(
Class<? extends Measurement> measurementType)
throws UnrecognizedMeasurementTypeException {
String measurementId;
try {
measurementId = (String) measurementType.getField("ID").get(
measurementType);
sMeasurementIdToClass.put(measurementId, measurementType);
} catch(NoSuchFieldException e) {
throw new UnrecognizedMeasurementTypeException(
measurementType + " doesn't have an ID field", e);
} catch(IllegalAccessException e) {
throw new UnrecognizedMeasurementTypeException(
measurementType + " has an inaccessible " +
"ID field", e);
}
}
public static MessageKey getKeyForMeasurement(
Class<? extends Measurement> measurementType)
throws UnrecognizedMeasurementTypeException {
if(!sMeasurementIdToClass.inverse().containsKey(measurementType)) {
cacheMeasurementId(measurementType);
}
return new NamedVehicleMessage(
sMeasurementIdToClass.inverse().get(measurementType)).getKey();
}
public static Class<? extends Measurement>
getClassForId(String measurementId)
throws UnrecognizedMeasurementTypeException {
Class<? extends Measurement> result = sMeasurementIdToClass.get(
measurementId);
if(result == null) {
throw new UnrecognizedMeasurementTypeException(
"Didn't have a measurement with ID " + measurementId +
" cached");
}
return result;
}
public static Measurement getMeasurementFromMessage(
SimpleVehicleMessage message)
throws UnrecognizedMeasurementTypeException, NoValueException {
Class<? extends Measurement> measurementClass =
BaseMeasurement.getClassForId(message.getName());
return BaseMeasurement.getMeasurementFromMessage(measurementClass,
message);
}
public static Measurement getMeasurementFromMessage(
Class<? extends Measurement> measurementType,
SimpleVehicleMessage message)
throws UnrecognizedMeasurementTypeException, NoValueException {
Constructor<? extends Measurement> constructor;
if(message == null) {
throw new NoValueException();
}
try {
Measurement measurement;
SimpleVehicleMessage simpleMessage = message.asSimpleMessage();
Class<?> valueClass = simpleMessage.getValue().getClass();
if(valueClass == Double.class || valueClass == Integer.class) {
valueClass = Number.class;
}
if(message instanceof EventedSimpleVehicleMessage) {
EventedSimpleVehicleMessage eventedMessage =
message.asEventedMessage();
Class<?> eventClass = eventedMessage.getEvent().getClass();
if(eventClass == Double.class || eventClass == Integer.class) {
eventClass = Number.class;
}
try {
constructor = measurementType.getConstructor(
valueClass, eventClass);
} catch(NoSuchMethodException e) {
throw new UnrecognizedMeasurementTypeException(
measurementType +
" doesn't have the expected constructor, " +
measurementType + "(" +
valueClass + ", " + eventClass + ")");
}
measurement = constructor.newInstance(
eventedMessage.getValue(),
eventedMessage.getEvent());
} else {
try {
constructor = measurementType.getConstructor(valueClass);
} catch(NoSuchMethodException e) {
throw new UnrecognizedMeasurementTypeException(
measurementType +
" doesn't have the expected constructor, " +
measurementType + "(" +
valueClass + ")");
}
measurement = constructor.newInstance(
simpleMessage.getValue());
}
if (simpleMessage.getTimestamp() != null) {
measurement.setTimestamp(simpleMessage.getTimestamp());
}
// https://github.com/openxc/openxc-android/issues/185
return measurement;
} catch(InstantiationException e) {
throw new UnrecognizedMeasurementTypeException(
measurementType + " is abstract", e);
} catch(IllegalAccessException e) {
throw new UnrecognizedMeasurementTypeException(
measurementType + " has a private constructor", e);
} catch(IllegalArgumentException e) {
throw new UnrecognizedMeasurementTypeException(
measurementType + " has unexpected arguments", e);
} catch(InvocationTargetException e) {
throw new UnrecognizedMeasurementTypeException(
measurementType + "'s constructor threw an exception",
e);
}
}
@Override
public boolean equals(Object obj) {
if(this == obj) {
return true;
}
if(obj == null) {
return false;
}
if(getClass() != obj.getClass()) {
return false;
}
@SuppressWarnings("unchecked")
final BaseMeasurement<TheUnit> other = (BaseMeasurement<TheUnit>) obj;
return Objects.equal(getValue(), other.getValue()) &&
Objects.equal(other.getRange(), getRange());
}
@Override
public String toString() {
return getValue().toString();
}
}