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
RevLine 
[5ec3a0b]1import re
[d1c4782]2import os
3import logging
[9ab0fe3]4import calendar
[d6f8304]5import shutil
6import time
[2f8a48f]7from datetime import datetime, timedelta
[53bb3e5]8from decimal import Decimal
9from shutil import copyfileobj
10
[5ec3a0b]11from unidecode import unidecode
12from xml.dom import minidom
[53bb3e5]13from ZODB.blob import Blob
[d6f8304]14from splinter import Browser
15
[d517001]16from pyramid.i18n import TranslationStringFactory
17
18_ = TranslationStringFactory('OpenWorkouts')
19
[5ec3a0b]20
[d1c4782]21log = logging.getLogger(__name__)
22
[5ec3a0b]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:
[53bb3e5]97                    t = datetime.strptime(
[5ec3a0b]98                        rfc3339, '%Y-%m-%dT%H:%M:%S.%fZ')
99                except ValueError:
[53bb3e5]100                    t = datetime.strptime(
[5ec3a0b]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})
[53bb3e5]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
[119412d]187def create_blob(data, file_extension, binary=False):
[53bb3e5]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
[119412d]195        if not binary and not isinstance(data, bytes):
[53bb3e5]196            data = data.encode('utf-8')
197        open_blob.write(data)
198    return blob
[d1c4782]199
200
[d6f8304]201def save_map_screenshot(workout, request):
202
[d1c4782]203    if workout.has_gpx:
204
[d6f8304]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__))
[d1c4782]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
[d6f8304]230        shutil.move(splinter_screenshot_path, screenshot_path)
[02aee97]231        os.chmod(screenshot_path, 0o644)
[d1c4782]232        return True
233
234    return False
[2f8a48f]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
[9ab0fe3]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
[d517001]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.