source: OpenWorkouts-current/ow/utilities.py @ 2f8a48f

currentfeature/docs
Last change on this file since 2f8a48f was 2f8a48f, checked in by borja <borja@…>, 5 years ago

(#7) Added several methods to the User model to gather some stats (yearly,

monthly, weekly).

Added two new utilities:

  • timedelta_to_hms (so we can print timedelta objects properly in template code)
  • get_week_days (returns a list of datetime objects for the days in the same week as a given day)

Added a template_helpers module, containing code that affects template
rendering.

Added timedelta_to_hms as a global to the default template rendering context

Refactored some code in the Workout model so it uses timedelta_to_hms instead
of running the same code twice.

  • Property mode set to 100644
File size: 7.0 KB
Line 
1import re
2import os
3import logging
4import subprocess
5from datetime import datetime, timedelta
6from decimal import Decimal
7from shutil import copyfileobj
8
9from unidecode import unidecode
10from xml.dom import minidom
11from ZODB.blob import Blob
12
13log = logging.getLogger(__name__)
14
15
16def slugify(text, delim=u'-'):
17    """
18    Generates an ASCII-only slug.
19    from http://flask.pocoo.org/snippets/5/
20    """
21    _punct_re = re.compile(r'[\t !"#$%&\'()*\-/<=>?@\[\\\]^_`{|},.]+')
22    result = []
23    text = unidecode(text)
24    for word in _punct_re.split(text.lower()):
25        result.extend(word.split())
26    return delim.join(result)
27
28
29class GPXMinidomParser(object):
30    """
31    GPX parser, using minidom from the base library.
32
33    We need this as a workaround, as gpxpy does not handle GPX 1.1 extensions
34    correctly right now (and we have not been able to fix it).
35
36    This method is inspired by this blog post:
37
38    http://castfortwo.blogspot.com.au/2014/06/
39    parsing-strava-gpx-file-with-python.html
40    """
41
42    def __init__(self, gpx_path):
43        self.gpx_path = gpx_path
44        self.gpx = None
45        self.tracks = {}
46
47    def load_gpx(self):
48        """
49        Load the given gpx file into a minidom doc, normalize it and set
50        self.gpx to the document root so we can reuse it later on
51        """
52        doc = minidom.parse(self.gpx_path)
53        doc.normalize()
54        self.gpx = doc.documentElement
55
56    def parse_tracks(self):
57        """
58        Loop over all the tracks found in the gpx, parsing them
59        """
60        for trk in self.gpx.getElementsByTagName('trk'):
61            self.parse_track(trk)
62
63    def parse_track(self, trk):
64        """
65        Parse the given track, extracting all the information and putting it
66        into a dict where the key is the track name and the value is a list
67        of data for the the different segments and points in the track.
68
69        All the data is saved in self.tracks
70        """
71        name = trk.getElementsByTagName('name')[0].firstChild.data
72        if name not in self.tracks:
73            self.tracks[name] = []
74
75        for trkseg in trk.getElementsByTagName('trkseg'):
76            for trkpt in trkseg.getElementsByTagName('trkpt'):
77                lat = Decimal(trkpt.getAttribute('lat'))
78                lon = Decimal(trkpt.getAttribute('lon'))
79
80                # There could happen there is no elevation data
81                ele = trkpt.getElementsByTagName('ele')
82                if ele:
83                    ele = Decimal(ele[0].firstChild.data)
84                else:
85                    ele = None
86
87                rfc3339 = trkpt.getElementsByTagName('time')[0].firstChild.data
88                try:
89                    t = datetime.strptime(
90                        rfc3339, '%Y-%m-%dT%H:%M:%S.%fZ')
91                except ValueError:
92                    t = datetime.strptime(
93                        rfc3339, '%Y-%m-%dT%H:%M:%SZ')
94
95                hr = None
96                cad = None
97                atemp = None
98                extensions = trkpt.getElementsByTagName('extensions')
99                if extensions:
100                    extensions = extensions[0]
101                    trkPtExt = extensions.getElementsByTagName(
102                        'gpxtpx:TrackPointExtension')[0]
103                    if trkPtExt:
104                        hr_ext = trkPtExt.getElementsByTagName('gpxtpx:hr')
105                        cad_ext = trkPtExt.getElementsByTagName('gpxtpx:cad')
106                        atemp_ext = trkPtExt.getElementsByTagName(
107                            'gpxtpx:atemp')
108                        if hr_ext:
109                            hr = Decimal(hr_ext[0].firstChild.data)
110                        if cad_ext:
111                            cad = Decimal(cad_ext[0].firstChild.data)
112                        if atemp_ext:
113                            atemp = Decimal(atemp_ext[0].firstChild.data)
114
115                self.tracks[name].append({
116                    'lat': lat,
117                    'lon': lon,
118                    'ele': ele,
119                    'time': t,
120                    'hr': hr,
121                    'cad': cad,
122                    'atemp': atemp})
123
124
125def semicircles_to_degrees(semicircles):
126    return semicircles * (180 / pow(2, 31))
127
128
129def degrees_to_semicircles(degrees):
130    return degrees * (pow(2, 31) / 180)
131
132
133def miles_to_kms(miles):
134    factor = 0.62137119
135    return miles / factor
136
137
138def kms_to_miles(kms):
139    factor = 0.62137119
140    return kms * factor
141
142
143def meters_to_kms(meters):
144    return meters / 1000
145
146
147def kms_to_meters(kms):
148    return kms * 1000
149
150
151def mps_to_kmph(mps):
152    """
153    Transform a value from meters-per-second to kilometers-per-hour
154    """
155    return mps * 3.6
156
157
158def kmph_to_mps(kmph):
159    """
160    Transform a value from kilometers-per-hour to meters-per-second
161    """
162    return kmph * 0.277778
163
164
165def copy_blob(blob):
166    """
167    Create a copy of a blob object, returning another blob object that is
168    the copy of the given blob file.
169    """
170    new_blob = Blob()
171    if getattr(blob, 'file_extension', None):
172        new_blob.file_extension = blob.file_extension
173    with blob.open('r') as orig_blob, new_blob.open('w') as dest_blob:
174        orig_blob.seek(0)
175        copyfileobj(orig_blob, dest_blob)
176    return new_blob
177
178
179def create_blob(data, file_extension, binary=False):
180    """
181    Create a ZODB blob file from some data, return the blob object
182    """
183    blob = Blob()
184    blob.file_extension = file_extension
185    with blob.open('w') as open_blob:
186        # use .encode() to convert the string to bytes if needed
187        if not binary and not isinstance(data, bytes):
188            data = data.encode('utf-8')
189        open_blob.write(data)
190    return blob
191
192
193def save_map_screenshot(workout):
194    if workout.has_gpx:
195        current_path = os.path.abspath(os.path.dirname(__file__))
196        tool_path = os.path.join(current_path, '../bin/screenshot_map')
197
198        screenshots_path = os.path.join(
199            current_path, 'static/maps', str(workout.owner.uid))
200        if not os.path.exists(screenshots_path):
201            os.makedirs(screenshots_path)
202
203        screenshot_path = os.path.join(
204            screenshots_path, str(workout.workout_id))
205        screenshot_path += '.png'
206
207        subprocess.run(
208            [tool_path, str(workout.owner.uid), str(workout.workout_id),
209             screenshot_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
210
211        return True
212
213    return False
214
215
216def timedelta_to_hms(value):
217    """
218    Return hours, minutes, seconds from a timedelta object
219    """
220    hours, remainder = divmod(int(value.total_seconds()), 3600)
221    minutes, seconds = divmod(remainder, 60)
222    return hours, minutes, seconds
223
224
225def get_week_days(day, start_day=1):
226    """
227    Return a list of datetime objects for the days of the week "day" is in.
228
229    day is a datetime object (like in datetime.now() for "today")
230
231    start_day can be used to set if week starts on monday (1) or sunday (0)
232    """
233    first_day = day - timedelta(days=day.isoweekday() - start_day)
234    week_days = [first_day + timedelta(days=i) for i in range(7)]
235    return week_days
Note: See TracBrowser for help on using the repository browser.