source: OpenWorkouts-current/ow/utilities.py @ d517001

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

(#58) Set a title automatically when adding manually a workout without
providing one.

The title is generated based on the only required data we have (starting
date and time) + sport (if provided).

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