Merge branch 'gui' v2.2.0
authorAlexander Vasarab <alexander@wylark.com>
Mon, 29 Jun 2020 18:30:56 +0000 (11:30 -0700)
committerAlexander Vasarab <alexander@wylark.com>
Mon, 29 Jun 2020 18:30:56 +0000 (11:30 -0700)
README.md
munter/__init__.py
munter/gui.py [new file with mode: 0644]
munter/munter.py
setup.py

index 37c034c6c4b37a8c84e60d75e63935a001b5e2cf..0e03ac1b06a7d49218f6f79e4438e1b2459c71e9 100644 (file)
--- a/README.md
+++ b/README.md
@@ -34,6 +34,11 @@ By default, the output will be the time in hours and minutes of the
 specified leg. If you prefer, you can use the `-p` switch to get a
 "prettier" output.
 
+There is also a GUI mode available, based on WxPython, which can be used
+by simply invoking like so:
+
+`./munter.py -g`
+
 ### Use as a library
 
 You can also use Munter.py programmatically from Python, like so:
@@ -46,25 +51,28 @@ This will store a value like "3.64914" in the `est` variable.
 Workflow
 --------
 
-My workflow involves planning my tour using tools like ArcGIS or CalTopo. Then,
-I take the stats between each leg (distance, vertical gain/loss) of the tour
-and run them through Munter.py and record its output to my field
-notebook.
+My workflow involves planning my tour using tools like ArcGIS or
+CalTopo. Then, I take the stats between each leg (distance, vertical
+gain/loss) of the tour and run them through Munter.py and record its
+output to my field notebook.
 
-The rudimentary "GUI" can be directly transferred to e.g. the format used by
-SnowPit Technologies' "Avalanche Field Notebook" or your own personal format
-(e.g. RitR No. 471).
+The text-based "pretty" format can be directly transferred to e.g. the
+format used by SnowPit Technologies' "Avalanche Field Notebook" or your
+own personal format (e.g. RitR No. 471).
 
 Future plans
 ------------
 
-* GTK mode
 * Lint it (e.g. therapist)
 * Sphinx/autodoc?
 
 Version History
 ---------------
 
+- 2.2.0 (Jun 2020)
+
+  Implement GUI mode.
+
 - 2.1.0 (Jun 2020)
 
   Implement fitness modifier. Make some text changes and other
index 4393d40d0a73ab979cbbca6acb8587c662c6004b..ddb0600a947511f4c9743f6f1f3d669058e5d4d7 100755 (executable)
@@ -1,3 +1,4 @@
 """Main package for Munter.py"""
-__version__ = "2.1.0"
+__version__ = '2.2.0'
+__progname__ = 'Munter.py'
 from .munter import time_calc
