Bump version
[munter.git] / munter / munter.py
1 # -*- coding: utf-8 -*-
2
3
4 """
5 Munter Time Calculation
6 Alexander Vasarab
7 Wylark Mountaineering LLC
8
9 A rudimentary program which implements the Munter time calculation.
10 """
11
12 import sys
13 import argparse
14
15 from . import __progname__ as progname
16 from . import __version__ as version
17
18 class InvalidUnitsException(Exception):
19 """Exception class for when invalid units are specified"""
20
21 RATES = {
22 'uphill': {'rate': 4, 'direction': '↑'},
23 'flat': {'rate': 6, 'direction': '→'}, # or downhill on foot
24 'downhill': {'rate': 10, 'direction': '↓'},
25 'bushwhacking': {'rate': 2, 'direction': '↹'},
26 }
27
28 FITNESSES = {
29 'slow': 1.2,
30 'average': 1,
31 'fast': .7,
32 }
33
34 UNIT_CHOICES = ['metric', 'imperial']
35 TRAVEL_MODE_CHOICES = RATES.keys()
36 FITNESS_CHOICES = FITNESSES.keys()
37
38 def time_calc(distance, elevation, fitness='average', rate='uphill',
39 units='imperial'):
40 """
41 the heart of the program, the Munter time calculation implementation
42 """
43 retval = {}
44
45 if units not in UNIT_CHOICES:
46 raise InvalidUnitsException
47
48 unit_count = 0
49
50 if units == 'imperial':
51 # convert to metric
52 distance = (distance * 1.609) # mi to km
53 elevation = (elevation * .305) # ft to m
54
55 unit_count = distance + (elevation / 100.0)
56
57 retval['time'] = (distance + (elevation / 100.0)) / RATES[rate]['rate']
58 retval['time'] = retval['time'] * FITNESSES[fitness]
59
60 retval['unit_count'] = unit_count
61 retval['direction'] = RATES[rate]['direction']
62 retval['pace'] = RATES[rate]['rate']
63
64 return retval
65
66 def print_ugly_estimate(est):
67 """plain-jane string containing result"""
68 hours = int(est['time'])
69 minutes = int((est['time'] - hours) * 60)
70 print("{human_time}".format(
71 human_time="{hours} hours {minutes} minutes".format(
72 hours=hours, minutes=minutes)))
73
74 def print_pretty_estimate(est):
75 """more elaborate, console-based 'GUI' displaying result"""
76 hours = int(est['time'])
77 minutes = int((est['time'] - hours) * 60)
78
79 # NOTE: Below, the line with the unicode up arrow uses an alignment
80 # value of 31. In the future, consider using e.g. wcwidth
81 # library so that this is more elegant.
82 print("\n\t╒═══════════════════════════════╕")
83 print("\t╎▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒╎╮")
84 print("\t╎▒{:^29}▒╎│".format(''))
85 print("\t╎▒{pace_readable:^31}▒╎│".format(
86 pace_readable="{units} {direction} @ {pace}".format(
87 units=round(est['unit_count']),
88 direction=est['direction'],
89 pace=est['pace'])))
90 print("\t╎▒{human_time:^29}▒╎│".format(
91 human_time="{hours} hours {minutes} minutes".format(
92 hours=hours,
93 minutes=minutes)))
94 print("\t╎▒{:^29}▒╎│".format(''))
95 print("\t╎▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒╎│")
96 print("\t╘═══════════════════════════════╛│")
97 print("\t └───────────────────────────────┘\n")
98
99 def get_parser():
100 """return ArgumentParser for this program"""
101 parser = argparse.ArgumentParser(description='Implementation of '
102 'the Munter time calculation')
103
104 # No required args anymore, since -g overrides any requirement
105 parser.add_argument('--distance',
106 '-d',
107 default=0.0,
108 type=float,
109 required=False,
110 help='Distance (in km, by default)')
111
112 parser.add_argument('--elevation',
113 '-e',
114 default=0.0,
115 type=float,
116 required=False,
117 help='Elevation change (in m, by default)')
118
119 parser.add_argument('--travel-mode',
120 '-t',
121 type=str,
122 default='uphill',
123 choices=TRAVEL_MODE_CHOICES, required=False,
124 help='Travel mode (uphill, by default)')
125
126 parser.add_argument('--fitness',
127 '-f',
128 type=str,
129 default='average',
130 choices=FITNESS_CHOICES, required=False,
131 help='Fitness modifier (average, by default)')
132
133 parser.add_argument('--units',
134 '-u',
135 type=str,
136 default='imperial',
137 required=False,
138 choices=UNIT_CHOICES,
139 help='Units of input values')
140
141 parser.add_argument('--pretty',
142 '-p',
143 action='store_true',
144 default=False,
145 required=False,
146 help='Make output pretty')
147
148 parser.add_argument('--gui',
149 '-g',
150 action='store_true',
151 default=False,
152 required=False,
153 help='Launch GUI mode (overrides --pretty)')
154
155 parser.add_argument('--version',
156 '-v',
157 action='store_true',
158 default=False,
159 required=False,
160 help='Print version and exit')
161
162 return parser
163
164 def main():
165 """main routine: sort through args, decide what to do"""
166 parser = get_parser()
167 opts = parser.parse_args()
168
169 distance = opts.distance
170 elevation = opts.elevation
171 fitness = opts.fitness
172 units = opts.units
173 travel_mode = opts.travel_mode
174 pretty = opts.pretty
175 gui = opts.gui
176 get_version = opts.version
177
178 if get_version:
179 print("%s - v%s" % (progname, version))
180 return 0
181
182 time_estimate = time_calc(distance=distance, elevation=elevation,
183 fitness=fitness, rate=travel_mode,
184 units=units)
185
186 # auto-start in GUI mode if the program is not invoked from terminal
187 if len(sys.argv) == 1 and not sys.stdin.isatty():
188 gui = True
189
190 if gui:
191 from . import gui
192 gui.startup()
193 else:
194 if pretty:
195 print_pretty_estimate(time_estimate)
196 else:
197 print_ugly_estimate(time_estimate)
198
199 return 0
200
201 if __name__ == "__main__":
202 sys.exit(main())