package beast.evolution.tree;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import beast.core.BEASTObject;
import beast.core.Description;
import beast.core.Input;
import beast.core.Input.Validate;
import beast.core.util.Log;
import beast.evolution.alignment.TaxonSet;
@Description("A trait set represent a collection of properties of taxons, for the use of initializing a tree. " +
"The traits are represented as text content in taxon=value form, for example, for a date trait, we" +
"could have a content of chimp=1950,human=1991,neander=-10000. All white space is ignored, so they can" +
"be put on multiple tabbed lines in the XML. " +
"The type of node in the tree determines what happes with this information. The default Node only " +
"recognizes 'date', 'date-forward' and 'date-backward' as a trait, but by creating custom Node classes " +
"other traits can be supported as well.")
public class TraitSet extends BEASTObject {
public enum Units {
year, month, day
}
final public Input<String> traitNameInput = new Input<>("traitname", "name of the trait, used as meta data name for the tree. " +
"Special traitnames that are recognized are '" + DATE_TRAIT + "','" + DATE_FORWARD_TRAIT + "' and '" + DATE_BACKWARD_TRAIT + "'.", Validate.REQUIRED);
final public Input<Units> unitsInput = new Input<>("units", "name of the units in which values are posed, " +
"used for conversion to a real value. This can be " + Arrays.toString(Units.values()) + " (default 'year')", Units.year, Units.values());
final public Input<String> traitsInput = new Input<>("value", "traits encoded as taxon=value pairs separated by commas", Validate.REQUIRED);
final public Input<TaxonSet> taxaInput = new Input<>("taxa", "contains list of taxa to map traits to", Validate.REQUIRED);
final public Input<String> dateTimeFormatInput = new Input<>("dateFormat", "the date/time format to be parsed, (e.g., 'dd/M/yyyy')");
final public static String DATE_TRAIT = "date";
final public static String DATE_FORWARD_TRAIT = "date-forward";
final public static String DATE_BACKWARD_TRAIT = "date-backward";
/**
* String values of taxa in order of taxons in alignment*
*/
protected String[] taxonValues;
/**
* double representation of taxa value *
*/
double[] values;
double minValue;
double maxValue;
Map<String, Integer> map;
/**
* Whether or not values are ALL numeric.
*/
boolean numeric = true;
@Override
public void initAndValidate() {
if (traitsInput.get().matches("^\\s*$")) {
return;
}
// first, determine taxon numbers associated with traits
// The Taxon number is the index in the alignment, and
// used as node number in a tree.
map = new HashMap<>();
List<String> labels = taxaInput.get().asStringList();
String[] traits = traitsInput.get().split(",");
taxonValues = new String[labels.size()];
values = new double[labels.size()];
for (String trait : traits) {
trait = trait.replaceAll("\\s+", " ");
String[] strs = trait.split("=");
if (strs.length != 2) {
throw new IllegalArgumentException("could not parse trait: " + trait);
}
String taxonID = normalize(strs[0]);
int taxonNr = labels.indexOf(taxonID);
if (taxonNr < 0) {
throw new IllegalArgumentException("Trait (" + taxonID + ") is not a known taxon. Spelling error perhaps?");
}
taxonValues[taxonNr] = normalize(strs[1]);
values[taxonNr] = parseDouble(taxonValues[taxonNr]);
map.put(taxonID, taxonNr);
if (Double.isNaN(values[taxonNr]))
numeric = false;
}
// sanity check: did we cover all taxa?
for (int i = 0; i < labels.size(); i++) {
if (taxonValues[i] == null) {
Log.warning.println("WARNING: no trait specified for " + labels.get(i) +": Assumed to be 0");
map.put(labels.get(i), i);
}
}
// find extremes
minValue = values[0];
maxValue = values[0];
for (double value : values) {
minValue = Math.min(minValue, value);
maxValue = Math.max(maxValue, value);
}
if (traitNameInput.get().equals(DATE_TRAIT) || traitNameInput.get().equals(DATE_FORWARD_TRAIT)) {
for (int i = 0; i < labels.size(); i++) {
values[i] = maxValue - values[i];
}
}
if (traitNameInput.get().equals(DATE_BACKWARD_TRAIT)) {
for (int i = 0; i < labels.size(); i++) {
values[i] = values[i] - minValue;
}
}
for (int i = 0; i < labels.size(); i++) {
Log.info.println(labels.get(i) + " = " + taxonValues[i] + " (" + (values[i]) + ")");
}
} // initAndValidate
/**
* some getters and setters *
*/
public String getTraitName() {
return traitNameInput.get();
}
@Deprecated // use getStringValue by name instead
public String getStringValue(int taxonNr) {
return taxonValues[taxonNr];
}
@Deprecated // use getValue by name instead
public double getValue(int taxonNr) {
if (values == null) {
return 0;
}
return values[taxonNr];
}
public String getStringValue(String taxonName) {
if (taxonValues == null || map == null || map.get(taxonName) == null) {
return null;
}
return taxonValues[map.get(taxonName)];
}
public double getValue(String taxonName) {
if (values == null || map == null || map.get(taxonName) == null) {
return 0;
}
//Log.trace.println("Trait " + taxonName + " => " + values[map.get(taxonName)]);
return values[map.get(taxonName)];
}
/**
* see if we can convert the string to a double value *
*/
private double parseDouble(String str) {
// default, try to interpret the string as a number
try {
return Double.parseDouble(str);
} catch (NumberFormatException e) {
// does not look like a number
if (traitNameInput.get().equals(DATE_TRAIT) ||
traitNameInput.get().equals(DATE_FORWARD_TRAIT) ||
traitNameInput.get().equals(DATE_BACKWARD_TRAIT)) {
try {
double year;
if (dateTimeFormatInput.get() == null) {
if (str.matches(".*[a-zA-Z].*")) {
str = str.replace('/', '-');
}
// following is deprecated, but the best thing around at the moment
// see also comments in TipDatesInputEditor
long date = Date.parse(str);
year = 1970.0 + date / (60.0 * 60 * 24 * 365 * 1000);
Log.warning.println("No date/time format provided, using default parsing: '" + str + "' parsed as '" + year + "'");
} else {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(dateTimeFormatInput.get());
LocalDate date = LocalDate.parse(str, formatter);
Log.warning.println("Using format '" + dateTimeFormatInput.get() + "' to parse '" + str +
"' as: " + (date.getYear() + (date.getDayOfYear()-1.0) / (date.isLeapYear() ? 366.0 : 365.0)));
year = date.getYear() + (date.getDayOfYear()-1.0) / (date.isLeapYear() ? 366.0 : 365.0);
}
switch (unitsInput.get()) {
case month:
return year * 12.0;
case day:
return year * 365;
default:
return year;
}
} catch (DateTimeParseException e2) {
Log.err.println("Failed to parse date '" + str + "' using format '" + dateTimeFormatInput.get() + "'");
System.exit(1);
}
}
}
//return 0;
return Double.NaN;
} // parseStrings
/**
* remove start and end spaces
*/
String normalize(String str) {
if (str.charAt(0) == ' ') {
str = str.substring(1);
}
if (str.endsWith(" ")) {
str = str.substring(0, str.length() - 1);
}
return str;
}
public double getDate(double height) {
if (traitNameInput.get().equals(DATE_TRAIT) || traitNameInput.get().equals(DATE_FORWARD_TRAIT)) {
return maxValue - height;
}
if (traitNameInput.get().equals(DATE_BACKWARD_TRAIT)) {
return minValue + height;
}
return height;
}
/**
* Determines whether trait is recognised as specifying taxa dates.
* @return true if this is a date trait.
*/
public boolean isDateTrait() {
return traitNameInput.get().equals(DATE_TRAIT)
|| traitNameInput.get().equals(DATE_FORWARD_TRAIT)
|| traitNameInput.get().equals(DATE_BACKWARD_TRAIT);
}
/**
* @return true if trait values are (all) numeric.
*/
public boolean isNumeric() {
return numeric;
}
} // class TraitSet