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; } } }