1 package tim.prune.function.estimate;
3 import java.text.DecimalFormat;
4 import java.text.NumberFormat;
5 import java.util.Locale;
7 import tim.prune.I18nManager;
8 import tim.prune.config.Config;
9 import tim.prune.data.RangeStats;
10 import tim.prune.data.Unit;
11 import tim.prune.data.UnitSetLibrary;
14 * Class to hold, parse and convert the parameters for time estimation.
15 * These are five (metric) values which can be loaded and saved from config,
16 * and are used by the EstimateTime function
18 public class EstimationParameters
20 /** Minutes required for flat travel, fixed metric distance */
21 private double _flatMins = 0.0;
22 /** Minutes required for climbing, fixed metric distance */
23 private double _gentleClimbMins = 0.0, _steepClimbMins;
24 /** Minutes required for descending, fixed metric distance */
25 private double _gentleDescentMins = 0.0, _steepDescentMins;
26 /** True if parsing from a string failed */
27 private boolean _parseFailed = false;
29 /** Kilometres unit for comparison */
30 private static final Unit KILOMETRES = UnitSetLibrary.UNITS_KILOMETRES;
34 * Bare constructor using default values
36 public EstimationParameters()
42 * Constructor from config string
43 * @param inConfigString single, semicolon-separated string from config
45 public EstimationParameters(String inConfigString)
47 populateWithString(inConfigString);
54 * Reset all the values to their hardcoded defaults
56 public void resetToDefaults()
59 _gentleClimbMins = 12.0; _steepClimbMins = 18.0;
60 _gentleDescentMins = 0.0; _steepDescentMins = 12.0;
65 * @return true if this set of parameters is the same as the default set
67 public boolean sameAsDefaults()
69 EstimationParameters defaultParams = new EstimationParameters();
70 return _flatMins == defaultParams._flatMins
71 && _gentleClimbMins == defaultParams._gentleClimbMins
72 && _steepClimbMins == defaultParams._steepClimbMins
73 && _gentleDescentMins == defaultParams._gentleDescentMins
74 && _steepDescentMins == defaultParams._steepDescentMins;
78 * Populate the values from the config, which means all values are metric
79 * @param inString semicolon-separated string of five parameters
81 private void populateWithString(String inString)
83 if (inString != null && !inString.trim().equals(""))
85 String[] params = inString.trim().split(";");
86 _parseFailed = (params == null || params.length != 5);
89 for (String p : params)
91 if (!isParamStringValid(p)) {
100 // Use fixed UK locale to parse these, because of fixed "." formatting
101 NumberFormat twoDpFormatter = NumberFormat.getNumberInstance(Locale.UK);
102 _flatMins = twoDpFormatter.parse(params[0]).doubleValue();
103 _gentleClimbMins = twoDpFormatter.parse(params[1]).doubleValue();
104 _steepClimbMins = twoDpFormatter.parse(params[2]).doubleValue();
105 _gentleDescentMins = twoDpFormatter.parse(params[3]).doubleValue();
106 _steepDescentMins = twoDpFormatter.parse(params[4]).doubleValue();
108 catch (Exception e) {
113 else _parseFailed = true;
117 * Populate the values using five user-entered strings (now Units-specific!)
118 * @param inFlat minutes for flat
119 * @param inGClimb minutes for gentle climb
120 * @param inSClimb minutes for steep climb
121 * @param inGDescent minutes for gentle descent
122 * @param inSDescent minutes for steep descent
124 public void populateWithStrings(String inFlat, String inGClimb, String inSClimb, String inGDescent, String inSDescent)
126 if (isParamStringValid(inFlat) && isParamStringValid(inGClimb) && isParamStringValid(inSClimb)
127 && isParamStringValid(inGDescent) && isParamStringValid(inSDescent))
129 Unit distUnit = Config.getUnitSet().getDistanceUnit();
130 Unit altUnit = Config.getUnitSet().getAltitudeUnit();
131 final double distFactor = (distUnit == KILOMETRES ? 1.0 : (5000/3.0 * distUnit.getMultFactorFromStd()));
132 final double altFactor = (altUnit.isStandard() ? 1.0 : (1.0/3.0 * altUnit.getMultFactorFromStd()));
133 NumberFormat localFormatter = NumberFormat.getNumberInstance();
136 _flatMins = localFormatter.parse(inFlat).doubleValue() * distFactor;
137 _gentleClimbMins = localFormatter.parse(inGClimb).doubleValue() * altFactor;
138 _steepClimbMins = localFormatter.parse(inSClimb).doubleValue() * altFactor;
139 _gentleDescentMins = localFormatter.parse(inGDescent).doubleValue() * altFactor;
140 _steepDescentMins = localFormatter.parse(inSDescent).doubleValue() * altFactor;
142 catch (Exception e) {_parseFailed = true;}
144 else _parseFailed = true;
148 * Populate with double metric values, for example the results of a Learning process
149 * @param inFlat time for 5km on the flat
150 * @param inGClimb time for 100m gentle climb
151 * @param inSClimb time for 100m steep climb
152 * @param inGDescent time for 100m gentle descent
153 * @param inSDescent time for 100m steep descent
155 public void populateWithMetrics(double inFlat, double inGClimb, double inSClimb, double inGDescent, double inSDescent)
158 _gentleClimbMins = inGClimb;
159 _steepClimbMins = inSClimb;
160 _gentleDescentMins = inGDescent;
161 _steepDescentMins = inSDescent;
165 * @param inString parameter string to check
166 * @return true if it looks valid (no letters, at least one digit)
168 private static boolean isParamStringValid(String inString)
170 if (inString == null) {return false;}
171 boolean hasDigit = false, currPunc = false, prevPunc = false;
172 for (int i=0; i<inString.length(); i++)
174 char c = inString.charAt(i);
175 if (Character.isLetter(c)) {return false;} // no letters allowed
176 currPunc = (c == '.' || c == ',');
177 if (currPunc && prevPunc) {return false;} // no consecutive . or , allowed
179 hasDigit = hasDigit || Character.isDigit(c);
181 return hasDigit; // must have at least one digit!
185 * @return true if the parameters are valid, with no parsing errors
187 public boolean isValid()
189 return !_parseFailed; // && _flatMins > 0.0 && _gentleClimbMins >= 0.0 && _steepClimbMins >= 0.0;
193 * @return five strings for putting in text fields for editing / display
195 public String[] getStrings()
197 Unit distUnit = Config.getUnitSet().getDistanceUnit();
198 Unit altUnit = Config.getUnitSet().getAltitudeUnit();
199 double distFactor = (distUnit == KILOMETRES ? 1.0 : (5000/3.0 * distUnit.getMultFactorFromStd()));
200 double altFactor = (altUnit.isStandard() ? 1.0 : (1.0/3.0 * altUnit.getMultFactorFromStd()));
201 // Use locale-specific number formatting, eg commas for german
202 NumberFormat numFormatter = NumberFormat.getNumberInstance();
203 if (numFormatter instanceof DecimalFormat) {
204 ((DecimalFormat) numFormatter).applyPattern("0.00");
206 // Conversion between units
207 return new String[] {
208 numFormatter.format(_flatMins / distFactor),
209 numFormatter.format(_gentleClimbMins / altFactor), numFormatter.format(_steepClimbMins / altFactor),
210 numFormatter.format(_gentleDescentMins / altFactor), numFormatter.format(_steepDescentMins / altFactor)
215 * @return unit-specific string describing the distance for the flat time (5km/3mi/3NM)
217 public static String getStandardDistance()
219 Unit distUnit = Config.getUnitSet().getDistanceUnit();
220 return (distUnit == KILOMETRES ? "5 " : "3 ") + I18nManager.getText(distUnit.getShortnameKey());
224 * @return unit-specific string describing the height difference for the climbs/descents (100m/300ft)
226 public static String getStandardClimb()
228 Unit altUnit = Config.getUnitSet().getAltitudeUnit();
229 return (altUnit.isStandard() ? "100 " : "300 ") + I18nManager.getText(altUnit.getShortnameKey());
233 * @return contents of parameters as a semicolon-separated (metric) string for the config
235 public String toConfigString()
237 return "" + twoDp(_flatMins) + ";" + twoDp(_gentleClimbMins) + ";" + twoDp(_steepClimbMins) + ";"
238 + twoDp(_gentleDescentMins) + ";" + twoDp(_steepDescentMins);
242 * @param inNum number to output
243 * @return formatted string to two decimal places, with decimal point
245 private static String twoDp(double inNum)
247 if (inNum < 0.0) return "-" + twoDp(-inNum);
248 int hundreds = (int) (inNum * 100 + 0.5);
249 return "" + (hundreds / 100) + "." + (hundreds % 100);
253 * Apply the parameters to the given range stats
254 * @param inStats stats of current range
255 * @return estimated number of minutes required
257 public double applyToStats(RangeStats inStats)
259 if (inStats == null || !inStats.isValid()) return 0.0;
260 final Unit METRES = UnitSetLibrary.UNITS_METRES;
261 final double STANDARD_CLIMB = 100.0; // metres
262 final double STANDARD_DISTANCE = 5.0; // kilometres
263 return _flatMins * inStats.getMovingDistanceKilometres() / STANDARD_DISTANCE
264 + _gentleClimbMins * inStats.getGentleAltitudeRange().getClimb(METRES) / STANDARD_CLIMB
265 + _steepClimbMins * inStats.getSteepAltitudeRange().getClimb(METRES) / STANDARD_CLIMB
266 + _gentleDescentMins * inStats.getGentleAltitudeRange().getDescent(METRES) / STANDARD_CLIMB
267 + _steepDescentMins * inStats.getSteepAltitudeRange().getDescent(METRES) / STANDARD_CLIMB;
271 * Combine two sets of parameters together
272 * @param inOther other set
273 * @param inFraction fractional weight
274 * @return combined set
276 public EstimationParameters combine(EstimationParameters inOther, double inFraction)
278 if (inFraction < 0.0 || inFraction > 1.0 || inOther == null) {
281 // inFraction is the weight of this one, weight of the other one is 1-inFraction
282 final double fraction2 = 1 - inFraction;
283 EstimationParameters combined = new EstimationParameters();
284 combined._flatMins = inFraction * _flatMins + fraction2 * inOther._flatMins;
285 combined._gentleClimbMins = inFraction * _gentleClimbMins + fraction2 * inOther._gentleClimbMins;
286 combined._gentleDescentMins = inFraction * _gentleDescentMins + fraction2 * inOther._gentleDescentMins;
287 combined._steepClimbMins = inFraction * _steepClimbMins + fraction2 * inOther._steepClimbMins;
288 combined._steepDescentMins = inFraction * _steepDescentMins + fraction2 * inOther._steepDescentMins;