diff --git a/gpx.xsd b/gpx.xsd new file mode 100644 index 00000000..2237442b --- /dev/null +++ b/gpx.xsd @@ -0,0 +1 @@ + GPX schema version 1.1 - For more information on GPX and this schema, visit http://www.topografix.com/gpx.asp GPX uses the following conventions: all coordinates are relative to the WGS84 datum. All measurements are in metric units. GPX is the root element in the XML file. GPX documents contain a metadata header, followed by waypoints, routes, and tracks. You can add your own elements to the extensions section of the GPX document. Metadata about the file. A list of waypoints. A list of routes. A list of tracks. You can add extend GPX by adding your own elements from another schema here. You must include the version number in your GPX document. You must include the name or URL of the software that created your GPX document. This allows others to inform the creator of a GPX instance document that fails to validate. Information about the GPX file, author, and copyright restrictions goes in the metadata section. Providing rich, meaningful information about your GPX files allows others to search for and use your GPS data. The name of the GPX file. A description of the contents of the GPX file. The person or organization who created the GPX file. Copyright and license information governing use of the file. URLs associated with the location described in the file. The creation date of the file. Keywords associated with the file. Search engines or databases can use this information to classify the data. Minimum and maximum coordinates which describe the extent of the coordinates in the file. You can add extend GPX by adding your own elements from another schema here. wpt represents a waypoint, point of interest, or named feature on a map. Elevation (in meters) of the point. Creation/modification timestamp for element. Date and time in are in Univeral Coordinated Time (UTC), not local time! Conforms to ISO 8601 specification for date/time representation. Fractional seconds are allowed for millisecond timing in tracklogs. Magnetic variation (in degrees) at the point Height (in meters) of geoid (mean sea level) above WGS84 earth ellipsoid. As defined in NMEA GGA message. The GPS name of the waypoint. This field will be transferred to and from the GPS. GPX does not place restrictions on the length of this field or the characters contained in it. It is up to the receiving application to validate the field before sending it to the GPS. GPS waypoint comment. Sent to GPS as comment. A text description of the element. Holds additional information about the element intended for the user, not the GPS. Source of data. Included to give user some idea of reliability and accuracy of data. "Garmin eTrex", "USGS quad Boston North", e.g. Link to additional information about the waypoint. Text of GPS symbol name. For interchange with other programs, use the exact spelling of the symbol as displayed on the GPS. If the GPS abbreviates words, spell them out. Type (classification) of the waypoint. Type of GPX fix. Number of satellites used to calculate the GPX fix. Horizontal dilution of precision. Vertical dilution of precision. Position dilution of precision. Number of seconds since last DGPS update. ID of DGPS station used in differential correction. You can add extend GPX by adding your own elements from another schema here. The latitude of the point. This is always in decimal degrees, and always in WGS84 datum. The longitude of the point. This is always in decimal degrees, and always in WGS84 datum. rte represents route - an ordered list of waypoints representing a series of turn points leading to a destination. GPS name of route. GPS comment for route. Text description of route for user. Not sent to GPS. Source of data. Included to give user some idea of reliability and accuracy of data. Links to external information about the route. GPS route number. Type (classification) of route. You can add extend GPX by adding your own elements from another schema here. A list of route points. trk represents a track - an ordered list of points describing a path. GPS name of track. GPS comment for track. User description of track. Source of data. Included to give user some idea of reliability and accuracy of data. Links to external information about track. GPS track number. Type (classification) of track. You can add extend GPX by adding your own elements from another schema here. A Track Segment holds a list of Track Points which are logically connected in order. To represent a single GPS track where GPS reception was lost, or the GPS receiver was turned off, start a new Track Segment for each continuous span of track data. You can add extend GPX by adding your own elements from another schema here. You can add extend GPX by adding your own elements from another schema here. A Track Segment holds a list of Track Points which are logically connected in order. To represent a single GPS track where GPS reception was lost, or the GPS receiver was turned off, start a new Track Segment for each continuous span of track data. A Track Point holds the coordinates, elevation, timestamp, and metadata for a single point in a track. You can add extend GPX by adding your own elements from another schema here. Information about the copyright holder and any license governing use of this file. By linking to an appropriate license, you may place your data into the public domain or grant additional usage rights. Year of copyright. Link to external file containing license text. Copyright holder (TopoSoft, Inc.) A link to an external resource (Web page, digital photo, video clip, etc) with additional information. Text of hyperlink. Mime type of content (image/jpeg) URL of hyperlink. An email address. Broken into two parts (id and domain) to help prevent email harvesting. id half of email address (billgates2004) domain half of email address (hotmail.com) A person or organization. Name of person or organization. Email address. Link to Web site or other external information about person. A geographic point with optional elevation and time. Available for use by other schemas. The elevation (in meters) of the point. The time that the point was recorded. The latitude of the point. Decimal degrees, WGS84 datum. The latitude of the point. Decimal degrees, WGS84 datum. An ordered sequence of points. (for polygons or polylines, e.g.) Ordered list of geographic points. Two lat/lon pairs defining the extent of an element. The minimum latitude. The minimum longitude. The maximum latitude. The maximum longitude. The latitude of the point. Decimal degrees, WGS84 datum. The longitude of the point. Decimal degrees, WGS84 datum. Used for bearing, heading, course. Units are decimal degrees, true (not magnetic). Type of GPS fix. none means GPS had no fix. To signify "the fix info is unknown, leave out fixType entirely. pps = military signal used Represents a differential GPS station. \ No newline at end of file diff --git a/rowers/dataprep.py b/rowers/dataprep.py index c244c7af..b76ed1a2 100644 --- a/rowers/dataprep.py +++ b/rowers/dataprep.py @@ -398,7 +398,8 @@ def save_workout_database(f2,r,dosmooth=True,workouttype='rower', notes='',totaldist=0,totaltime=0, summary='', makeprivate=False, - oarlength=2.89,inboard=0.88): + oarlength=2.89,inboard=0.88, + consistencychecks=True): message = None powerperc = 100*np.array([r.pw_ut2, r.pw_ut1, @@ -417,10 +418,13 @@ def save_workout_database(f2,r,dosmooth=True,workouttype='rower', for key,value in checks.iteritems(): if not value: allchecks = 0 - a_messages.error(r.user,'Failed consistency check: '+key+', autocorrected') + if consistencychecks: + a_messages.error(r.user,'Failed consistency check: '+key+', autocorrected') + else: + a_messages.error(r.user,'Failed consistency check: '+key+', not corrected') - if not allchecks: - #row.repair() + if not allchecks and consistencychecks: + # row.repair() pass @@ -529,6 +533,7 @@ def save_workout_database(f2,r,dosmooth=True,workouttype='rower', user=r) if (len(ws) != 0): message = "Warning: This workout probably already exists in the database" + privacy = 'private' # checking for inf values totaldist = np.nan_to_num(totaldist) @@ -781,7 +786,8 @@ def new_workout_from_df(r,df, oarlength=oarlength, inboard=inboard, makeprivate=makeprivate, - dosmooth=False) + dosmooth=False, + consistencychecks=False) return (id,message) diff --git a/rowers/dataprepnodjango.py b/rowers/dataprepnodjango.py index fd8c6d55..78d3903b 100644 --- a/rowers/dataprepnodjango.py +++ b/rowers/dataprepnodjango.py @@ -243,6 +243,7 @@ def save_workout_database(f2,r,dosmooth=True,workouttype='rower', user=r) if (len(ws) != 0): message = "Warning: This workout probably already exists in the database" + privacy = 'private' diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py index 7f640cee..0e2e4d23 100644 --- a/rowers/interactiveplots.py +++ b/rowers/interactiveplots.py @@ -596,6 +596,106 @@ def googlemap_chart(lat,lon,name=""): return [script,div] +def interactive_otwcpchart(powerdf,promember=0): + powerdf = powerdf[~(powerdf == 0).any(axis=1)] + # plot tools + if (promember==1): + TOOLS = 'save,pan,box_zoom,wheel_zoom,reset,tap,hover,resize,crosshair' + else: + TOOLS = 'pan,box_zoom,wheel_zoom,reset,tap,hover,crosshair' + + + x_axis_type = 'log' + y_axis_type = 'linear' + + deltas = powerdf['Delta'].apply(lambda x: timedeltaconv(x)) + powerdf['ftime'] = niceformat(deltas) + + + source = ColumnDataSource( + data = powerdf + ) + + # there is no Paul's law for OTW + + # Fit the data to thee parameter CP model + fitfunc = lambda pars,x: pars[0]/(1+(x/pars[2])) + pars[1]/(1+(x/pars[3])) + errfunc = lambda pars,x,y: fitfunc(pars,x)-y + + p0 = [500,350,10,8000] + + p1 = p0 + + thesecs = powerdf['Delta'] + theavpower = powerdf['CP'] + + if len(thesecs)>=4: + p1, success = optimize.leastsq(errfunc, p0[:], args = (thesecs,theavpower)) + else: + factor = fitfunc(p0,thesecs.mean())/theavpower.mean() + p1 = [p0[0]/factor,p0[1]/factor,p0[2],p0[3]] + + + fitt = pd.Series(10**(4*np.arange(100)/100.)) + + fitpower = fitfunc(p1,fitt) + + message = "" + #if len(fitpower[fitpower<0]) > 0: + # message = "CP model fit didn't give correct results" + + + sourcecomplex = ColumnDataSource( + data = dict( + power = fitpower, + duration = fitt + ) + ) + + # making the plot + plot = Figure(tools=TOOLS,x_axis_type=x_axis_type, + plot_width=900, + toolbar_location="above", + toolbar_sticky=False) + + # add watermark + plot.extra_y_ranges = {"watermark": watermarkrange} + + plot.image_url([watermarkurl],1.8*max(thesecs),watermarky, + watermarkw,watermarkh, + global_alpha=watermarkalpha, + w_units='screen', + h_units='screen', + anchor=watermarkanchor, + dilate=True, + y_range_name = "watermark", + ) + + plot.circle('Delta','CP',source=source,fill_color='red',size=15, + legend='Power Data') + plot.xaxis.axis_label = "Duration (seconds)" + plot.yaxis.axis_label = "Power (W)" + + plot.y_range = Range1d(0,1.5*max(theavpower)) + plot.x_range = Range1d(1,2*max(thesecs)) + plot.legend.orientation = "vertical" + + hover = plot.select(dict(type=HoverTool)) + + hover.tooltips = OrderedDict([ + ('Duration ','@ftime'), + ('Power (W)','@CP{int}'), + ]) + + hover.mode = 'mouse' + + plot.line('duration','power',source=sourcecomplex,legend="CP Model", + color='green') + + script, div = components(plot) + + return [script,div,p1,message] + def interactive_cpchart(thedistances,thesecs,theavpower, theworkouts,promember=0): diff --git a/rowers/models.py b/rowers/models.py index 528c28b9..05e27344 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -425,7 +425,8 @@ class Workout(models.Model): summary = models.TextField(blank=True) privacy = models.CharField(default='visible',max_length=30, choices=privacychoices) - + rankingpiece = models.BooleanField(default=False,verbose_name='Ranking Piece') + def __unicode__(self): date = self.date @@ -552,7 +553,7 @@ class WorkoutForm(ModelForm): duration = forms.TimeInput(format='%H:%M:%S.%f') class Meta: model = Workout - fields = ['name','date','starttime','duration','distance','workouttype','notes','privacy','boattype'] + fields = ['name','date','starttime','duration','distance','workouttype','notes','privacy','rankingpiece','boattype'] widgets = { 'date': DateInput(), 'notes': forms.Textarea, diff --git a/rowers/stravastuff.py b/rowers/stravastuff.py index 28900411..eff56909 100644 --- a/rowers/stravastuff.py +++ b/rowers/stravastuff.py @@ -245,7 +245,7 @@ def get_strava_workout(user,stravaid): return [workoutsummary,df] # Generate Workout data for Strava (a TCX file) -def createstravaworkoutdata(w): +def createstravaworkoutdata(w,dozip=True): filename = w.csvfilename row = rowingdata(filename) @@ -256,18 +256,22 @@ def createstravaworkoutdata(w): newnotes = 'from '+w.workoutsource+' via rowsandall.com' row.exporttotcx(tcxfilename,notes=newnotes) - gzfilename = tcxfilename+'.gz' - with file(tcxfilename,'rb') as inF: - s = inF.read() - with gzip.GzipFile(gzfilename,'wb') as outF: - outF.write(s) + if dozip: + gzfilename = tcxfilename+'.gz' + with file(tcxfilename,'rb') as inF: + s = inF.read() + with gzip.GzipFile(gzfilename,'wb') as outF: + outF.write(s) - try: - os.remove(tcxfilename) - except WindowError: - pass - - return gzfilename,"" + try: + os.remove(tcxfilename) + except WindowError: + pass + + return gzfilename,"" + + else: + return tcxfilename,"" # Upload the TCX file to Strava and set the workout activity type diff --git a/rowers/templates/analysis.html b/rowers/templates/analysis.html index 2dc2cf43..9fd83289 100644 --- a/rowers/templates/analysis.html +++ b/rowers/templates/analysis.html @@ -77,8 +77,28 @@ +
+
+

 

