package tim.prune.load;
import tim.prune.I18nManager;
import tim.prune.data.Field;
import tim.prune.data.Latitude;
import tim.prune.data.Longitude;
import tim.prune.data.Timestamp;
/**
* Class to try to match data with field names,
* using a variety of guessing techniques
*/
public abstract class FieldGuesser
{
/**
* Try to guess whether the given line is a header line or data
* @param inValues array of values from first non-blank line of file
* @return true if it looks like a header row, false if it looks like data
*/
private static boolean isHeaderRow(String[] inValues)
{
// Loop over values seeing if any are mostly numeric
if (inValues != null)
{
for (String value : inValues)
{
if (fieldLooksNumeric(value)) {return false;}
}
}
// No (mostly) numeric values found so presume header
return true;
}
/**
* See if a field looks numeric or not by comparing the number of numeric vs non-numeric characters
* @param inValue field value to check
* @return true if there are more numeric characters than not
*/
private static boolean fieldLooksNumeric(String inValue)
{
if (inValue == null) {
return false;
}
final int numChars = inValue.length();
if (numChars < 3) {return false;} // Don't care about one or two character values
// Loop through characters seeing which ones are numeric and which not
int numNums = 0;
for (int i=0; i<numChars; i++)
{
char currChar = inValue.charAt(i);
if (currChar >= '0' && currChar <= '9') {numNums++;}
}
// Return true if more than half the characters are numeric
return numNums > (numChars/2);
}
/**
* Try to guess the fields for the given values from the file
* @param inValues array of values from first non-blank line of file
* @return array of fields which hopefully match
*/
public static Field[] guessFields(String[] inValues)
{
// Guess whether it's a header line or not
boolean isHeader = isHeaderRow(inValues);
// make array of Fields
int numFields = inValues.length;
Field[] fields = new Field[numFields];
// Loop over fields to try to guess the main ones
for (int f=0; f<numFields; f++)
{
if (inValues[f] != null) {
String value = inValues[f].trim();
// check for latitude
if (!checkArrayHasField(fields, Field.LATITUDE) && fieldLooksLikeLatitude(value, isHeader))
{
fields[f] = Field.LATITUDE;
continue;
}
// check for longitude
if (!checkArrayHasField(fields, Field.LONGITUDE) && fieldLooksLikeLongitude(value, isHeader))
{
fields[f] = Field.LONGITUDE;
continue;
}
// check for altitude
if (!checkArrayHasField(fields, Field.ALTITUDE) && fieldLooksLikeAltitude(value, isHeader))
{
fields[f] = Field.ALTITUDE;
continue;
}
// check for waypoint name
if (!checkArrayHasField(fields, Field.WAYPT_NAME) && fieldLooksLikeName(value, isHeader))
{
fields[f] = Field.WAYPT_NAME;
continue;
}
// check for timestamp
if (!checkArrayHasField(fields, Field.TIMESTAMP) && fieldLooksLikeTimestamp(value, isHeader))
{
fields[f] = Field.TIMESTAMP;
continue;
}
// check for tracksegment
if (!checkArrayHasField(fields, Field.NEW_SEGMENT) && fieldLooksLikeSegment(value, isHeader))
{
fields[f] = Field.NEW_SEGMENT;
continue;
}
// check for waypoint type
if (!checkArrayHasField(fields, Field.WAYPT_TYPE) && fieldLooksLikeWaypointType(value, isHeader))
{
fields[f] = Field.WAYPT_TYPE;
continue;
}
}
}
// Fill in the rest of the fields using just custom fields
// Could try to guess other fields (waypoint type, segment) or unguessed altitude, name, but keep simple for now
String customPrefix = I18nManager.getText("fieldname.prefix") + " ";
int customFieldNum = 0;
for (int f=0; f<numFields; f++) {
if (fields[f] == null)
{
// Make sure lat and long are filled in if not already
if (!checkArrayHasField(fields, Field.LATITUDE)) {
fields[f] = Field.LATITUDE;
}
else if (!checkArrayHasField(fields, Field.LONGITUDE)) {
fields[f] = Field.LONGITUDE;
}
else
{
// Can we use the field name given?
Field customField = null;
if (isHeader && inValues[f] != null && inValues[f].length() > 0) {
customField = new Field(inValues[f]);
}
// Find an unused field number
while (customField == null || checkArrayHasField(fields, customField))
{
customFieldNum++;
customField = new Field(customPrefix + (customFieldNum));
}
fields[f] = customField;
}
}
}
// Do a final check to make sure lat and long are in there
if (!checkArrayHasField(fields, Field.LATITUDE)) {
fields[0] = Field.LATITUDE;
}
else if (!checkArrayHasField(fields, Field.LONGITUDE)) {
fields[1] = Field.LONGITUDE;
}
// Longitude _could_ have overwritten latitude in position 1
if (!checkArrayHasField(fields, Field.LATITUDE)) {
fields[0] = Field.LATITUDE;
}
return fields;
}
/**
* Check whether the given field array has the specified field
* @param inFields
* @param inCheckField
* @return true if Field is contained within the array
*/
private static boolean checkArrayHasField(Field[] inFields, Field inCheckField)
{
for (int f=0; f<inFields.length; f++)
{
if (inFields[f] != null && inFields[f].equals(inCheckField)) {
return true;
}
}
// not found
return false;
}
/**
* Check whether the given String looks like a Latitude value
* @param inValue value from file
* @param inIsHeader true if this is a header line, false for data
* @return true if it could be latitude
*/
private static boolean fieldLooksLikeLatitude(String inValue, boolean inIsHeader)
{
if (inValue == null || inValue.equals("")) {return false;}
if (inIsHeader)
{
// This is a header line so look for english or local text
String upperValue = inValue.toUpperCase();
return (upperValue.equals("LATITUDE")
|| upperValue.equals(I18nManager.getText("fieldname.latitude").toUpperCase()));
}
else
{
// Note this will also catch longitudes too
Latitude lat = new Latitude(inValue);
return lat.isValid();
}
}
/**
* Check whether the given String looks like a Longitude value
* @param inValue value from file
* @param inIsHeader true if this is a header line, false for data
* @return true if it could be longitude
*/
private static boolean fieldLooksLikeLongitude(String inValue, boolean inIsHeader)
{
if (inValue == null || inValue.equals("")) {return false;}
if (inIsHeader)
{
// This is a header line so look for english or local text
String upperValue = inValue.toUpperCase();
return (upperValue.equals("LONGITUDE")
|| upperValue.equals(I18nManager.getText("fieldname.longitude").toUpperCase()));
}
else
{
// Note this will also catch latitudes too
Longitude lon = new Longitude(inValue);
return lon.isValid();
}
}
/**
* Check whether the given String looks like an Altitude value
* @param inValue value from file
* @param inIsHeader true if this is a header line, false for data
* @return true if it could be altitude
*/
private static boolean fieldLooksLikeAltitude(String inValue, boolean inIsHeader)
{
if (inValue == null || inValue.equals("")) {return false;}
if (inIsHeader)
{
// This is a header line so look for english or local text
String upperValue = inValue.toUpperCase();
return (upperValue.equals("ALTITUDE")
|| upperValue.equals("ALT")
|| upperValue.equals("HMSL") // height above mean sea level
|| upperValue.equals(I18nManager.getText("fieldname.altitude").toUpperCase()));
}
else
{
// Look for a number less than 100000
try
{
int intValue = Integer.parseInt(inValue);
return (intValue > 0 && intValue < 100000);
}
catch (NumberFormatException nfe) {}
return false;
}
}
/**
* Check whether the given String looks like a waypoint name
* @param inValue value from file
* @param inIsHeader true if this is a header line, false for data
* @return true if it could be a name
*/
private static boolean fieldLooksLikeName(String inValue, boolean inIsHeader)
{
if (inValue == null || inValue.equals("")) {return false;}
if (inIsHeader)
{
// This is a header line so look for english or local text
String upperValue = inValue.toUpperCase();
return (upperValue.equals("NAME")
|| upperValue.equals("LABEL")
|| upperValue.equals(I18nManager.getText("fieldname.waypointname").toUpperCase()));
}
else
{
// Look for at least two letters in it
int numLetters = 0;
for (int i=0; i<inValue.length(); i++)
{
char currChar = inValue.charAt(i);
if (Character.isLetter(currChar)) {
numLetters++;
}
// Not interested if it contains ":" or "."
if (currChar == ':' || currChar == '.') {return false;}
}
return numLetters >= 2;
}
}
/**
* Check whether the given String looks like a timestamp
* @param inValue value from file
* @param inIsHeader true if this is a header line, false for data
* @return true if it could be a timestamp
*/
private static boolean fieldLooksLikeTimestamp(String inValue, boolean inIsHeader)
{
if (inValue == null || inValue.equals("")) {return false;}
if (inIsHeader)
{
String upperValue = inValue.toUpperCase();
// This is a header line so look for english or local text
return (upperValue.equals("TIMESTAMP")
|| upperValue.equals("TIME")
|| upperValue.equals(I18nManager.getText("fieldname.timestamp").toUpperCase()));
}
else
{
// must be at least 7 characters long
if (inValue.length() < 7) {return false;}
Timestamp stamp = new Timestamp(inValue);
return stamp.isValid();
}
}
/**
* Check whether the given String looks like a track segment field
* @param inValue value from file
* @param inIsHeader true if this is a header line, false for data
* @return true if it could be a track segment
*/
private static boolean fieldLooksLikeSegment(String inValue, boolean inIsHeader)
{
if (inValue == null || inValue.equals("")) {return false;}
if (inIsHeader)
{
String upperValue = inValue.toUpperCase();
// This is a header line so look for english or local text
return upperValue.equals("SEGMENT")
|| upperValue.equals(I18nManager.getText("fieldname.newsegment").toUpperCase());
}
else
{
// can't reliably identify it just using the value
return false;
}
}
/**
* Check whether the given String looks like a waypoint type
* @param inValue value from file
* @param inIsHeader true if this is a header line, false for data
* @return true if it could be a waypoint type
*/
private static boolean fieldLooksLikeWaypointType(String inValue, boolean inIsHeader)
{
if (inValue == null || inValue.equals("")) {return false;}
if (inIsHeader)
{
String upperValue = inValue.toUpperCase();
// This is a header line so look for english or local text
return (upperValue.equals("TYPE")
|| upperValue.equals(I18nManager.getText("fieldname.waypointtype").toUpperCase()));
}
else
{
// can't reliably identify it just using the value
return false;
}
}
}