diff --git a/munter/gui.py b/munter/gui.py
new file mode 100644 (file)
index 0000000..87ccd08
--- /dev/null
@@ -0,0 +1,230 @@
+# -*- coding: utf-8 -*-
+
+
+"""
+GUI implementation
+"""
+
+import wx
+from . import munter
+from . import __progname__ as progname
+from . import __version__ as version
+
+class MainFrame(wx.Frame):
+    """
+    The main wx.Frame
+    """
+
+    def __init__(self, *args, **kw):
+        super(MainFrame, self).__init__(*args, **kw)
+
+        self.SetTitle(progname)
+        self.SetSize(600, 400)
+
+        self.props = self.init_props()
+
+        self.pnl = wx.Panel(self)
+
+        st = wx.StaticText(self.pnl, label=progname)
+        font = st.GetFont()
+        font.PointSize += 10
+        font = font.Bold()
+
+        st.SetFont(font)
+
+        sizer = wx.BoxSizer(wx.HORIZONTAL)
+        sizer.Add(st, wx.SizerFlags().Border(wx.TOP|wx.LEFT, 0))
+
+        # text entry fields
+        self.st_distance = wx.StaticText(self.pnl, label="Distance: ", style=wx.ALIGN_RIGHT)
+        self.te_distance = wx.TextCtrl(self.pnl, value="0", size=(140, -1))
+
+        self.st_elevation = wx.StaticText(self.pnl, label="Elevation: ", style=wx.ALIGN_RIGHT)
+        self.te_elevation = wx.TextCtrl(self.pnl, value="0", size=(140, -1))
+
+        self.st_fitness   = wx.StaticText(self.pnl, label="Fitness: ", style=wx.ALIGN_RIGHT)
+        rb_fitness_choices = ['slow', 'average', 'fast']
+        rb_fitness_default = 'average'
+        self.rb_fitness   = wx.ComboBox(self.pnl, choices=rb_fitness_choices,
+                value=rb_fitness_default, style=wx.CB_READONLY)
+
+        self.st_travel_mode    = wx.StaticText(self.pnl, label="Travel Mode: ", style=wx.ALIGN_RIGHT)
+        rb_travel_mode_choices = ['uphill', 'flat', 'downhill', 'bushwhacking']
+        rb_travel_mode_default = 'uphill'
+        self.rb_travel_mode    = wx.ComboBox(self.pnl,
+            choices=rb_travel_mode_choices,
+            value=rb_travel_mode_default, style=wx.CB_READONLY)
+
+        self.st_units   = wx.StaticText(self.pnl, label="Units: ", style=wx.ALIGN_RIGHT)
+        rb_units_choices = ['imperial', 'metric']
+        rb_units_default = 'imperial'
+
+        self.rb_units = []
+        for choice in range(len(rb_units_choices)):
+            label = rb_units_choices[choice]
+            style = wx.RB_GROUP if not choice else 0
+            self.rb_units.append(wx.RadioButton(self.pnl, label=label, style=style))
+
+        # static text
+        self.st_mtc = wx.StaticText(self.pnl, label="",
+                style=wx.ALIGN_CENTRE_HORIZONTAL)
+
+        st_mtc_font = st.GetFont()
+        st_mtc_font.PointSize += 10
+        self.st_mtc.SetFont(st_mtc_font)
+
+        # buttons
+        self.b_reset = wx.Button(self.pnl, wx.NewId(), '&Reset', (-1, -1),
+                wx.DefaultSize)
+
+        # bindings
+        self.pnl.Bind(wx.EVT_TEXT, self.update_distance, self.te_distance)
+        self.pnl.Bind(wx.EVT_TEXT, self.update_elevation, self.te_elevation)
+        self.rb_fitness.Bind(wx.EVT_COMBOBOX, self.update_fitness)
+        self.rb_travel_mode.Bind(wx.EVT_COMBOBOX, self.update_travel_mode)
+        self.b_reset.Bind(wx.EVT_BUTTON, self.reset)
+
+        for cb in self.rb_units:
+            cb.Bind(wx.EVT_RADIOBUTTON, self.update_units)
+
+        # layout
+        b = 5
+        w = 100
+
+        static_line = wx.StaticLine(self.pnl, wx.NewId(), style=wx.LI_HORIZONTAL)
+
+        hsizer_distance = wx.BoxSizer(wx.HORIZONTAL)
+        hsizer_distance.Add(self.st_distance, 0, wx.RIGHT, b)
+        hsizer_distance.Add(self.te_distance, 1, wx.GROW, b)
+        hsizer_distance.SetItemMinSize(self.st_distance, (w, -1))
+
+        hsizer_elevation = wx.BoxSizer(wx.HORIZONTAL)
+        hsizer_elevation.Add(self.st_elevation, 0, wx.RIGHT, b)
+        hsizer_elevation.Add(self.te_elevation, 1, wx.GROW, b)
+        hsizer_elevation.SetItemMinSize(self.st_elevation, (w, -1))
+
+        hsizer_fitness = wx.BoxSizer(wx.HORIZONTAL)
+        hsizer_fitness.Add(self.st_fitness, 0, wx.RIGHT, b)
+        hsizer_fitness.Add(self.rb_fitness, 1, wx.GROW, b)
+        hsizer_fitness.SetItemMinSize(self.st_fitness, (w, -1))
+
+        hsizer_travel_mode = wx.BoxSizer(wx.HORIZONTAL)
+        hsizer_travel_mode.Add(self.st_travel_mode, 0, wx.RIGHT, b)
+        hsizer_travel_mode.Add(self.rb_travel_mode, 1, wx.GROW, b)
+        hsizer_travel_mode.SetItemMinSize(self.st_travel_mode, (w, -1))
+
+        hsizer_units = wx.BoxSizer(wx.HORIZONTAL)
+        hsizer_units.Add(self.st_units, 0, wx.RIGHT, b)
+        for cb in range(len(self.rb_units)):
+            hsizer_units.Add(self.rb_units[cb], cb + 1, wx.GROW, b)
+        hsizer_units.SetItemMinSize(self.st_units, (w, -1))
+
+        hsizer_mtc = wx.BoxSizer(wx.HORIZONTAL)
+        hsizer_mtc.Add(self.st_mtc, 1, wx.GROW, b)
+        hsizer_mtc.SetItemMinSize(self.st_mtc, (w, -1))
+
+        hsizer5 = wx.BoxSizer(wx.HORIZONTAL)
+        hsizer5.Add(self.b_reset, 0)
+
+        b = 5
+        vsizer1 = wx.BoxSizer(wx.VERTICAL)
+        vsizer1.Add(sizer, 0, wx.EXPAND | wx.ALL, b*b)
+        vsizer1.Add(hsizer_distance, 0, wx.EXPAND | wx.ALL, b)
+        vsizer1.Add(hsizer_elevation, 0, wx.EXPAND | wx.ALL, b)
+        vsizer1.Add(hsizer_fitness, 0, wx.EXPAND | wx.ALL, b)
+        vsizer1.Add(hsizer_travel_mode, 0, wx.EXPAND | wx.ALL, b)
+        vsizer1.Add(hsizer_units, 0, wx.EXPAND | wx.ALL, b)
+        vsizer1.Add(hsizer_mtc, 0, wx.EXPAND | wx.ALL, b)
+        vsizer1.Add(static_line, 0, wx.GROW | wx.ALL, b)
+        vsizer1.Add(hsizer5, 0, wx.ALIGN_RIGHT | wx.ALL, b)
+
+        self.pnl.SetSizerAndFit(vsizer1)
+        self.pnl.SetClientSize(vsizer1.GetSize())
+        self.update_mtc()
+
+    def init_props(self):
+        props = dict()
+        props['distance'] = 0
+        props['elevation'] = 0
+        props['fitness'] = 'average'
+        props['units'] = 'imperial'
+        props['travel_mode'] = 'uphill'
+        return props
+
+    def update_distance(self, event):
+        value = self.te_distance.GetValue()
+        if value:
+            try:
+                new_val = float(value)
+                self.props['distance'] = new_val
+            except:
+                # reset GUI to last-accepted val
+                self.te_distance.SetValue(str(self.props['distance']))
+                pass
+        self.update_mtc()
+
+    def update_elevation(self, event):
+        value = self.te_elevation.GetValue()
+        if value:
+            try:
+                new_val = int(value)
+                self.props['elevation'] = new_val
+            except:
+                # reset GUI to last-accepted val
+                self.te_elevation.SetValue(str(self.props['elevation']))
+                pass
+        self.update_mtc()
+
+    def update_fitness(self, event):
+        value = self.rb_fitness.GetValue()
+        if value:
+            self.props['fitness'] = value
+        self.update_mtc()
+
+    def update_travel_mode(self, event):
+        value = self.rb_travel_mode.GetValue()
+        if value:
+            self.props['travel_mode'] = value
+        self.update_mtc()
+
+    def update_units(self, event):
+        rb = event.GetEventObject()
+        value = rb.GetLabel()
+        if value:
+            self.props['units'] = value
+        self.update_mtc()
+
+    def update_mtc(self):
+        if (self.props['distance'] is None) or (self.props['elevation'] is None):
+            return
+
+        est = munter.time_calc(self.props['distance'],
+            self.props['elevation'],
+            self.props['fitness'],
+            self.props['travel_mode'],
+            self.props['units'])
+
+        hours = int(est['time'])
+        minutes = int((est['time'] - hours) * 60)
+        self.st_mtc.SetLabel("{human_time}".format(
+                human_time="{hours} hours {minutes} minutes".format(
+                    hours=hours,
+                    minutes=minutes)))
+
+        self.pnl.Layout()
+
+    def reset(self, event):
+        self.props = self.init_props()
+        self.te_distance.SetValue(str(self.props['distance']))
+        self.te_elevation.SetValue(str(self.props['elevation']))
+        self.rb_fitness.SetValue(str(self.props['fitness']))
+        self.rb_travel_mode.SetValue(str(self.props['travel_mode']))
+        # leave units as the user selected
+        self.update_mtc()
+
+def startup():
+    app = wx.App()
+    frm = MainFrame(None)
+    frm.Show()
+
+    app.MainLoop()
index 98669c56516c60228640881b321efb7d8700e010..d8683491f2bd899aa08e1baea82697c935ca547a 100644 (file)
@@ -12,6 +12,9 @@ A rudimentary program which implements the Munter time calculation.
 import sys
 import argparse
 