+
+
+
+

+ {% if user.rower.rowerplan == 'pro' or user.rower.rowerplan == 'coach' %} + OTW Ranking Pieces + {% else %} + OTW Ranking Pieces + {% endif %} +

+

+ Analyse power vs piece duration to make predictions. +

+
+ +
+ {% endblock %} diff --git a/rowers/templates/export.html b/rowers/templates/export.html index 52f53480..3953b013 100644 --- a/rowers/templates/export.html +++ b/rowers/templates/export.html @@ -147,6 +147,10 @@ {% endif %} +
+ + GPX Export +
diff --git a/rowers/templates/otwrankings.html b/rowers/templates/otwrankings.html new file mode 100644 index 00000000..a78330e4 --- /dev/null +++ b/rowers/templates/otwrankings.html @@ -0,0 +1,207 @@ +{% extends "base.html" %} +{% load staticfiles %} +{% load rowerfilters %} + +{% block title %}Workouts{% endblock %} + +{% block content %} + + + + + {{ interactiveplot |safe }} + + + + + +
+
+ {% if theuser %} +

{{ theuser.first_name }}'s Ranking Pieces

+ {% else %} +

{{ user.first_name }}'s Ranking Pieces

+ {% endif %} +
+
+ {% if user.is_authenticated and user|is_manager %} + +
+ +
+

