]> gitweb.fperrin.net Git - GpsPrune.git/blob - src/tim/prune/function/estimate/EstimationParameters.java
Version 20.4, May 2021
[GpsPrune.git] / src / tim / prune / function / estimate / EstimationParameters.java
1 package tim.prune.function.estimate;
2
3 import java.text.DecimalFormat;
4 import java.text.NumberFormat;
5 import java.util.Locale;
6
7 import tim.prune.I18nManager;
8 import tim.prune.config.Config;
9 import tim.prune.data.RangeStatsWithGradients;
10 import tim.prune.data.Unit;
11 import tim.prune.data.UnitSetLibrary;
12
13 /**
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
17  */
18 public class EstimationParameters
19 {
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;
28
29         /** Kilometres unit for comparison */
30         private static final Unit KILOMETRES = UnitSetLibrary.UNITS_KILOMETRES;
31
32
33         /**
34          * Bare constructor using default values
35          */
36         public EstimationParameters()
37         {
38                 resetToDefaults();
39         }
40
41         /**
42          * Constructor from config string
43          * @param inConfigString single, semicolon-separated string from config
44          */
45         public EstimationParameters(String inConfigString)
46         {
47                 populateWithString(inConfigString);
48                 if (_parseFailed) {
49                         resetToDefaults();
50                 }
51         }
52
53         /**
54          * Reset all the values to their hardcoded defaults
55          */
56         public void resetToDefaults()
57         {
58                 _flatMins = 60.0;
59                 _gentleClimbMins = 12.0; _steepClimbMins = 18.0;
60                 _gentleDescentMins = 0.0; _steepDescentMins = 12.0;
61                 _parseFailed = false;
62         }
63
64         /**
65          * @return true if this set of parameters is the same as the default set
66          */
67         public boolean sameAsDefaults()
68         {
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;
75         }
76
77         /**
78          * Populate the values from the config, which means all values are metric
79          * @param inString semicolon-separated string of five parameters
80          */
81         private void populateWithString(String inString)
82         {
83                 if (inString != null && !inString.trim().equals(""))
84                 {
85                         String[] params = inString.trim().split(";");
86                         _parseFailed = (params == null || params.length != 5);
87                         if (!_parseFailed)
88                         {
89                                 for (String p : params)
90                                 {
91                                         if (!isParamStringValid(p)) {
92                                                 _parseFailed = true;
93                                         }
94                                 }
95                         }
96                         if (!_parseFailed)
97                         {
98                                 try
99                                 {
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();
107                                 }
108                                 catch (Exception e) {
109                                         _parseFailed = true;
110                                 }
111                         }
112                 }
113                 else _parseFailed = true;
114         }
115
116         /**
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
123          */
124         public void populateWithStrings(String inFlat, String inGClimb, String inSClimb, String inGDescent, String inSDescent)
125         {
126                 if (isParamStringValid(inFlat) && isParamStringValid(inGClimb) && isParamStringValid(inSClimb)
127                         && isParamStringValid(inGDescent) && isParamStringValid(inSDescent))
128                 {
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();
134                         try
135                         {
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;
141                         }
142                         catch (Exception e) {_parseFailed = true;}
143                 }
144                 else _parseFailed = true;
145         }
146
147         /**
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
154          */
155         public void populateWithMetrics(double inFlat, double inGClimb, double inSClimb, double inGDescent, double inSDescent)
156         {
157                 _flatMins = inFlat;
158                 _gentleClimbMins = inGClimb;
159                 _steepClimbMins  = inSClimb;
160                 _gentleDescentMins = inGDescent;
161                 _steepDescentMins  = inSDescent;
162         }
163
164         /**
165          * @param inString parameter string to check
166          * @return true if it looks valid (no letters, at least one digit)
167          */
168         private static boolean isParamStringValid(String inString)
169         {
170                 if (inString == null) {return false;}
171                 boolean hasDigit = false, currPunc = false, prevPunc = false;
172                 for (int i=0; i<inString.length(); i++)
173                 {
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
178                         prevPunc = currPunc;
179                         hasDigit = hasDigit || Character.isDigit(c);
180                 }
181                 return hasDigit; // must have at least one digit!
182         }
183
184         /**
185          * @return true if the parameters are valid, with no parsing errors
186          */
187         public boolean isValid()
188         {
189                 return !_parseFailed; // && _flatMins > 0.0 && _gentleClimbMins >= 0.0 && _steepClimbMins >= 0.0;
190         }
191
192         /**
193          * @return five strings for putting in text fields for editing / display
194          */
195         public String[] getStrings()
196         {
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");
205                 }
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)
211                 };
212         }
213
214         /**
215          * @return unit-specific string describing the distance for the flat time (5km/3mi/3NM)
216          */
217         public static String getStandardDistance()
218         {
219                 Unit distUnit = Config.getUnitSet().getDistanceUnit();
220                 return (distUnit == KILOMETRES ? "5 " : "3 ") + I18nManager.getText(distUnit.getShortnameKey());
221         }
222
223         /**
224          * @return unit-specific string describing the height difference for the climbs/descents (100m/300ft)
225          */
226         public static String getStandardClimb()
227         {
228                 Unit altUnit  = Config.getUnitSet().getAltitudeUnit();
229                 return (altUnit.isStandard() ? "100 " : "300 ") + I18nManager.getText(altUnit.getShortnameKey());
230         }
231
232         /**
233          * @return contents of parameters as a semicolon-separated (metric) string for the config
234          */
235         public String toConfigString()
236         {
237                 return "" + twoDp(_flatMins) + ";" + twoDp(_gentleClimbMins) + ";" + twoDp(_steepClimbMins) + ";"
238                         + twoDp(_gentleDescentMins) + ";" + twoDp(_steepDescentMins);
239         }
240
241         /**
242          * @param inNum number to output
243          * @return formatted string to two decimal places, with decimal point
244          */
245         private static String twoDp(double inNum)
246         {
247                 if (inNum < 0.0) return "-" + twoDp(-inNum);
248                 int hundreds = (int) (inNum * 100 + 0.5);
249                 return "" + (hundreds / 100) + "." + (hundreds % 100);
250         }
251
252         /**
253          * Apply the parameters to the given range stats
254          * @param inStats stats of current range
255          * @return estimated number of minutes required
256          */
257         public double applyToStats(RangeStatsWithGradients inStats)
258         {
259                 if (inStats == null) {
260                         return 0.0;
261                 }
262                 final Unit METRES = UnitSetLibrary.UNITS_METRES;
263                 final double STANDARD_CLIMB = 100.0; // metres
264                 final double STANDARD_DISTANCE = 5.0; // kilometres
265                 return _flatMins * inStats.getMovingDistanceKilometres() / STANDARD_DISTANCE
266                         + _gentleClimbMins * inStats.getGentleAltitudeRange().getClimb(METRES) / STANDARD_CLIMB
267                         + _steepClimbMins  * inStats.getSteepAltitudeRange().getClimb(METRES) / STANDARD_CLIMB
268                         + _gentleDescentMins * inStats.getGentleAltitudeRange().getDescent(METRES) / STANDARD_CLIMB
269                         + _steepDescentMins  * inStats.getSteepAltitudeRange().getDescent(METRES) / STANDARD_CLIMB;
270         }
271
272         /**
273          * Combine two sets of parameters together
274          * @param inOther other set
275          * @param inFraction fractional weight
276          * @return combined set
277          */
278         public EstimationParameters combine(EstimationParameters inOther, double inFraction)
279         {
280                 if (inFraction < 0.0 || inFraction > 1.0 || inOther == null) {
281                         return null;
282                 }
283                 // inFraction is the weight of this one, weight of the other one is 1-inFraction
284                 final double fraction2 = 1 - inFraction;
285                 EstimationParameters combined = new EstimationParameters();
286                 combined._flatMins = inFraction * _flatMins + fraction2 * inOther._flatMins;
287                 combined._gentleClimbMins = inFraction * _gentleClimbMins + fraction2 * inOther._gentleClimbMins;
288                 combined._gentleDescentMins = inFraction * _gentleDescentMins + fraction2 * inOther._gentleDescentMins;
289                 combined._steepClimbMins = inFraction * _steepClimbMins + fraction2 * inOther._steepClimbMins;
290                 combined._steepDescentMins = inFraction * _steepDescentMins + fraction2 * inOther._steepDescentMins;
291                 return combined;
292         }
293 }