+from . import __progname__ as progname
+from . import __version__ as version
+
 class InvalidUnitsException(Exception):
     pass
 
@@ -93,16 +96,19 @@ def get_parser():
     parser = argparse.ArgumentParser(description='Implementation of '
         'the Munter time calculation')
 
+    # No required args anymore, since -g overrides any requirement
     parser.add_argument('--distance',
         '-d',
+        default=0.0,
         type=float,
-        required=True,
+        required=False,
         help='Distance (in km, by default)')
 
     parser.add_argument('--elevation',
         '-e',
+        default=0.0,
         type=float,
-        required=True,
+        required=False,
         help='Elevation change (in m, by default)')
 
     parser.add_argument('--travel-mode',
@@ -134,6 +140,20 @@ def get_parser():
         required=False,
         help="Make output pretty");
 
+    parser.add_argument('--gui',
+        '-g',
+        action='store_true',
+        default=False,
+        required=False,
+        help='Launch GUI mode (overrides --pretty)')
+
+    parser.add_argument('--version',
+        '-v',
+        action='store_true',
+        default=False,
+        required=False,
+        help='Print version and exit')
+
     return parser
 
 def main():
@@ -145,14 +165,25 @@ def main():
     fitness = opts.fitness
     units = opts.units
     travel_mode = opts.travel_mode
+    pretty = opts.pretty
+    gui = opts.gui
+    get_version = opts.version
+
+    if get_version:
+        print("%s - v%s" % (progname, version))
+        return 0
 
     time_estimate = time_calc(distance=distance, elevation=elevation,
         fitness=fitness, rate=travel_mode, units=units)
 
-    if opts.pretty:
-        print_pretty_estimate(time_estimate)
+    if gui:
+        from . import gui
+        gui.startup()
     else:
-        print_ugly_estimate(time_estimate)
+        if pretty:
+            print_pretty_estimate(time_estimate)
+        else:
+            print_ugly_estimate(time_estimate)
 
     return 0
 
index ac9ff306dec17742c579338971e0625945f949db..6d960ec0871379548ebed3e266d57cff7b565f03 100644 (file)
--- a/setup.py
+++ b/setup.py
@@ -6,7 +6,7 @@ from setuptools import setup
 
 cur_dir = os.path.dirname(__file__)
 version = re.search(
-    '^__version__\s*=\s*"(.*)"',
+    "^__version__\s*=\s*'(.*)'",
     open(os.path.join(cur_dir, 'munter/__init__.py')).read(),
     re.M
     ).group(1)