Summary for {{ theuser.first_name }} {{ theuser.last_name }} + between {{ startdate|date }} and {{ enddate|date }}

+ +

Direct link for other users: + https://rowsandall.com/rowers/{{ id }}/otw-bests/{{ startdate|date:"Y-m-d" }}/{{ enddate|date:"Y-m-d" }} +

+ +

The table gives the OTW efforts you marked as Ranking Piece. + The graph shows the best segments from those pieces, plotted as + average power (over the segment) vs the duration of the segment/ + In other words: How long you can hold that power. +

+

At the bottom of the page, you will find predictions derived from the model.

+
+
+

Use this form to select a different date range:

+

+ Select start and end date for a date range: +

+ +
+ + + {{ dateform.as_table }} +
+ {% csrf_token %} +
+
+ +
+
+
+ Or use the last {{ deltaform }} days. +
+
+ {% csrf_token %} + + +
+
+ + + +
+ +

Critical Power Plot

+ + {{ the_div|safe }} + +
+ +
+ +

Ranking Piece Results

+ + {% if rankingworkouts %} + + + + + + + + + + + + + + + {% for workout in rankingworkouts %} + + + + + + + + + + + + {% endfor %} + +
Distance Duration Avg Power Date Avg HR Max HR Edit
{{ workout.distance }} m {{ workout.duration |durationprint:"%H:%M:%S.%f" }} {{ avgpower|lookup:workout.id }} W {{ workout.date }} {{ workout.averagehr }} {{ workout.maxhr }} + {{ workout.name }}
+ {% else %} +

No ranking workouts found

+ {% endif %} + +
+ +
+

Pace predictions for Ranking Pieces

+ +

Add non-ranking piece using the form. The piece will be added in the prediction tables below.

+ + + +
+ + + + + + + + + {% for pred in cpredictions %} + + {% for key, value in pred.items %} + {% if key == "power" %} + + {% endif %} + {% if key == "duration" %} + + {% endif %} + {% endfor %} + + {% endfor %} + +
Duration Power
{{ value }} W {{ value |deltatimeprint }}
+ +
+ +
+
+ {{ form.value }} {{ form.pieceunit }} + + {% csrf_token %} +
+
+ minutes +
+
+ + +
+ + +
+ +{% endblock %} diff --git a/rowers/urls.py b/rowers/urls.py index 33f953f7..e850a4c6 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -135,6 +135,12 @@ urlpatterns = [ url(r'^ote-bests/(?P\d+)$',views.rankings_view), url(r'^ote-bests/$',views.rankings_view), url(r'^(?P\d+)/ote-bests/$',views.rankings_view), + url(r'^(?P\d+)/otw-bests/(?P\w+.*)/(?P\w+.*)$',views.otwrankings_view), + url(r'^(?P\d+)/otw-bests/(?P\d+)$',views.otwrankings_view), + url(r'^otw-bests/(?P\w+.*)/(?P\w+.*)$',views.otwrankings_view), + url(r'^otw-bests/(?P\d+)$',views.otwrankings_view), + url(r'^otw-bests/$',views.otwrankings_view), + url(r'^(?P\d+)/otw-bests/$',views.otwrankings_view), url(r'^(?P\d+)/flexall/(?P\w+.*)/(?P\w+.*)/(?P\w+.*)/(?P\w+.*)/(?P\w+.*)$',views.cum_flex), url(r'^flexall/(?P\w+.*)/(?P\w+.*)/(?P\w+.*)/(?P\w+.*)/(?P\w+.*)$',views.cum_flex), url(r'^flexall/(?P\w+.*)/(?P\w+.*)/(?P\w+.*)$',views.cum_flex), @@ -169,6 +175,7 @@ urlpatterns = [ url(r'^workout/(?P\d+)/export$',views.workout_export_view), url(r'^workout/(?P\d+)/comment$',views.workout_comment_view), url(r'^workout/(?P\d+)/emailtcx$',views.workout_tcxemail_view), + url(r'^workout/(?P\d+)/emailgpx$',views.workout_gpxemail_view), url(r'^workout/(?P\d+)/emailcsv$',views.workout_csvemail_view), url(r'^workout/(?P\d+)/csvtoadmin$',views.workout_csvtoadmin_view), url(r'^workout/compare/(?P\d+)/$',views.workout_comparison_list), diff --git a/rowers/views.py b/rowers/views.py index 1eaa47b8..0c5ae939 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -138,6 +138,8 @@ from scipy.special import lambertw from dataprep import timedeltaconv +from scipy.interpolate import griddata + LOCALTIMEZONE = tz('Etc/UTC') USER_LANGUAGE = 'en-US' @@ -290,14 +292,11 @@ def iscoachmember(user): return result def getrower(user): - if not user.is_anonymous(): - try: - r = Rower.objects.get(user=user) - except Rower.DoesNotExist: - r = Rower(user=user) - r.save() - else: - raise PermissionDenied("You need to log in to use this function") + try: + r = Rower.objects.get(user=user) + except Rower.DoesNotExist: + r = Rower(user=user) + r.save() return r @@ -1086,7 +1085,7 @@ def workout_tcxemail_view(request,id=0): raise Http404("Workout doesn't exist") if (checkworkoutuser(request.user,w)): try: - tcxfile,tcxmessg = stravastuff.createstravaworkoutdata(w) + tcxfile,tcxmessg = stravastuff.createstravaworkoutdata(w,dozip=False) if tcxfile == 0: message = "Something went wrong (TCX export) "+tcxmessg messages.error(request,message) @@ -1138,6 +1137,51 @@ def workout_tcxemail_view(request,id=0): return response +# Export workout to GPX and send to user's email address +@login_required() +def workout_gpxemail_view(request,id=0): + message = "" + successmessage = "" + r = Rower.objects.get(user=request.user) + try: + w = Workout.objects.get(id=id) + except Workout.DoesNotExist: + raise Http404("Workout doesn't exist") + if (checkworkoutuser(request.user,w)): + filename = w.csvfilename + row = rdata(filename) + gpxfilename = filename[:-4]+'.gpx' + row.exporttogpx(gpxfilename) + if settings.DEBUG: + res = handle_sendemailtcx.delay(r.user.first_name, + r.user.last_name, + r.user.email,gpxfilename) + + else: + res = queuehigh.enqueue(handle_sendemailtcx,r.user.first_name, + r.user.last_name, + r.user.email,gpxfilename) + + successmessage = "The GPX file was sent to you per email" + messages.info(request,successmessage) + url = reverse(workout_export_view, + kwargs = { + 'id':str(w.id), + }) + + response = HttpResponseRedirect(url) + + else: + message = "You are not allowed to export this workout" + messages.error(request,message) + url = reverse(workout_export_view, + kwargs = { + 'id':str(w.id), + }) + response = HttpResponseRedirect(url) + + return response + # Get Workout CSV file and send it to user's email address @login_required() def workout_csvemail_view(request,id=0): @@ -2730,6 +2774,274 @@ def rankings_view(request,theuser=0, 'teams':get_my_teams(request.user), }) +# Show ranking distances including predicted paces +@login_required() +def otwrankings_view(request,theuser=0, + startdate=timezone.now()-datetime.timedelta(days=365), + enddate=timezone.now(), + deltadays=-1, + startdatestring="", + enddatestring=""): + + if deltadays>0: + startdate = enddate-datetime.timedelta(days=int(deltadays)) + + if startdatestring != "": + startdate = iso8601.parse_date(startdatestring) + + if enddatestring != "": + enddate = iso8601.parse_date(enddatestring) + + if enddate < startdate: + s = enddate + enddate = startdate + startdate = s + + if theuser == 0: + theuser = request.user.id + + promember=0 + if not request.user.is_anonymous(): + r = Rower.objects.get(user=request.user) + result = request.user.is_authenticated() and ispromember(request.user) + if result: + promember=1 + + # get all OTW rows in date range + + # process form + if request.method == 'POST' and "daterange" in request.POST: + dateform = DateRangeForm(request.POST) + deltaform = DeltaDaysForm(request.POST) + if dateform.is_valid(): + startdate = dateform.cleaned_data['startdate'] + enddate = dateform.cleaned_data['enddate'] + if startdate > enddate: + s = enddate + enddate = startdate + startdate = s + elif request.method == 'POST' and "datedelta" in request.POST: + deltaform = DeltaDaysForm(request.POST) + if deltaform.is_valid(): + deltadays = deltaform.cleaned_data['deltadays'] + if deltadays: + enddate = timezone.now() + startdate = enddate-datetime.timedelta(days=deltadays) + if startdate > enddate: + s = enddate + enddate = startdate + startdate = s + dateform = DateRangeForm(initial={ + 'startdate': startdate, + 'enddate': enddate, + }) + else: + dateform = DateRangeForm() + deltaform = DeltaDaysForm() + + else: + dateform = DateRangeForm(initial={ + 'startdate': startdate, + 'enddate': enddate, + }) + deltaform = DeltaDaysForm() + + # get all 2k (if any) - this rower, in date range + try: + r = Rower.objects.get(user=theuser) + except Rower.DoesNotExist: + allergworkouts = [] + r=0 + + + try: + uu = User.objects.get(id=theuser) + except User.DoesNotExist: + uu = '' + + + # test to fix bug + startdate = datetime.datetime.combine(startdate,datetime.time()) + enddate = datetime.datetime.combine(enddate,datetime.time(23,59,59)) + enddate = enddate+datetime.timedelta(days=1) + + + rankingdurations = [] + rankingdurations.append(datetime.time(minute=1)) + rankingdurations.append(datetime.time(minute=4)) + rankingdurations.append(datetime.time(minute=30)) + rankingdurations.append(datetime.time(hour=1)) + + thedistances = [] + theworkouts = [] + thesecs = [] + + theworkouts = Workout.objects.filter(user=r,rankingpiece=True, + workouttype='water', + startdatetime__gte=startdate, + startdatetime__lte=enddate) + + + # get all power data from database (plus workoutid) + theids = [w.id for w in theworkouts] + columns = ['power','workoutid','time'] + df = dataprep.getsmallrowdata_db(columns,ids=theids) + + thesecs = [] + + for w in theworkouts: + timesecs = 3600*w.duration.hour + timesecs += 60*w.duration.minute + timesecs += w.duration.second + timesecs += 1.e-5*w.duration.microsecond + + thesecs.append(timesecs) + + if len(thesecs) != 0: + maxt = pd.Series(thesecs).max() + else: + maxt = 1000. + + maxlog10 = np.log10(maxt) + logarr = np.arange(100)*maxlog10/100. + logarr = [int(10.**(la)) for la in logarr] + logarr = pd.Series(logarr) + logarr.drop_duplicates(keep='first',inplace=True) + logarr = logarr.values + + delta = [] + cpvalue = [] + avgpower = {} + + dfgrouped = df.groupby(['workoutid']) + for id,group in dfgrouped: + tt = group['time'] + ww = group['power'] + try: + avgpower[id] = int(ww.mean()) + except ValueError: + avgpower[id] = '---' + if not np.isnan(ww.mean()): + length = len(ww) + dt = [] + cpw = [] + for i in range(length-2): + w_roll = ww.rolling(i+2,min_periods=2).mean() + # now goes with # data points - should be fixed seconds + indexmax = w_roll.idxmax(axis=1) + try: + t_0 = tt.ix[indexmax] + t_1 = tt.ix[indexmax-i-2] + deltat = 1.0e-3*(t_0-t_1) + wmax = w_roll.ix[indexmax] + dt.append(deltat) + cpw.append(wmax) + except KeyError: + pass + + dt = pd.Series(dt) + cpw = pd.Series(cpw) + cpvalues = griddata(dt.values, + cpw.values, + logarr,method='linear',fill_value=0) + + for cpv in cpvalues: + cpvalue.append(cpv) + for d in logarr: + delta.append(d) + + print avgpower + dt = pd.Series(delta,name='Delta') + cpvalue = pd.Series(cpvalue,name='CP') + + + powerdf = pd.DataFrame({ + 'Delta':delta, + 'CP':cpvalue, + }) + + powerdf = powerdf[powerdf['CP']>0] + powerdf.dropna(axis=0,inplace=True) + powerdf.sort_values(['Delta','CP'],ascending=[1,0],inplace=True) + powerdf.drop_duplicates(subset='Delta',keep='first',inplace=True) + + + # create interactive plot + if len(powerdf) !=0 : + res = interactive_otwcpchart(powerdf,promember=promember) + script = res[0] + div = res[1] + p1 = res[2] + paulslope = 1 + paulintercept = 1 + message = res[3] + else: + script = '' + div = '

No ranking pieces found.

' + paulslope = 1 + paulintercept = 1 + p1 = [1,1,1,1] + message = "" + + + if request.method == 'POST' and "piece" in request.POST: + form = PredictedPieceForm(request.POST) + clean = form.is_valid() + value = form.cleaned_data['value'] + hourvalue,value = divmod(value,60) + if hourvalue >= 24: + hourvalue = 23 + rankingdurations.append(datetime.time(minute=value,hour=hourvalue)) + else: + form = PredictedPieceForm() + + + predictions = [] + cpredictions = [] + + + + for rankingduration in rankingdurations: + t = 3600.*rankingduration.hour + t += 60.*rankingduration.minute + t += rankingduration.second + t += rankingduration.microsecond/1.e6 + + + # CP model + pwr = p1[0]/(1+t/p1[2]) + pwr += p1[1]/(1+t/p1[3]) + + if pwr <= 0: + pwr = 50. + + if not np.isnan(pwr): + a = { + 'duration':timedeltaconv(t), + 'power':int(pwr)} + cpredictions.append(a) + + + del form.fields["pieceunit"] + + messages.error(request,message) + return render(request, 'otwrankings.html', + {'rankingworkouts':theworkouts, + 'interactiveplot':script, + 'the_div':div, + 'predictions':predictions, + 'cpredictions':cpredictions, + 'avgpower':avgpower, + 'form':form, + 'dateform':dateform, + 'deltaform':deltaform, + 'id': theuser, + 'theuser':uu, + 'startdate':startdate, + 'enddate':enddate, + 'teams':get_my_teams(request.user), + }) + # Reload the workout and calculate the summary from the stroke data (lapIDx) @login_required() def workout_recalcsummary_view(request,id=0): @@ -5207,6 +5519,11 @@ def workout_edit_view(request,id=0,message="",successmessage=""): privacy = request.POST['privacy'] except KeyError: privacy = Workout.objects.get(id=id).privacy + try: + rankingpiece = form.cleaned_data['rankingpiece'] + except KeyError: + rankingpiece =- Workout.objects.get(id=id).rankingpiece + startdatetime = (str(date) + ' ' + str(starttime)) startdatetime = datetime.datetime.strptime(startdatetime, "%Y-%m-%d %H:%M:%S") @@ -5223,6 +5540,7 @@ def workout_edit_view(request,id=0,message="",successmessage=""): row.distance = distance row.boattype = boattype row.privacy = privacy + row.rankingpiece = rankingpiece try: row.save() except IntegrityError: diff --git a/static/img/gpx.jpg b/static/img/gpx.jpg new file mode 100644 index 00000000..b81a5ef4 Binary files /dev/null and b/static/img/gpx.jpg differ