Merge branch 'develop' of https://bitbucket.org/sanderroosendaal/rowsandall into develop
This commit is contained in:
+131
-2
@@ -10,6 +10,7 @@ from __future__ import unicode_literals, absolute_import
|
||||
from rowers.models import Workout, Team
|
||||
|
||||
import pytz
|
||||
import collections
|
||||
|
||||
from rowingdata import rowingdata as rrdata
|
||||
|
||||
@@ -55,7 +56,7 @@ from rowingdata import (
|
||||
from rowingdata.csvparsers import HumonParser
|
||||
|
||||
|
||||
from rowers.metrics import axes,calc_trimp,rowingmetrics,dtypes
|
||||
from rowers.metrics import axes,calc_trimp,rowingmetrics,dtypes,metricsgroups
|
||||
from rowers.models import strokedatafields
|
||||
|
||||
#allowedcolumns = [item[0] for item in rowingmetrics]
|
||||
@@ -122,6 +123,100 @@ from scipy.signal import savgol_filter
|
||||
|
||||
import datetime
|
||||
|
||||
|
||||
def get_video_data(w,groups=['basic'],mode='water'):
|
||||
modes = [mode,'both','basic']
|
||||
columns = ['time','velo','spm']
|
||||
columns += [name for name,d in rowingmetrics if d['group'] in groups and d['mode'] in modes]
|
||||
columns = list(set(columns))
|
||||
df = getsmallrowdata_db(columns,ids=[w.id],
|
||||
workstrokesonly=False,doclean=False,compute=False)
|
||||
df['time'] = (df['time']-df['time'].min())/1000.
|
||||
df.sort_values(by='time',inplace=True)
|
||||
|
||||
|
||||
df.set_index(pd.to_timedelta(df['time'],unit='s'),inplace=True)
|
||||
df2 = df.resample('1s').first().fillna(method='ffill')
|
||||
if 'pace' in columns:
|
||||
df2['pace'] = df2['pace']/1000.
|
||||
p = df2['pace']
|
||||
p = p.apply(lambda x:timedeltaconv(x))
|
||||
p = nicepaceformat(p)
|
||||
df2['pace'] = p
|
||||
|
||||
|
||||
|
||||
|
||||
#mask = df2['time'] < delay
|
||||
#df2 = df2.mask(mask).dropna()
|
||||
df2['time'] = (df2['time']-df2['time'].min())
|
||||
|
||||
df2 = df2.round(decimals=2)
|
||||
|
||||
boatspeed = (100*df2['velo']).astype(int)/100.
|
||||
|
||||
try:
|
||||
coordinates = get_latlon_time(w.id)
|
||||
except KeyError:
|
||||
nulseries = df['time']*0
|
||||
coordinates = pd.DataFrame({
|
||||
'time': df['time'],
|
||||
'latitude': nulseries,
|
||||
'longitude': nulseries,
|
||||
})
|
||||
|
||||
coordinates.set_index(pd.to_timedelta(coordinates['time'],unit='s'),inplace=True)
|
||||
coordinates = coordinates.resample('1s').mean().interpolate()
|
||||
#mask = coordinates['time'] < delay
|
||||
#coordinates = coordinates.mask(mask).dropna()
|
||||
coordinates['time'] = coordinates['time']-coordinates['time'].min()
|
||||
latitude = coordinates['latitude']
|
||||
longitude = coordinates['longitude']
|
||||
|
||||
|
||||
|
||||
# bundle data
|
||||
data = {
|
||||
'boatspeed':boatspeed.values.tolist(),
|
||||
'latitude':latitude.values.tolist(),
|
||||
'longitude':longitude.values.tolist(),
|
||||
}
|
||||
|
||||
# metrics = {
|
||||
# 'boatspeed': {
|
||||
# 'unit': 'm/s',
|
||||
# 'metric': 'boatspeed',
|
||||
# 'name': 'Boat Speed'
|
||||
# },
|
||||
# }
|
||||
|
||||
metrics = {}
|
||||
|
||||
for c in columns:
|
||||
if c != 'time':
|
||||
try:
|
||||
if dict(rowingmetrics)[c]['numtype'] == 'integer':
|
||||
data[c] = df2[c].astype(int).tolist()
|
||||
else:
|
||||
data[c] = df2[c].values.tolist()
|
||||
metrics[c] = {
|
||||
'name': dict(rowingmetrics)[c]['verbose_name'],
|
||||
'metric': c,
|
||||
'unit': ''
|
||||
}
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
metrics['boatspeed'] = metrics.pop('velo')
|
||||
# metrics['workperstroke'] = metrics.pop('driveenergy')
|
||||
metrics = collections.OrderedDict(sorted(metrics.items()))
|
||||
|
||||
maxtime = coordinates['time'].max()
|
||||
|
||||
|
||||
return data, metrics, maxtime
|
||||
|
||||
|
||||
def polarization_index(df,rower):
|
||||
df['dt'] = df['time'].diff()/6.e4
|
||||
# remove rest (spm<15)
|
||||
@@ -172,6 +267,36 @@ def get_latlon(id):
|
||||
|
||||
return [pd.Series([]), pd.Series([])]
|
||||
|
||||
def get_latlon_time(id):
|
||||
try:
|
||||
w = Workout.objects.get(id=id)
|
||||
except Workout.DoesNotExist:
|
||||
return False
|
||||
|
||||
|
||||
rowdata = rdata(w.csvfilename)
|
||||
|
||||
if rowdata.df.empty:
|
||||
return [pd.Series([]), pd.Series([])]
|
||||
|
||||
try:
|
||||
try:
|
||||
latitude = rowdata.df.loc[:, ' latitude']
|
||||
longitude = rowdata.df.loc[:, ' longitude']
|
||||
except KeyError:
|
||||
latitude = 0 * rowdata.df.loc[:, 'TimeStamp (sec)']
|
||||
longitude = 0 * rowdata.df.loc[:, 'TimeStamp (sec)']
|
||||
except AttributeError:
|
||||
return pd.DataFrame()
|
||||
|
||||
df = pd.DataFrame({
|
||||
'time': rowdata.df[' ElapsedTime (sec)'],
|
||||
'latitude': rowdata.df[' latitude'],
|
||||
'longitude': rowdata.df[' longitude']
|
||||
})
|
||||
|
||||
return df
|
||||
|
||||
def workout_summary_to_df(
|
||||
rower,
|
||||
startdate=datetime.datetime(1970,1,1),
|
||||
@@ -1693,7 +1818,11 @@ def getrowdata_db(id=0, doclean=False, convertnewtons=True,
|
||||
|
||||
|
||||
|
||||
if not data.empty and data['efficiency'].mean() == 0 and data['power'].mean() != 0 and checkefficiency == True:
|
||||
if checkefficiency==True and not data.empty:
|
||||
try:
|
||||
if data['efficiency'].mean() == 0 and data['power'].mean() != 0:
|
||||
data = add_efficiency(id=id)
|
||||
except KeyError:
|
||||
data = add_efficiency(id=id)
|
||||
|
||||
if doclean:
|
||||
|
||||
@@ -13,6 +13,9 @@ from rowingdata import empower_bug_correction,get_empower_rigging
|
||||
from time import strftime
|
||||
from pandas import DataFrame,Series
|
||||
|
||||
import shutil
|
||||
from shutil import copyfile
|
||||
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
import itertools
|
||||
|
||||
+46
-5
@@ -24,7 +24,7 @@ import rowers.mytypes as mytypes
|
||||
import datetime
|
||||
from django.forms import formset_factory
|
||||
from rowers.utils import landingpages
|
||||
from rowers.metrics import axes
|
||||
from rowers.metrics import axes, metricsgroups,rowingmetrics
|
||||
from rowers.metrics import axlabels
|
||||
|
||||
formaxlabels = axlabels.copy()
|
||||
@@ -51,6 +51,40 @@ class FlexibleDecimalField(forms.DecimalField):
|
||||
value = value.replace('.', '').replace(',', '.')
|
||||
return super(FlexibleDecimalField, self).to_python(value)
|
||||
|
||||
# Video Analysis creation form
|
||||
class VideoAnalysisCreateForm(forms.Form):
|
||||
name = forms.CharField(max_length=255,label='Analysis Name',required=False)
|
||||
url = forms.CharField(max_length=255,required=True,label='YouTube Video URL')
|
||||
delay = forms.IntegerField(initial=0,label='Delay (seconds)')
|
||||
|
||||
def get_metricschoices(mode='water'):
|
||||
modes = [mode,'both','basic']
|
||||
metricsdescriptions = {}
|
||||
for m in metricsgroups:
|
||||
metricsdescriptions[m] = m+' ('
|
||||
for name,d in rowingmetrics:
|
||||
if d['group']==m and d['mode'] in modes:
|
||||
metricsdescriptions[m]+=d['verbose_name']+', '
|
||||
metricsdescriptions[m]=metricsdescriptions[m][0:-2]+')'
|
||||
|
||||
metricsgroupschoices = ((m,metricsdescriptions[m]) for m in metricsgroups)
|
||||
|
||||
return metricsgroupschoices
|
||||
|
||||
class VideoAnalysisMetricsForm(forms.Form):
|
||||
groups = forms.MultipleChoiceField(label='Metrics Groups',
|
||||
choices=get_metricschoices(mode='water'),
|
||||
widget=forms.CheckboxSelectMultiple,)
|
||||
|
||||
class Meta:
|
||||
mode = 'water'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
mode = kwargs.pop('mode','water')
|
||||
super(VideoAnalysisMetricsForm, self).__init__(*args, **kwargs)
|
||||
self.fields['groups'].choices = get_metricschoices(mode=mode)
|
||||
|
||||
|
||||
# BillingForm form
|
||||
class BillingForm(forms.Form):
|
||||
amount = FlexibleDecimalField(required=True,decimal_places=2,
|
||||
@@ -816,6 +850,17 @@ class PlanSelectForm(forms.Form):
|
||||
class CourseSelectForm(forms.Form):
|
||||
course = forms.ModelChoiceField(queryset=GeoCourse.objects.all())
|
||||
|
||||
class WorkoutSingleSelectForm(forms.Form):
|
||||
workout = forms.ModelChoiceField(
|
||||
queryset=Workout.objects.filter(),
|
||||
widget=forms.RadioSelect
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
workouts = kwargs.pop('workouts',Workout.objects.filter().order_by('-date'))
|
||||
super(WorkoutSingleSelectForm,self).__init__(*args,**kwargs)
|
||||
self.fields['workout'].queryset = workouts
|
||||
|
||||
class WorkoutMultipleCompareForm(forms.Form):
|
||||
workouts = forms.ModelMultipleChoiceField(
|
||||
queryset=Workout.objects.filter(),
|
||||
@@ -1380,7 +1425,3 @@ class FlexAxesForm(forms.Form):
|
||||
self.fields['xaxis'].choices = axchoicesbasicx
|
||||
self.fields['yaxis1'].choices = axchoicesbasicy
|
||||
self.fields['yaxis2'].choices = axchoicesbasicy
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1809,6 +1809,127 @@ def leaflet_chart2(lat,lon,name=""):
|
||||
|
||||
return script,div
|
||||
|
||||
def leaflet_chart_video(lat,lon,name=""):
|
||||
if not len(lat) or not len(lon):
|
||||
return [0,"invalid coordinate data"]
|
||||
|
||||
|
||||
# Throw out 0,0
|
||||
df = pd.DataFrame({
|
||||
'lat':lat,
|
||||
'lon':lon
|
||||
})
|
||||
|
||||
df = df.replace(0,np.nan)
|
||||
df = df.loc[(df!=0).any(axis=1)]
|
||||
df.fillna(method='bfill',axis=0,inplace=True)
|
||||
df.fillna(method='ffill',axis=0,inplace=True)
|
||||
lat = df['lat']
|
||||
lon = df['lon']
|
||||
if lat.empty or lon.empty:
|
||||
return [0,"invalid coordinate data"]
|
||||
|
||||
latmean = lat.mean()
|
||||
lonmean = lon.mean()
|
||||
latbegin = lat[lat.index[0]]
|
||||
longbegin = lon[lon.index[0]]
|
||||
latend = lat[lat.index[-1]]
|
||||
longend = lon[lon.index[-1]]
|
||||
|
||||
coordinates = zip(lat,lon)
|
||||
|
||||
scoordinates = "["
|
||||
|
||||
for x,y in coordinates:
|
||||
scoordinates += """[{x},{y}],
|
||||
""".format(
|
||||
x=x,
|
||||
y=y
|
||||
)
|
||||
|
||||
scoordinates += "]"
|
||||
|
||||
script = """
|
||||
|
||||
|
||||
var streets = L.tileLayer('https://api.tiles.mapbox.com/v4/{{id}}/{{z}}/{{x}}/{{y}}.png?access_token=pk.eyJ1Ijoic2FuZGVycm9vc2VuZGFhbCIsImEiOiJjajY3aTRkeWQwNmx6MzJvMTN3andlcnBlIn0.MFG8Xt0kDeSA9j7puZQ9hA', {{
|
||||
maxZoom: 18,
|
||||
id: 'mapbox.streets'
|
||||
}}),
|
||||
|
||||
satellite = L.tileLayer('https://api.tiles.mapbox.com/v4/{{id}}/{{z}}/{{x}}/{{y}}.png?access_token=pk.eyJ1Ijoic2FuZGVycm9vc2VuZGFhbCIsImEiOiJjajY3aTRkeWQwNmx6MzJvMTN3andlcnBlIn0.MFG8Xt0kDeSA9j7puZQ9hA', {{
|
||||
maxZoom: 18,
|
||||
id: 'mapbox.satellite'
|
||||
}}),
|
||||
|
||||
outdoors = L.tileLayer('https://api.tiles.mapbox.com/v4/{{id}}/{{z}}/{{x}}/{{y}}.png?access_token=pk.eyJ1Ijoic2FuZGVycm9vc2VuZGFhbCIsImEiOiJjajY3aTRkeWQwNmx6MzJvMTN3andlcnBlIn0.MFG8Xt0kDeSA9j7puZQ9hA', {{
|
||||
maxZoom: 18,
|
||||
id: 'mapbox.outdoors'
|
||||
}});
|
||||
|
||||
|
||||
|
||||
var mymap = L.map('map_canvas', {{
|
||||
center: [{latmean}, {lonmean}],
|
||||
zoom: 13,
|
||||
layers: [streets, satellite]
|
||||
}}).setView([{latmean},{lonmean}], 13);
|
||||
|
||||
var navionics = new JNC.Leaflet.NavionicsOverlay({{
|
||||
navKey: 'Navionics_webapi_03205',
|
||||
chartType: JNC.NAVIONICS_CHARTS.NAUTICAL,
|
||||
isTransparent: true,
|
||||
zIndex: 1
|
||||
}});
|
||||
|
||||
|
||||
var osmUrl2='http://tiles.openseamap.org/seamark/{{z}}/{{x}}/{{y}}.png';
|
||||
var osmUrl='http://{{s}}.tile.openstreetmap.org/{{z}}/{{x}}/{{y}}.png';
|
||||
|
||||
|
||||
//create two TileLayer
|
||||
var nautical=new L.TileLayer(osmUrl,{{
|
||||
maxZoom:18}});
|
||||
|
||||
|
||||
L.control.layers({{
|
||||
"Streets": streets,
|
||||
"Satellite": satellite,
|
||||
"Outdoors": outdoors,
|
||||
"Nautical": nautical,
|
||||
}},{{
|
||||
"Navionics":navionics,
|
||||
}},
|
||||
{{
|
||||
position:'topleft'
|
||||
}}).addTo(mymap);
|
||||
|
||||
var marker = L.marker([{latbegin}, {longbegin}]).addTo(mymap);
|
||||
marker.bindPopup("<b>Start</b>");
|
||||
|
||||
var latlongs = {scoordinates}
|
||||
var polyline = L.polyline(latlongs, {{color:'red'}}).addTo(mymap)
|
||||
mymap.fitBounds(polyline.getBounds())
|
||||
|
||||
""".format(
|
||||
latmean=latmean,
|
||||
lonmean=lonmean,
|
||||
latbegin = latbegin,
|
||||
latend=latend,
|
||||
longbegin=longbegin,
|
||||
longend=longend,
|
||||
scoordinates=scoordinates,
|
||||
)
|
||||
|
||||
div = """
|
||||
<div id="map_canvas" style="width: 640px; height: 390px;"><p> </p></div>
|
||||
"""
|
||||
|
||||
|
||||
|
||||
return script,div
|
||||
|
||||
|
||||
def interactive_agegroupcpchart(age,normalized=False):
|
||||
durations = [1,4,30,60]
|
||||
distances = [100,500,1000,2000,5000,6000,10000,21097,42195]
|
||||
@@ -2828,6 +2949,114 @@ def interactive_chart(id=0,promember=0,intervaldata = {}):
|
||||
|
||||
return [script,div]
|
||||
|
||||
def interactive_chart_video(videodata):
|
||||
|
||||
spm = videodata['spm']
|
||||
time = range(len(spm))
|
||||
|
||||
data = zip(time,spm)
|
||||
|
||||
data2 = "["
|
||||
|
||||
for t,s in data:
|
||||
data2 += "{x: %s, y: %s}, " % (t,s)
|
||||
|
||||
data2 = data2[:-2] + "]"
|
||||
|
||||
markerpoint = {
|
||||
'x': time[0],
|
||||
'y': spm[0],
|
||||
'r': 10,
|
||||
}
|
||||
|
||||
div = """
|
||||
<canvas id="myChart">
|
||||
</canvas>
|
||||
"""
|
||||
|
||||
script = """
|
||||
var ctx = document.getElementById("myChart").getContext('2d');
|
||||
var data = %s
|
||||
|
||||
var myChart = new Chart(ctx, {
|
||||
type: 'scatter',
|
||||
label: 'SPM',
|
||||
animationSteps: 10,
|
||||
options: {
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
animation: {
|
||||
duration: 100,
|
||||
},
|
||||
scales: {
|
||||
yAxes: [{
|
||||
scaleLabel: {
|
||||
display: true,
|
||||
labelString: 'Stroke Rate'
|
||||
}
|
||||
}],
|
||||
xAxes: [{
|
||||
scaleLabel: {
|
||||
type: 'linear',
|
||||
display: true,
|
||||
labelString: 'Time (seconds)'
|
||||
}
|
||||
}],
|
||||
}
|
||||
},
|
||||
data:
|
||||
{
|
||||
datasets: [
|
||||
{
|
||||
type: 'bubble',
|
||||
label: 'now',
|
||||
data: [ %s ],
|
||||
backgroundColor: '#36a2eb',
|
||||
},
|
||||
{
|
||||
label: 'spm',
|
||||
data: data,
|
||||
backgroundColor: "#ff0000",
|
||||
borderColor: "#ff0000",
|
||||
fill: false,
|
||||
borderDash: [0, 0],
|
||||
pointRadius: 1,
|
||||
pointHoverRadius: 1,
|
||||
showLine: true,
|
||||
tension: 0,
|
||||
},
|
||||
|
||||
]
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
var marker = {
|
||||
datapoint: %s ,
|
||||
setLatLng: function (LatLng) {
|
||||
var lat = LatLng.lat;
|
||||
var lng = LatLng.lng;
|
||||
this.datapoint = {
|
||||
'x': lat,
|
||||
'y': lng,
|
||||
'r': 10,
|
||||
}
|
||||
myChart.data.datasets[0].data[0] = this.datapoint;
|
||||
myChart.update();
|
||||
}
|
||||
}
|
||||
marker.setLatLng({
|
||||
'lat': data[0]['x'],
|
||||
'lng': data[0]['y']
|
||||
})
|
||||
|
||||
""" % (data2, markerpoint, markerpoint)
|
||||
|
||||
return [script,div]
|
||||
|
||||
|
||||
|
||||
def interactive_multiflex(datadf,xparam,yparam,groupby,extratitle='',
|
||||
ploterrorbars=False,
|
||||
title=None,binsize=1,colorlegend=[],
|
||||
|
||||
+54
-30
@@ -34,7 +34,6 @@ nometrics = [
|
||||
'workoutid',
|
||||
'totalangle',
|
||||
'hr_bottom',
|
||||
'x_right',
|
||||
'Position',
|
||||
'Extensions',
|
||||
'GPS Speed',
|
||||
@@ -56,7 +55,8 @@ rowingmetrics = (
|
||||
'ax_min': 0,
|
||||
'ax_max': 1e5,
|
||||
'mode':'both',
|
||||
'type': 'basic'}),
|
||||
'type': 'basic',
|
||||
'group':'basic'}),
|
||||
|
||||
('hr',{
|
||||
'numtype':'integer',
|
||||
@@ -65,7 +65,8 @@ rowingmetrics = (
|
||||
'ax_min': 100,
|
||||
'ax_max': 200,
|
||||
'mode':'both',
|
||||
'type': 'basic'}),
|
||||
'type': 'basic',
|
||||
'group':'athlete'}),
|
||||
|
||||
('pace',{
|
||||
'numtype':'float',
|
||||
@@ -74,7 +75,8 @@ rowingmetrics = (
|
||||
'ax_min': 1.0e3*210,
|
||||
'ax_max': 1.0e3*75,
|
||||
'mode':'both',
|
||||
'type': 'basic'}),
|
||||
'type': 'basic',
|
||||
'group': 'basic'}),
|
||||
|
||||
('velo',{
|
||||
'numtype':'float',
|
||||
@@ -84,7 +86,8 @@ rowingmetrics = (
|
||||
'ax_max': 8,
|
||||
'default': 0,
|
||||
'mode':'both',
|
||||
'type':'pro'}),
|
||||
'type':'pro',
|
||||
'group': 'basic'}),
|
||||
|
||||
# ('powerhr',{
|
||||
# 'numtype':'float',
|
||||
@@ -93,7 +96,8 @@ rowingmetrics = (
|
||||
# 'ax_min':0,
|
||||
# 'ax_max':300,
|
||||
# 'mode':'both',
|
||||
# 'type':'pro'}),
|
||||
# 'type':'pro',
|
||||
# 'group':'athlete'}),
|
||||
|
||||
('spm',{
|
||||
'numtype':'float',
|
||||
@@ -102,7 +106,8 @@ rowingmetrics = (
|
||||
'ax_min': 15,
|
||||
'ax_max': 45,
|
||||
'mode':'both',
|
||||
'type': 'basic'}),
|
||||
'type': 'basic',
|
||||
'group':'basic'}),
|
||||
|
||||
('driveenergy',{
|
||||
'numtype':'float',
|
||||
@@ -111,7 +116,8 @@ rowingmetrics = (
|
||||
'ax_min': 0,
|
||||
'ax_max': 1000,
|
||||
'mode':'both',
|
||||
'type': 'pro'}),
|
||||
'type': 'pro',
|
||||
'group':'forcepower'}),
|
||||
|
||||
('power',{
|
||||
'numtype':'float',
|
||||
@@ -120,7 +126,8 @@ rowingmetrics = (
|
||||
'ax_min': 0,
|
||||
'ax_max': 600,
|
||||
'mode':'both',
|
||||
'type': 'basic'}),
|
||||
'type': 'basic',
|
||||
'group':'forcepower'}),
|
||||
|
||||
('averageforce',{
|
||||
'numtype':'float',
|
||||
@@ -129,7 +136,8 @@ rowingmetrics = (
|
||||
'ax_min': 0,
|
||||
'ax_max': 1200,
|
||||
'mode':'both',
|
||||
'type': 'pro'}),
|
||||
'type': 'pro',
|
||||
'group':'forcepower'}),
|
||||
|
||||
('peakforce',{
|
||||
'numtype':'float',
|
||||
@@ -138,7 +146,8 @@ rowingmetrics = (
|
||||
'ax_min': 0,
|
||||
'ax_max': 1500,
|
||||
'mode':'both',
|
||||
'type': 'pro'}),
|
||||
'type': 'pro',
|
||||
'group':'forcepower'}),
|
||||
|
||||
('drivelength',{
|
||||
'numtype':'float',
|
||||
@@ -146,8 +155,9 @@ rowingmetrics = (
|
||||
'verbose_name': 'Drive Length (m)',
|
||||
'ax_min': 0,
|
||||
'ax_max': 2.5,
|
||||
'mode':'both',
|
||||
'type': 'pro'}),
|
||||
'mode':'rower',
|
||||
'type': 'pro',
|
||||
'group':'stroke'}),
|
||||
|
||||
('forceratio',{
|
||||
'numtype':'float',
|
||||
@@ -156,16 +166,18 @@ rowingmetrics = (
|
||||
'ax_min': 0,
|
||||
'ax_max': 1,
|
||||
'mode':'both',
|
||||
'type': 'pro'}),
|
||||
'type': 'pro',
|
||||
'group': 'forcepower'}),
|
||||
|
||||
('distance',{
|
||||
'numtype':'float',
|
||||
'null':True,
|
||||
'verbose_name': 'Distance (m)',
|
||||
'verbose_name': 'Interval Distance (m)',
|
||||
'ax_min': 0,
|
||||
'ax_max': 1e5,
|
||||
'mode':'both',
|
||||
'type': 'basic'}),
|
||||
'type': 'basic',
|
||||
'group':'basic'}),
|
||||
|
||||
('cumdist',{
|
||||
'numtype':'float',
|
||||
@@ -174,7 +186,8 @@ rowingmetrics = (
|
||||
'ax_min': 0,
|
||||
'ax_max': 1e5,
|
||||
'mode':'both',
|
||||
'type': 'basic'}),
|
||||
'type': 'basic',
|
||||
'group':'basic'}),
|
||||
|
||||
('drivespeed',{
|
||||
'numtype':'float',
|
||||
@@ -184,7 +197,8 @@ rowingmetrics = (
|
||||
'ax_max': 4,
|
||||
'default': 0,
|
||||
'mode':'both',
|
||||
'type': 'pro'}),
|
||||
'type': 'pro',
|
||||
'group': 'stroke'}),
|
||||
|
||||
('catch',{
|
||||
'numtype':'float',
|
||||
@@ -194,7 +208,8 @@ rowingmetrics = (
|
||||
'ax_max': -75,
|
||||
'default': 0,
|
||||
'mode':'water',
|
||||
'type': 'pro'}),
|
||||
'type': 'pro',
|
||||
'group': 'stroke'}),
|
||||
|
||||
('slip',{
|
||||
'numtype':'float',
|
||||
@@ -204,7 +219,8 @@ rowingmetrics = (
|
||||
'ax_max': 20,
|
||||
'default': 0,
|
||||
'mode':'water',
|
||||
'type': 'pro'}),
|
||||
'type': 'pro',
|
||||
'group': 'stroke'}),
|
||||
|
||||
('finish',{
|
||||
'numtype':'float',
|
||||
@@ -214,7 +230,8 @@ rowingmetrics = (
|
||||
'ax_max': 55,
|
||||
'default': 0,
|
||||
'mode':'water',
|
||||
'type': 'pro'}),
|
||||
'type': 'pro',
|
||||
'group': 'stroke'}),
|
||||
|
||||
('wash',{
|
||||
'numtype':'float',
|
||||
@@ -224,7 +241,8 @@ rowingmetrics = (
|
||||
'ax_max': 30,
|
||||
'default': 0,
|
||||
'mode':'water',
|
||||
'type': 'pro'}),
|
||||
'type': 'pro',
|
||||
'group': 'stroke'}),
|
||||
|
||||
('peakforceangle',{
|
||||
'numtype':'float',
|
||||
@@ -234,7 +252,8 @@ rowingmetrics = (
|
||||
'ax_max': 50,
|
||||
'default': 0,
|
||||
'mode':'water',
|
||||
'type': 'pro'}),
|
||||
'type': 'pro',
|
||||
'group':'stroke'}),
|
||||
|
||||
|
||||
('totalangle',{
|
||||
@@ -245,7 +264,8 @@ rowingmetrics = (
|
||||
'ax_max': 140,
|
||||
'default': 0,
|
||||
'mode':'water',
|
||||
'type': 'pro'}),
|
||||
'type': 'pro',
|
||||
'group':'stroke'}),
|
||||
|
||||
|
||||
('effectiveangle',{
|
||||
@@ -256,7 +276,8 @@ rowingmetrics = (
|
||||
'ax_max': 140,
|
||||
'default': 0,
|
||||
'mode':'water',
|
||||
'type': 'pro'}),
|
||||
'type': 'pro',
|
||||
'group':'stroke'}),
|
||||
|
||||
('rhythm',{
|
||||
'numtype':'float',
|
||||
@@ -266,7 +287,8 @@ rowingmetrics = (
|
||||
'ax_max': 55,
|
||||
'default': 1.0,
|
||||
'mode':'erg',
|
||||
'type': 'pro'}),
|
||||
'type': 'pro',
|
||||
'group':'stroke'}),
|
||||
|
||||
('efficiency',{
|
||||
'numtype':'float',
|
||||
@@ -276,7 +298,8 @@ rowingmetrics = (
|
||||
'ax_max': 110,
|
||||
'default': 0,
|
||||
'mode':'water',
|
||||
'type': 'pro'}),
|
||||
'type': 'pro',
|
||||
'group':'forcepower'}),
|
||||
|
||||
('distanceperstroke',{
|
||||
'numtype':'float',
|
||||
@@ -286,10 +309,13 @@ rowingmetrics = (
|
||||
'ax_max': 15,
|
||||
'default': 0,
|
||||
'mode':'both',
|
||||
'type': 'basic'}),
|
||||
'type': 'basic',
|
||||
'group':'basic'}),
|
||||
|
||||
)
|
||||
|
||||
metricsgroups = list(set([d['group'] for n,d in rowingmetrics]))
|
||||
|
||||
dtypes = {}
|
||||
|
||||
for name,d in rowingmetrics:
|
||||
@@ -395,5 +421,3 @@ def calc_trimp(df,sex,hrmax,hrmin,hrftp):
|
||||
hrtss = 100*trimp/trimp1hr
|
||||
|
||||
return trimp,hrtss
|
||||
|
||||
|
||||
|
||||
@@ -3720,3 +3720,18 @@ class BlogPost(models.Model):
|
||||
title = models.TextField(max_length=300)
|
||||
link = models.TextField(max_length=300)
|
||||
date = models.DateField()
|
||||
|
||||
defaultgroups = ['basic']
|
||||
|
||||
class VideoAnalysis(models.Model):
|
||||
name = models.CharField(default='', max_length=150,blank=True,null=True)
|
||||
video_id = models.CharField(default='',max_length=150)
|
||||
delay = models.IntegerField(default=0)
|
||||
workout = models.ForeignKey(Workout, on_delete=models.CASCADE)
|
||||
metricsgroups = TemplateListField(default=defaultgroups)
|
||||
|
||||
class Meta:
|
||||
unique_together = ('video_id','workout')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@@ -0,0 +1,360 @@
|
||||
{% extends "newbase.html" %}
|
||||
{% load staticfiles %}
|
||||
{% load rowerfilters %}
|
||||
{% load i18n %}
|
||||
{% load leaflet_tags %}
|
||||
|
||||
|
||||
|
||||
{% block title %}Workout Video{% endblock %}
|
||||
|
||||
{% block meta %}
|
||||
{% leaflet_js %}
|
||||
{% leaflet_css %}
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.2/Chart.js"></script>
|
||||
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$('form').submit(function(e) {
|
||||
$(':disabled').each(function(e) {
|
||||
$(this).removeAttr('disabled');
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
|
||||
<style>
|
||||
.slidecontainer {
|
||||
width: 100%; /* Width of the outside container */
|
||||
}
|
||||
|
||||
.bold { font-weight: bold; }
|
||||
|
||||
/* The slider itself */
|
||||
.slider {
|
||||
-webkit-appearance: none; /* Override default CSS styles */
|
||||
appearance: none;
|
||||
width: 100%; /* Full-width */
|
||||
height: 25px; /* Specified height */
|
||||
background: #d3d3d3; /* Grey background */
|
||||
outline: none; /* Remove outline */
|
||||
opacity: 0.7; /* Set transparency (for mouse-over effects on hover) */
|
||||
-webkit-transition: .2s; /* 0.2 seconds transition on hover */
|
||||
transition: opacity .2s;
|
||||
}
|
||||
|
||||
/* Mouse-over effects */
|
||||
.slider:hover {
|
||||
opacity: 1; /* Fully shown on mouse-over */
|
||||
}
|
||||
|
||||
/* The slider handle (use -webkit- (Chrome, Opera, Safari, Edge) and -moz- (Firefox) to override default look) */
|
||||
.slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none; /* Override default look */
|
||||
appearance: none;
|
||||
width: 25px; /* Set a specific slider handle width */
|
||||
height: 25px; /* Slider handle height */
|
||||
background: #4CAF50; /* Green background */
|
||||
cursor: pointer; /* Cursor on hover */
|
||||
}
|
||||
|
||||
.slider::-moz-range-thumb {
|
||||
width: 25px; /* Set a specific slider handle width */
|
||||
height: 25px; /* Slider handle height */
|
||||
background: #4CAF50; /* Green background */
|
||||
cursor: pointer; /* Cursor on hover */
|
||||
}
|
||||
</style>
|
||||
|
||||
{% language 'en' %}
|
||||
<h1>Video Analysis for {{ workout.name }}</h1>
|
||||
<ul class="main-content">
|
||||
{% if user.is_authenticated and user == workout.user.user and not locked %}
|
||||
<li class="grid_2">
|
||||
<p>Paste link to you tube video below</p>
|
||||
<p>Use the slider to locate start point for video on workout map</p>
|
||||
<p>Playing the video will advance the data in synchonization. Use the regular YouTube
|
||||
controls
|
||||
to move around in the video and play it.</p>
|
||||
<p>You can make manual adjustments to the delay to fine tune the alignment.
|
||||
Once you are finished, check "Lock Video and Data" to lock the video and the data.</p>
|
||||
<p>Once you are happy with the alignment, you can save the analysis, and share with other people.</p>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="grid_2">
|
||||
<p>Playing the video will advance the data in synchronization. Use the regular
|
||||
YouTube controls to move around in the video and play it.</p>
|
||||
{% endif %}
|
||||
</ul>
|
||||
<ul class="main-content">
|
||||
<li class="grid_2">
|
||||
<div id="theplot" class="flexplot mapdiv">
|
||||
{{ mapdiv | safe}}
|
||||
|
||||
</div>
|
||||
<div class="slidecontainer">
|
||||
<input type="range" min="0" max="{{ maxtime }}" value="{{ analysis.delay }}"
|
||||
id="myRange">
|
||||
</div>
|
||||
</li>
|
||||
<li class="grid_2">
|
||||
<div id="player"></div>
|
||||
<script>
|
||||
// 1. Code for the map
|
||||
{{ mapscript | safe }}
|
||||
|
||||
|
||||
// 2. This code loads the IFrame Player API code asynchronously.
|
||||
var tag = document.createElement('script');
|
||||
|
||||
tag.src = "https://www.youtube.com/iframe_api";
|
||||
var firstScriptTag = document.getElementsByTagName('script')[0];
|
||||
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
|
||||
|
||||
// 3. This function creates an <iframe> (and YouTube player)
|
||||
// after the API code downloads.
|
||||
var player;
|
||||
var playing;
|
||||
var videotime = 0;
|
||||
var data = JSON.parse('{{ data|safe }}');
|
||||
{% for id, metric in metrics.items %}
|
||||
var {{ id }}_values = data["{{ id }}"];
|
||||
// console.log("{{ id }}_values",{{ id }}_values);
|
||||
{% endfor %}
|
||||
// var boatspeed = data["boatspeed"];
|
||||
var latitude = data["latitude"];
|
||||
var longitude = data["longitude"];
|
||||
// var spm = data["spm"];
|
||||
// var ctch = data["catch"];
|
||||
|
||||
function onYouTubeIframeAPIReady() {
|
||||
player = new YT.Player('player', {
|
||||
height: '315',
|
||||
width: '560',
|
||||
modestbranding: '1',
|
||||
videoId: '{{ video_id }}',
|
||||
events: {
|
||||
'onReady': onPlayerReady,
|
||||
'onStateChange': onPlayerStateChange
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 4. The API will call this function when the video player is ready.
|
||||
function onPlayerReady(event) {
|
||||
// event.target.playVideo();
|
||||
|
||||
|
||||
}
|
||||
|
||||
function updateTime() {
|
||||
var oldTime = videotime;
|
||||
var slider = document.getElementById("myRange");
|
||||
var lock = document.getElementById("lock");
|
||||
if(player && player.getCurrentTime) {
|
||||
videotime = player.getCurrentTime();
|
||||
var delay = document.getElementById("id_delay").value;
|
||||
sliderpos = Math.round(videotime) + Math.round(delay);
|
||||
slider.value = sliderpos;
|
||||
|
||||
var datatime = parseFloat(videotime)+parseFloat(delay);
|
||||
// velo = boatspeed[Math.round(datatime)];
|
||||
lat = latitude[Math.round(datatime)];
|
||||
lon = longitude[Math.round(datatime)];
|
||||
// strokerate = spm[Math.round(datatime)];
|
||||
// catchangle = ctch[Math.round(datatime)];
|
||||
{% for id, metric in metrics.items %}
|
||||
{{ id }}_now = {{ id }}_values[Math.round(datatime)];
|
||||
// console.log(datatime,{{ id }}_now, "{{ metric.name }}")
|
||||
{% endfor %}
|
||||
|
||||
document.getElementById("time").innerHTML = Math.round(videotime);
|
||||
document.getElementById("datatime").innerHTML = Math.round(datatime);
|
||||
// document.getElementById("speed").innerHTML = velo;
|
||||
// document.getElementById("spm").innerHTML = strokerate;
|
||||
// document.getElementById("catch").innerHTML = catchangle;
|
||||
{% for id, metric in metrics.items %}
|
||||
document.getElementById("{{ id }}").innerHTML = {{ id }}_now;
|
||||
document.getElementById("{{ id }}").className = 'bold';
|
||||
{% endfor %}
|
||||
{% for group in metricsgroups %}
|
||||
try {
|
||||
set_{{ group }}();
|
||||
} catch (e) {}
|
||||
{% endfor %}
|
||||
// gauge.set(catch_now);
|
||||
try {
|
||||
var newLatLng = new L.LatLng(lat, lon);
|
||||
// console.log(newLatLng);
|
||||
marker.setLatLng(newLatLng);
|
||||
} catch (e) {}
|
||||
}
|
||||
if(videotime !== oldTime) {
|
||||
onProgress(videotime);
|
||||
}
|
||||
}
|
||||
|
||||
timeupdater = setInterval(updateTime, 1000);
|
||||
|
||||
// when the time changes, this will be called.
|
||||
function onProgress(currentTime) {
|
||||
var slider = document.getElementById("myRange");
|
||||
var lock = document.getElementById("lock");
|
||||
videotime = player.getCurrentTime();
|
||||
var delay = document.getElementById("id_delay").value;
|
||||
sliderpos = Math.round(videotime) + Math.round(delay);
|
||||
slider.value = sliderpos;
|
||||
}
|
||||
|
||||
function stopVideo() {
|
||||
player.stopVideo();
|
||||
}
|
||||
|
||||
// call this function when player state changes
|
||||
function onPlayerStateChange(event) {
|
||||
if (event.data == YT.PlayerState.PLAYING) {
|
||||
playing = false;
|
||||
} else {
|
||||
playing = true;
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
</li>
|
||||
{% if user.is_authenticated and user == workout.user.user %}
|
||||
<li class="grid_4">
|
||||
<input type="checkbox" name="lock" id="lock" value="Lock">Lock Data and Video
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="grid_4">
|
||||
<input type="hidden" name="lock" id="lock" value="Lock">
|
||||
</li>
|
||||
{% endif %}
|
||||
<li>
|
||||
Data Time
|
||||
<span id="datatime">
|
||||
</span> seconds
|
||||
</li>
|
||||
<li>
|
||||
Video Time
|
||||
<span id="time">
|
||||
</span> seconds
|
||||
</li>
|
||||
{% for id, metric in metrics.items %}
|
||||
<li>
|
||||
{{ metric.name }}
|
||||
<span id="{{ id }}">
|
||||
</span> {{ metric.unit }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% for group in metricsgroups %}
|
||||
<li class="grid">
|
||||
<canvas id="{{ group }}"></canvas>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<p> </p>
|
||||
<form enctype="multipart/form-data" action="" method="post">
|
||||
<ul class="main-content">
|
||||
{% if form and user.is_authenticated and user == workout.user.user %}
|
||||
<li class="grid_2">
|
||||
<table>
|
||||
{{ form.as_table }}
|
||||
</table>
|
||||
{% csrf_token %}
|
||||
{% if not analysis.id %}
|
||||
<input type="submit" name="reload_button" value="Reload">
|
||||
{% endif %}
|
||||
<input type="submit" name="save_button" value="Save">
|
||||
</li>
|
||||
<li class="grid_2">
|
||||
<table>
|
||||
{{ metricsform.as_table }}
|
||||
</table>
|
||||
</li>
|
||||
{% else %}
|
||||
<input type="hidden" id="id_delay" value="{{ analysis.delay }}">
|
||||
{% endif %}
|
||||
<li>
|
||||
{% if analysis and user.is_authenticated and user == analysis.workout.user.user %}
|
||||
<p>
|
||||
<a href="/rowers/video/{{ analysis.id }}/delete/">Delete Analysis</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
</li>
|
||||
</ul>
|
||||
</form>
|
||||
<script>
|
||||
$(document).ready( function() {
|
||||
// lock
|
||||
var lock = document.getElementById("lock");
|
||||
var output = document.getElementById("id_delay");
|
||||
{% if locked %}
|
||||
lock.checked = true;
|
||||
output.disabled = true;
|
||||
{% endif %}
|
||||
|
||||
|
||||
// slider
|
||||
var slider = document.getElementById("myRange");
|
||||
{% if user.is_authenticated and user == workout.user.user %}
|
||||
document.getElementById("myRange").style.display = "block";
|
||||
{% else %}
|
||||
document.getElementById("myRange").style.display = "none";
|
||||
{% endif %}
|
||||
var output = document.getElementById("id_delay");
|
||||
try {
|
||||
output.value = Math.round(slider.value)-Math.round(player.getCurrentTime()); // Display the default slider value
|
||||
}
|
||||
catch(err) {
|
||||
output.value = Math.round(slider.value);
|
||||
}
|
||||
// Update the current slider value (each time you drag the slider handle)
|
||||
slider.oninput = function() {
|
||||
clearInterval(timeupdater)
|
||||
if (lock.checked) {
|
||||
if (this.value-output.value > 0) {
|
||||
player.seekTo(this.value-output.value);
|
||||
} else {
|
||||
if (playing) {
|
||||
player.seekTo(0);
|
||||
player.playVideo();
|
||||
}
|
||||
else {
|
||||
player.seekTo(0);
|
||||
player.pauseVideo();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// console.log('changing');
|
||||
output.value = this.value-Math.round(player.getCurrentTime());
|
||||
}
|
||||
timeupdater = setInterval(updateTime, 1000)
|
||||
}
|
||||
|
||||
output.oninput = function() {
|
||||
slider.value = this.value+Math.round(player.getCurrentTime());
|
||||
}
|
||||
|
||||
// lock delay form field if checkbox checked
|
||||
lock.oninput = function() {
|
||||
if (this.checked) {
|
||||
output.disabled = true;
|
||||
} else {
|
||||
output.disabled = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<script type="text/javascript" src="https://bernii.github.io/gauge.js/dist/gauge.js"></script>
|
||||
<script type="text/javascript" src="{% static 'js/videogauges.js' %}"></script>
|
||||
{% endlanguage %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% include 'menu_workout.html' %}
|
||||
{% endblock %}
|
||||
@@ -12,7 +12,6 @@
|
||||
<h1>Recent Graphs</h1>
|
||||
|
||||
<ul class="main-content">
|
||||
{% if graphs %}
|
||||
<li class="grid_2">
|
||||
<form id="searchform" action="."
|
||||
method="get" accept-charset="utf-8">
|
||||
@@ -20,6 +19,8 @@
|
||||
<input type="submit" value="GO"></input>
|
||||
</form>
|
||||
</li>
|
||||
{% if graphs %}
|
||||
|
||||
<li class="grid_2">
|
||||
<p>
|
||||
<span>
|
||||
@@ -28,7 +29,7 @@
|
||||
<a href="?page=1&q={{ request.GET.q }}">
|
||||
<i class="fas fa-arrow-alt-to-left"></i>
|
||||
</a>
|
||||
<a href="?page={{ workouts.previous_page_number }}&q={{ request.GET.q }}">
|
||||
<a href="?page={{ graphs.previous_page_number }}&q={{ request.GET.q }}">
|
||||
<i class="fas fa-arrow-alt-left"></i>
|
||||
</a>
|
||||
{% else %}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
{% extends "newbase.html" %}
|
||||
{% load staticfiles %}
|
||||
{% load rowerfilters %}
|
||||
|
||||
{% block title %}Rowsandall Recent Video Analyses{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{% include "monitorjobs.html" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<h1>Recent videos</h1>
|
||||
|
||||
<ul class="main-content">
|
||||
<li class="grid_2">
|
||||
<form id="searchform" action="."
|
||||
method="get" accept-charset="utf-8">
|
||||
{{ searchform }}
|
||||
<input type="submit" value="GO"></input>
|
||||
</form>
|
||||
</li>
|
||||
{% if analyses %}
|
||||
|
||||
<li class="grid_2">
|
||||
<p>
|
||||
<span>
|
||||
{% if analyses.has_previous %}
|
||||
{% if request.GET.q %}
|
||||
<a href="?page=1&q={{ request.GET.q }}">
|
||||
<i class="fas fa-arrow-alt-to-left"></i>
|
||||
</a>
|
||||
<a href="?page={{ analyses.previous_page_number }}&q={{ request.GET.q }}">
|
||||
<i class="fas fa-arrow-alt-left"></i>
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="?page=1">
|
||||
<i class="fas fa-arrow-alt-to-left"></i>
|
||||
</a>
|
||||
<a href="?page={{ analyses.previous_page_number }}">
|
||||
<i class="fas fa-arrow-alt-left"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<span>
|
||||
Page {{ analyses.number }} of {{ analyses.paginator.num_pages }}.
|
||||
</span>
|
||||
|
||||
{% if analyses.has_next %}
|
||||
{% if request.GET.q %}
|
||||
<a href="?page={{ analyses.next_page_number }}&q={{ request.GET.q }}">
|
||||
<i class="fas fa-arrow-alt-right"></i>
|
||||
</a>
|
||||
<a href="?page={{ analyses.paginator.num_pages }}&q={{ request.GET.q }}">
|
||||
<i class="fas fa-arrow-alt-to-right">
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="?page={{ analyses.next_page_number }}">
|
||||
<i class="fas fa-arrow-alt-right"></i>
|
||||
</a>
|
||||
<a href="?page={{ analyses.paginator.num_pages }}">
|
||||
<i class="fas fa-arrow-alt-to-right"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</span>
|
||||
</p>
|
||||
</li>
|
||||
{% for video in analyses %}
|
||||
<li>
|
||||
<div>{{ video.name }}</div>
|
||||
<a href="/rowers/video/{{ video.id|encode }}/">
|
||||
<img src="https://img.youtube.com/vi/{{ video.video_id }}/hqdefault.jpg"/>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<li class="grid_4">
|
||||
<p>
|
||||
No videos found
|
||||
</p>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% include 'menu_analytics.html' %}
|
||||
{% endblock %}
|
||||
@@ -2,6 +2,24 @@
|
||||
{% load rowerfilters %}
|
||||
<h1><a href="/rowers/analysis/">Analysis</a></h1>
|
||||
<ul class="cd-accordion-menu animated">
|
||||
<li class="has-children" id="videos">
|
||||
<input type="checkbox" name="group-videos" id="group-videos" checked>
|
||||
<label for="group-videos">
|
||||
<i class="fas fa-film fa-fw"></i> Video Analysis
|
||||
</label>
|
||||
<ul>
|
||||
<li id="videos-analyses">
|
||||
<a href="/rowers/videos/">
|
||||
<i class="fas fa-film fa-fw"></i> Video Analyses
|
||||
</a>
|
||||
</li>
|
||||
<li id="video-add">
|
||||
<a href="/rowers/add-video/">
|
||||
<i class="fas fa-video-plus fa-fw"></i> New Video Analysis
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="has-children" id="fitness">
|
||||
<input type="checkbox" name="group-fitness" id="group-fitness" checked>
|
||||
<label for="group-fitness">
|
||||
|
||||
@@ -45,6 +45,11 @@
|
||||
<i class="fas fa-balance-scale fa-fw"></i> Compare
|
||||
</a>
|
||||
</li>
|
||||
<li id="video-analysis">
|
||||
<a href="/rowers/workout/{{ workout.id|encode }}/video/">
|
||||
<i class="fas fa-video-plus fa-fw"></i> Video Analysis
|
||||
</a>
|
||||
</li>
|
||||
{% if user.is_authenticated and workout|may_edit:request %}
|
||||
<li id="chart-image">
|
||||
<a href="/rowers/workout/{{ workout.id|encode }}/image/">
|
||||
|
||||
@@ -8,6 +8,3 @@
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% load rowerfilters %}
|
||||
{% load leaflet_tags %}
|
||||
<div class="grid_2 alpha">
|
||||
<p>
|
||||
<a class="button gray small" href="/rowers/workout/{{ workout.id|encode }}/map">Map View</a>
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
{% extends "newbase.html" %}
|
||||
{% load staticfiles %}
|
||||
{% load rowerfilters %}
|
||||
|
||||
{% block title %}Delete Graph Image {% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<ul class="main-content">
|
||||
<li class="grid_2">
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
<p>Are you sure you want to delete this video analysis?</p>
|
||||
<p>
|
||||
<input class="button red" type="submit" value="Confirm">
|
||||
</p>
|
||||
</form>
|
||||
</li>
|
||||
<li class="grid_2">
|
||||
<a href="/rowers/video/{{ object.id|encode }}">
|
||||
<img src="https://img.youtube.com/vi/{{ object.video_id }}/hqdefault.jpg"/>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% include 'menu_workouts.html' %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,117 @@
|
||||
{% extends "newbase.html" %}
|
||||
{% load staticfiles %}
|
||||
{% load rowerfilters %}
|
||||
|
||||
{% block title %}Workouts{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<script>
|
||||
function toggle(source) {
|
||||
checkboxes = document.querySelectorAll("input[name='workouts']");
|
||||
for(var i=0, n=checkboxes.length;i<n;i++) {
|
||||
checkboxes[i].checked = source.checked;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script src="https://code.jquery.com/jquery-1.9.1.min.js"></script>
|
||||
<script>
|
||||
$(function() {
|
||||
|
||||
// Get the form fields and hidden div
|
||||
var modality = $("#id_modality");
|
||||
var hidden = $("#id_waterboattype");
|
||||
|
||||
|
||||
// Hide the fields.
|
||||
// Use JS to do this in case the user doesn't have JS
|
||||
// enabled.
|
||||
|
||||
hidden.hide();
|
||||
|
||||
if (modality.val() == 'water') {
|
||||
hidden.show();
|
||||
}
|
||||
|
||||
|
||||
// Setup an event listener for when the state of the
|
||||
// checkbox changes.
|
||||
modality.change(function() {
|
||||
// Check to see if the checkbox is checked.
|
||||
// If it is, show the fields and populate the input.
|
||||
// If not, hide the fields.
|
||||
var Value = modality.val();
|
||||
var otwtypes = ['water','coastal','c-boat','churchboat']
|
||||
if (otwtypes.includes(Value)) {
|
||||
// Show the hidden fields.
|
||||
hidden.show();
|
||||
} else {
|
||||
// Make sure that the hidden fields are indeed
|
||||
// hidden.
|
||||
hidden.hide();
|
||||
|
||||
// You may also want to clear the value of the
|
||||
// hidden fields here. Just in case somebody
|
||||
// shows the fields, enters data to them and then
|
||||
// unticks the checkbox.
|
||||
//
|
||||
// This would do the job:
|
||||
//
|
||||
// $("#hidden_field").val("");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<ul class="main-content">
|
||||
<li class="grid_4">
|
||||
<p>Select the workout you want to add a video to</p>
|
||||
</li>
|
||||
<li class="grid_2 maxheight">
|
||||
<form id="searchform" action=""
|
||||
method="get" accept-charset="utf-8">
|
||||
{{ searchform }}
|
||||
<input type="submit" value="GO"></input>
|
||||
</form>
|
||||
<form enctype="multipart/form-data" action="" method="post">
|
||||
|
||||
{% if workouts %}
|
||||
|
||||
<table width="100%" class="listtable">
|
||||
{{ form.as_table }}
|
||||
</table>
|
||||
{% else %}
|
||||
<p> No workouts found </p>
|
||||
{% endif %}
|
||||
</li>
|
||||
<li class="grid_2">
|
||||
<form enctype="multipart/form-data" method="post">
|
||||
<table>
|
||||
{{ dateform.as_table }}
|
||||
</table>
|
||||
{% csrf_token %}
|
||||
<input name='optionsform' type="submit" value="Submit">
|
||||
<input name='workoutform' type='submit' value="Create Video">
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{% if request.method == 'POST' %}
|
||||
<script type='text/javascript'
|
||||
src='https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js'>
|
||||
</script>
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% include 'menu_analytics.html' %}
|
||||
{% endblock %}
|
||||
@@ -171,6 +171,14 @@ $('#id_workouttype').change();
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% for video in videos %}
|
||||
<li>
|
||||
<div>{{ video.name }}</div>
|
||||
<a href="/rowers/video/{{ video.id|encode }}/">
|
||||
<img src="https://img.youtube.com/vi/{{ video.video_id }}/hqdefault.jpg"/>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -141,6 +141,16 @@ def mocked_read_df_sql(id):
|
||||
|
||||
return df
|
||||
|
||||
def mocked_get_video_data(*args, **kwargs):
|
||||
with open('rowers/tests/testdata/videodata.json','r') as infile:
|
||||
data = json.load(infile)
|
||||
|
||||
maxtime = 4135.
|
||||
with open('rowers/tests/testdata/videometrics.json','r') as infile:
|
||||
metrics = json.load(infile)
|
||||
|
||||
return data,metrics,maxtime
|
||||
|
||||
def mocked_getrowdata_db(*args, **kwargs):
|
||||
df = pd.read_csv('rowers/tests/testdata/getrowdata_mock.csv')
|
||||
|
||||
|
||||
@@ -256,12 +256,14 @@ class URLTests(TestCase):
|
||||
@patch('rowers.dataprep.getsmallrowdata_db')
|
||||
@patch('requests.get',side_effect=mocked_requests)
|
||||
@patch('requests.post',side_effect=mocked_requests)
|
||||
@patch('rowers.dataprep.get_video_data',side_effect=mocked_get_video_data)
|
||||
def test_url_generator(self,url,expected,
|
||||
mocked_sqlalchemy,
|
||||
mocked_read_df_sql,
|
||||
mocked_getsmallrowdata_db,
|
||||
mock_get,
|
||||
mock_post):
|
||||
mock_post,
|
||||
mocked_get_video_data):
|
||||
|
||||
if url not in tested:
|
||||
login = self.c.login(username='john',password='koeinsloot')
|
||||
@@ -294,5 +296,3 @@ class URLTests(TestCase):
|
||||
[200,302,301])
|
||||
else:
|
||||
tested.append(u)
|
||||
|
||||
|
||||
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
+1
@@ -0,0 +1 @@
|
||||
{"boatspeed": {"name": "Boat Speed (m/s)", "metric": "velo", "unit": ""}, "cumdist": {"name": "Cumulative Distance (m)", "metric": "cumdist", "unit": ""}, "distance": {"name": "Distance (m)", "metric": "distance", "unit": ""}, "distanceperstroke": {"name": "Distance per Stroke (m)", "metric": "distanceperstroke", "unit": ""}, "pace": {"name": "Pace (/500m)", "metric": "pace", "unit": ""}, "spm": {"name": "Stroke Rate (spm)", "metric": "spm", "unit": ""}}
|
||||
@@ -524,6 +524,7 @@ def do_sync(w,options):
|
||||
if ('upload_to_Strava' in options and not options['upload_to_Strava']):
|
||||
pass
|
||||
else:
|
||||
if options['stravaid'] != 0:
|
||||
try:
|
||||
message,id = stravastuff.workout_strava_upload(
|
||||
w.user.user,w
|
||||
|
||||
+7
-1
@@ -104,7 +104,6 @@ def permissiondenied_view(request):
|
||||
|
||||
|
||||
def filenotfound_view(request):
|
||||
print('aapje')
|
||||
return rowers.views.error403_view(request)
|
||||
|
||||
def response_error_handler(request, exception=None):
|
||||
@@ -344,6 +343,13 @@ urlpatterns = [
|
||||
re_path(r'^workout/(?P<id>\b[0-9A-Fa-f]+\b)/split/$',views.workout_split_view,name='workout_split_view'),
|
||||
# re_path(r'^workout/(?P<id>\d+)/interactiveplot/$',views.workout_biginteractive_view),
|
||||
re_path(r'^workout/(?P<id>\b[0-9A-Fa-f]+\b)/view/$',views.workout_view,name='workout_view'),
|
||||
re_path(r'^workout/(?P<id>\b[0-9A-Fa-f]+\b)/video/$',views.workout_video_create_view,
|
||||
name='workout_video_create_view'),
|
||||
re_path(r'^video/(?P<pk>\d+)/delete/$',views.VideoDelete.as_view(),name='video_delete'),
|
||||
re_path(r'^video/(?P<id>\w.+)/$',views.workout_video_view,
|
||||
name='workout_video_view'),
|
||||
re_path(r'^videos/',views.list_videos,name='list_videos'),
|
||||
re_path(r'^add-video/',views.video_selectworkout,name='video_selectworkout'),
|
||||
# re_path(r'^workout/(?P<id>\d+)/$',views.workout_view,name='workout_view'),
|
||||
re_path(r'^workout/(?P<id>\b[0-9A-Fa-f]+\b)/$',views.workout_view,name='workout_view'),
|
||||
re_path(r'^workout/fusion/(?P<id1>\b[0-9A-Fa-f]+\b)/(?P<id2>\b[0-9A-Fa-f]+\b)/$',views.workout_fusion_view,name='workout_fusion_view'),
|
||||
|
||||
@@ -58,7 +58,9 @@ from rowers.forms import (
|
||||
RaceResultFilterForm,PowerIntervalUpdateForm,FlexAxesForm,
|
||||
FlexOptionsForm,DataFrameColumnsForm,OteWorkoutTypeForm,
|
||||
MetricsForm,DisqualificationForm,disqualificationreasons,
|
||||
disqualifiers,SearchForm,BillingForm,PlanSelectForm
|
||||
disqualifiers,SearchForm,BillingForm,PlanSelectForm,
|
||||
VideoAnalysisCreateForm,WorkoutSingleSelectForm,
|
||||
VideoAnalysisMetricsForm,
|
||||
)
|
||||
|
||||
from django.urls import reverse, reverse_lazy
|
||||
@@ -95,7 +97,8 @@ from rowers.models import (
|
||||
TrainingMesoCycleForm, TrainingMicroCycleForm,
|
||||
RaceLogo,RowerBillingAddressForm,PaidPlan,
|
||||
AlertEditForm, ConditionEditForm,
|
||||
PlannedSessionComment,CoachRequest,CoachOffer,checkaccessplanuser
|
||||
PlannedSessionComment,CoachRequest,CoachOffer,checkaccessplanuser,
|
||||
VideoAnalysis
|
||||
)
|
||||
from rowers.models import (
|
||||
RowerPowerForm,RowerForm,GraphImage,AdvancedWorkoutForm,
|
||||
@@ -115,7 +118,7 @@ from rowers.models import (
|
||||
FavoriteForm,BaseFavoriteFormSet,SiteAnnouncement,BasePlannedSessionFormSet,
|
||||
get_course_timezone,BaseConditionFormSet,
|
||||
)
|
||||
from rowers.metrics import rowingmetrics,defaultfavoritecharts,nometrics
|
||||
from rowers.metrics import rowingmetrics,defaultfavoritecharts,nometrics,metricsgroups
|
||||
from rowers import metrics as metrics
|
||||
from rowers import courses as courses
|
||||
import rowers.uploads as uploads
|
||||
@@ -1288,5 +1291,3 @@ def trydf(df,aantal,column):
|
||||
|
||||
import rowers.teams as teams
|
||||
from rowers.models import C2WorldClassAgePerformance
|
||||
|
||||
|
||||
|
||||
@@ -5,6 +5,267 @@ from __future__ import unicode_literals
|
||||
|
||||
from rowers.views.statements import *
|
||||
import rowers.teams as teams
|
||||
import rowers.mytypes as mytypes
|
||||
import numpy
|
||||
|
||||
from urllib.parse import urlparse, parse_qs
|
||||
|
||||
def default(o):
|
||||
if isinstance(o, numpy.int64): return int(o)
|
||||
if isinstance(o, numpy.int32): return int(o)
|
||||
raise TypeError
|
||||
|
||||
def get_video_id(url):
|
||||
"""Returns Video_ID extracting from the given url of Youtube
|
||||
|
||||
Examples of URLs:
|
||||
Valid:
|
||||
'http://youtu.be/_lOT2p_FCvA',
|
||||
'www.youtube.com/watch?v=_lOT2p_FCvA&feature=feedu',
|
||||
'http://www.youtube.com/embed/_lOT2p_FCvA',
|
||||
'http://www.youtube.com/v/_lOT2p_FCvA?version=3&hl=en_US',
|
||||
'https://www.youtube.com/watch?v=rTHlyTphWP0&index=6&list=PLjeDyYvG6-40qawYNR4juzvSOg-ezZ2a6',
|
||||
'youtube.com/watch?v=_lOT2p_FCvA',
|
||||
|
||||
Invalid:
|
||||
'youtu.be/watch?v=_lOT2p_FCvA',
|
||||
"""
|
||||
|
||||
if url.startswith(('youtu', 'www')):
|
||||
url = 'http://' + url
|
||||
elif 'http' not in url:
|
||||
return url
|
||||
|
||||
query = urlparse(url)
|
||||
|
||||
if 'youtube' in query.hostname:
|
||||
if query.path == '/watch':
|
||||
return parse_qs(query.query)['v'][0]
|
||||
elif query.path.startswith(('/embed/', '/v/')):
|
||||
return query.path.split('/')[2]
|
||||
elif 'youtu.be' in query.hostname:
|
||||
return query.path[1:]
|
||||
else:
|
||||
raise ValueError
|
||||
|
||||
|
||||
# Show a video compared with data
|
||||
def workout_video_view(request,id=''):
|
||||
try:
|
||||
id = encoder.decode_hex(id)
|
||||
analysis = VideoAnalysis.objects.get(id=id)
|
||||
except VideoAnalysis.DoesNotExist:
|
||||
raise Http404("Video Analysis does not exist")
|
||||
|
||||
w = analysis.workout
|
||||
delay = analysis.delay
|
||||
|
||||
if w.workouttype in mytypes.otwtypes:
|
||||
mode = 'water'
|
||||
else:
|
||||
mode = 'erg'
|
||||
|
||||
if request.user.is_authenticated:
|
||||
mayedit = checkworkoutuser(request.user,w) and isprorower(request.user.rower)
|
||||
rower = request.user.rower
|
||||
else:
|
||||
mayedit = False
|
||||
rower = None
|
||||
|
||||
# get video ID and offset
|
||||
if mayedit and request.method == 'POST':
|
||||
form = VideoAnalysisCreateForm(request.POST)
|
||||
metricsform = VideoAnalysisMetricsForm(request.POST,mode=mode)
|
||||
if form.is_valid() and metricsform.is_valid():
|
||||
video_id = form.cleaned_data['url']
|
||||
try:
|
||||
video_id = get_video_id(form.cleaned_data['url'])
|
||||
except (TypeError,ValueError):
|
||||
pass
|
||||
delay = form.cleaned_data['delay']
|
||||
metricsgroups = metricsform.cleaned_data['groups']
|
||||
if 'save_button' in request.POST:
|
||||
analysis.name = form.cleaned_data['name']
|
||||
analysis.video_id = video_id
|
||||
analysis.delay = delay
|
||||
analysis.metricsgroups = metricsgroups
|
||||
analysis.save()
|
||||
else:
|
||||
video_id = id
|
||||
delay = 0
|
||||
elif mayedit:
|
||||
form = VideoAnalysisCreateForm(
|
||||
initial = {
|
||||
'name':analysis.name,
|
||||
'delay': analysis.delay,
|
||||
'url': analysis.video_id,
|
||||
}
|
||||
)
|
||||
metricsform = VideoAnalysisMetricsForm(initial={'groups':analysis.metricsgroups},
|
||||
mode=mode)
|
||||
metricsgroups = analysis.metricsgroups
|
||||
video_id = analysis.video_id
|
||||
else:
|
||||
form = None
|
||||
metricsform = None
|
||||
metricsgroups = analysis.metricsgroups
|
||||
|
||||
data, metrics, maxtime = dataprep.get_video_data(w,groups=metricsgroups,mode=mode)
|
||||
hascoordinates = pd.Series(data['latitude']).std() > 0
|
||||
# create map
|
||||
if hascoordinates:
|
||||
mapscript, mapdiv = leaflet_chart_video(data['latitude'],data['longitude'],
|
||||
w.name)
|
||||
else:
|
||||
mapscript, mapdiv = interactive_chart_video(data)
|
||||
data['longitude'] = data['spm']
|
||||
data['latitude'] = list(range(len(data['spm'])))
|
||||
|
||||
|
||||
breadcrumbs = [
|
||||
{
|
||||
'url':'/rowers/list-workouts/',
|
||||
'name':'Workouts'
|
||||
},
|
||||
{
|
||||
'url':get_workout_default_page(request,encoder.encode_hex(w.id)),
|
||||
'name': w.name
|
||||
},
|
||||
{
|
||||
'url':reverse('workout_video_view',kwargs={'id':encoder.encode_hex(analysis.id)}),
|
||||
'name': 'Video Analysis'
|
||||
}
|
||||
|
||||
]
|
||||
|
||||
|
||||
return render(request,
|
||||
'embedded_video.html',
|
||||
{
|
||||
'workout':w,
|
||||
'rower':rower,
|
||||
'data': json.dumps(data,default=default),
|
||||
'mapscript': mapscript,
|
||||
'mapdiv': mapdiv,
|
||||
'video_id': analysis.video_id,
|
||||
'form':form,
|
||||
'breadcrumbs':breadcrumbs,
|
||||
'analysis':analysis,
|
||||
'maxtime':maxtime,
|
||||
'metrics':metrics,
|
||||
'locked': True,
|
||||
'metricsform':metricsform,
|
||||
'metricsgroups': metricsgroups,
|
||||
})
|
||||
|
||||
|
||||
# Create a video compared with data
|
||||
@user_passes_test(ispromember,login_url="/rowers/paidplans/",
|
||||
message="This functionality requires a Pro plan or higher",
|
||||
redirect_field_name=None)
|
||||
def workout_video_create_view(request,id=0):
|
||||
# get workout
|
||||
w = get_workout_permitted(request.user,id)
|
||||
if w.workouttype in mytypes.otwtypes:
|
||||
mode = 'water'
|
||||
else:
|
||||
mode = 'erg'
|
||||
|
||||
# get video ID and offset
|
||||
if request.method == 'POST':
|
||||
form = VideoAnalysisCreateForm(request.POST)
|
||||
metricsform = VideoAnalysisMetricsForm(request.POST,mode=mode)
|
||||
if form.is_valid() and metricsform.is_valid():
|
||||
url = form.cleaned_data['url']
|
||||
delay = form.cleaned_data['delay']
|
||||
metricsgroups = metricsform.cleaned_data['groups']
|
||||
video_id = get_video_id(url)
|
||||
if 'save_button' in request.POST:
|
||||
analysis = VideoAnalysis(
|
||||
workout=w,
|
||||
name=form.cleaned_data['name'],
|
||||
video_id = video_id,
|
||||
delay=delay,
|
||||
metricsgroups=metricsgroups
|
||||
)
|
||||
try:
|
||||
analysis.save()
|
||||
url = reverse('workout_video_view',
|
||||
kwargs={'id':encoder.encode_hex(analysis.id)})
|
||||
return HttpResponseRedirect(url)
|
||||
except IntegrityError:
|
||||
messages.error(request,'You cannot save two video analysis with the same YouTube video and Workout. Redirecting to your existing analysis')
|
||||
analysis = VideoAnalysis.objects.filter(workout=w,video_id=video_id)
|
||||
if analysis:
|
||||
url = reverse('workout_video_view',
|
||||
kwargs={'id':encoder.encode_hex(analysis[0].id)})
|
||||
else:
|
||||
url = reverse('workout_video_create_view',
|
||||
kwargs={'id':encoder.encode_hex(w.id)})
|
||||
return HttpResponseRedirect(url)
|
||||
else:
|
||||
video_id = None
|
||||
delay = 0
|
||||
metricsgroups = ['basic']
|
||||
else:
|
||||
form = VideoAnalysisCreateForm()
|
||||
metricsform = VideoAnalysisMetricsForm(initial={'groups':['basic']},mode=mode)
|
||||
video_id = None
|
||||
delay = 0
|
||||
metricsgroups = ['basic']
|
||||
|
||||
# get data
|
||||
data, metrics, maxtime = dataprep.get_video_data(w,groups=metricsgroups,mode=mode)
|
||||
hascoordinates = pd.Series(data['latitude']).std() > 0
|
||||
|
||||
# create map
|
||||
if hascoordinates:
|
||||
mapscript, mapdiv = leaflet_chart_video(data['latitude'],data['longitude'],
|
||||
w.name)
|
||||
else:
|
||||
mapscript, mapdiv = interactive_chart_video(data)
|
||||
data['longitude'] = data['spm']
|
||||
data['latitude'] = list(range(len(data['spm'])))
|
||||
|
||||
breadcrumbs = [
|
||||
{
|
||||
'url':'/rowers/list-workouts/',
|
||||
'name':'Workouts'
|
||||
},
|
||||
{
|
||||
'url':get_workout_default_page(request,encoder.encode_hex(w.id)),
|
||||
'name': w.name
|
||||
},
|
||||
{
|
||||
'url':reverse('workout_video_create_view',kwargs={'id':encoder.encode_hex(w.id)}),
|
||||
'name': 'Video Analysis'
|
||||
}
|
||||
|
||||
]
|
||||
|
||||
analysis = {'delay':delay}
|
||||
|
||||
template = 'embedded_video.html'
|
||||
|
||||
|
||||
return render(request,
|
||||
template,
|
||||
{
|
||||
'workout':w,
|
||||
'rower':request.user.rower,
|
||||
'data': json.dumps(data,default=default),
|
||||
'mapscript': mapscript,
|
||||
'mapdiv': mapdiv,
|
||||
'video_id': video_id,
|
||||
'form':form,
|
||||
'metricsform':metricsform,
|
||||
'analysis':analysis,
|
||||
'breadcrumbs':breadcrumbs,
|
||||
'maxtime':maxtime,
|
||||
'metrics':metrics,
|
||||
'metricsgroups': metricsgroups,
|
||||
'locked': False,
|
||||
})
|
||||
|
||||
# Show the EMpower Oarlock generated Stroke Profile
|
||||
@user_passes_test(ispromember,login_url="/rowers/paidplans/",
|
||||
@@ -446,6 +707,205 @@ def workouts_join_view(request):
|
||||
url = reverse('workouts_join_select')
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
defaultoptions = {
|
||||
'includereststrokes': False,
|
||||
'workouttypes':['rower','dynamic','slides'],
|
||||
'waterboattype': mytypes.waterboattype,
|
||||
'rankingonly': False,
|
||||
'function':'boxplot'
|
||||
}
|
||||
|
||||
@user_passes_test(ispromember, login_url="/rowers/paidplans",
|
||||
message="This functionality requires a Pro plan or higher",
|
||||
redirect_field_name=None)
|
||||
def video_selectworkout(request,userid=0,teamid=0):
|
||||
r = getrequestrower(request, userid=userid)
|
||||
user = r.user
|
||||
userid = user.id
|
||||
workouts = Workout.objects.filter(user=r).order_by('-date')
|
||||
|
||||
try:
|
||||
theteam = Team.objects.get(id=teamid)
|
||||
except Team.DoesNotExist:
|
||||
theteam = None
|
||||
|
||||
|
||||
if 'options' in request.session:
|
||||
options = request.session['options']
|
||||
else:
|
||||
options=defaultoptions
|
||||
|
||||
options['userid'] = userid
|
||||
try:
|
||||
workouttypes = options['workouttypes']
|
||||
except KeyError:
|
||||
workouttypes = ['rower','dynamic','slides']
|
||||
|
||||
try:
|
||||
modalities = options['modalities']
|
||||
modality = modalities[0]
|
||||
except KeyError:
|
||||
modalities = [m[0] for m in mytypes.workouttypes_ordered.items()]
|
||||
modality = 'all'
|
||||
|
||||
|
||||
|
||||
try:
|
||||
rankingonly = options['rankingonly']
|
||||
except KeyError:
|
||||
rankingonly = False
|
||||
|
||||
query = request.GET.get('q')
|
||||
if query:
|
||||
query_list = query.split()
|
||||
workouts = workouts.filter(
|
||||
reduce(operator.and_,
|
||||
(Q(name__icontains=q) for q in query_list)) |
|
||||
reduce(operator.and_,
|
||||
(Q(notes__icontains=q) for q in query_list))
|
||||
)
|
||||
searchform = SearchForm(initial={'q':query})
|
||||
else:
|
||||
searchform = SearchForm()
|
||||
|
||||
|
||||
if 'startdate' in request.session:
|
||||
startdate = iso8601.parse_date(request.session['startdate'])
|
||||
else:
|
||||
startdate=timezone.now()-datetime.timedelta(days=42)
|
||||
|
||||
if 'enddate' in request.session:
|
||||
enddate = iso8601.parse_date(request.session['enddate'])
|
||||
else:
|
||||
enddate = timezone.now()
|
||||
|
||||
|
||||
workouts = workouts.filter(date__gte=startdate,date__lte=enddate)
|
||||
|
||||
waterboattype = mytypes.waterboattype
|
||||
|
||||
if request.method == 'POST':
|
||||
thediv = get_call()
|
||||
dateform = DateRangeForm(request.POST)
|
||||
if dateform.is_valid():
|
||||
startdate = dateform.cleaned_data['startdate']
|
||||
enddate = dateform.cleaned_data['enddate']
|
||||
startdatestring = startdate.strftime('%Y-%m-%d')
|
||||
enddatestring = enddate.strftime('%Y-%m-%d')
|
||||
request.session['startdate'] = startdatestring
|
||||
request.session['enddate'] = enddatestring
|
||||
|
||||
form = WorkoutSingleSelectForm(request.POST)
|
||||
if form.is_valid():
|
||||
cd = form.cleaned_data
|
||||
selectedworkout = cd['workout']
|
||||
url = reverse('workout_video_create_view',
|
||||
kwargs={'id':encoder.encode_hex(selectedworkout.id)})
|
||||
return HttpResponseRedirect(url)
|
||||
else:
|
||||
id = 0
|
||||
else:
|
||||
form = WorkoutSingleSelectForm(workouts=workouts)
|
||||
thediv = ''
|
||||
dateform = DateRangeForm(initial={
|
||||
'startdate':startdate,
|
||||
'enddate':enddate,
|
||||
})
|
||||
|
||||
|
||||
|
||||
negtypes = []
|
||||
for b in mytypes.boattypes:
|
||||
if b[0] not in waterboattype:
|
||||
negtypes.append(b[0])
|
||||
|
||||
|
||||
startdate = datetime.datetime.combine(startdate,datetime.time())
|
||||
enddate = datetime.datetime.combine(enddate,datetime.time(23,59,59))
|
||||
|
||||
if enddate < startdate:
|
||||
s = enddate
|
||||
enddate = startdate
|
||||
startdate = s
|
||||
|
||||
negtypes = []
|
||||
for b in mytypes.boattypes:
|
||||
if b[0] not in waterboattype:
|
||||
negtypes.append(b[0])
|
||||
|
||||
|
||||
|
||||
if theteam is not None and (theteam.viewing == 'allmembers' or theteam.manager == request.user):
|
||||
workouts = Workout.objects.filter(team=theteam,
|
||||
startdatetime__gte=startdate,
|
||||
startdatetime__lte=enddate,
|
||||
workouttype__in=modalities,
|
||||
)
|
||||
elif theteam is not None and theteam.viewing == 'coachonly':
|
||||
workouts = Workout.objects.filter(team=theteam,user=r,
|
||||
startdatetime__gte=startdate,
|
||||
startdatetime__lte=enddate,
|
||||
workouttype__in=modalities,
|
||||
)
|
||||
else:
|
||||
workouts = Workout.objects.filter(user=r,
|
||||
startdatetime__gte=startdate,
|
||||
startdatetime__lte=enddate,
|
||||
workouttype__in=modalities,
|
||||
)
|
||||
|
||||
workouts = workouts.order_by(
|
||||
"-date", "-starttime"
|
||||
).exclude(boattype__in=negtypes)
|
||||
|
||||
|
||||
if rankingonly:
|
||||
workouts = workouts.exclude(rankingpiece=False)
|
||||
|
||||
|
||||
|
||||
|
||||
optionsform = AnalysisOptionsForm(initial={
|
||||
'modality':modality,
|
||||
'waterboattype':waterboattype,
|
||||
'rankingonly':rankingonly,
|
||||
})
|
||||
|
||||
|
||||
|
||||
startdatestring = startdate.strftime('%Y-%m-%d')
|
||||
enddatestring = enddate.strftime('%Y-%m-%d')
|
||||
request.session['startdate'] = startdatestring
|
||||
request.session['enddate'] = enddatestring
|
||||
request.session['options'] = options
|
||||
|
||||
|
||||
breadcrumbs = [
|
||||
{
|
||||
'url':'/rowers/analysis',
|
||||
'name':'Analysis'
|
||||
},
|
||||
{
|
||||
'url':reverse('analysis_new',kwargs={'userid':userid}),
|
||||
'name': 'Analysis Select'
|
||||
},
|
||||
]
|
||||
return render(request, 'video_selectworkout.html',
|
||||
{'workouts': workouts,
|
||||
'dateform':dateform,
|
||||
'startdate':startdate,
|
||||
'enddate':enddate,
|
||||
'rower':r,
|
||||
'breadcrumbs':breadcrumbs,
|
||||
'theuser':user,
|
||||
'the_div':thediv,
|
||||
'form':form,
|
||||
'active':'nav-analysis',
|
||||
'searchform':searchform,
|
||||
'teams':get_my_teams(request.user),
|
||||
})
|
||||
|
||||
|
||||
@user_passes_test(ispromember,login_url="/rowers/paidplans",
|
||||
message="This functionality requires a Pro plan or higher. If you are already a Pro user, please log in to access this functionality. If you are already a Pro user, please log in to access this functionality",
|
||||
redirect_field_name=None)
|
||||
@@ -3530,6 +3990,7 @@ def workout_edit_view(request,id=0,message="",successmessage=""):
|
||||
except:
|
||||
pass
|
||||
|
||||
videos = VideoAnalysis.objects.filter(workout=row)
|
||||
|
||||
# create interactive plot
|
||||
f1 = row.csvfilename
|
||||
@@ -3593,6 +4054,7 @@ def workout_edit_view(request,id=0,message="",successmessage=""):
|
||||
'workout':row,
|
||||
'teams':get_my_teams(request.user),
|
||||
'graphs':g,
|
||||
'videos':videos,
|
||||
'breadcrumbs':breadcrumbs,
|
||||
'rower':r,
|
||||
'indoorraces':indoorraces,
|
||||
@@ -4495,6 +4957,45 @@ def team_workout_upload_view(request,message="",
|
||||
|
||||
|
||||
# A page with all the recent graphs (searchable on workout name)
|
||||
@login_required()
|
||||
def list_videos(request):
|
||||
r = getrequestrower(request)
|
||||
workouts = Workout.objects.filter(user=r).order_by("-date", "-starttime")
|
||||
query = request.GET.get('q')
|
||||
if query:
|
||||
query_list = query.split()
|
||||
if query:
|
||||
query_list = query.split()
|
||||
workouts = workouts.filter(
|
||||
reduce(operator.and_,
|
||||
(Q(name__icontains=q) for q in query_list)) |
|
||||
reduce(operator.and_,
|
||||
(Q(notes__icontains=q) for q in query_list))
|
||||
)
|
||||
searchform = SearchForm(initial={'q':query})
|
||||
else:
|
||||
searchform = SearchForm()
|
||||
|
||||
g = VideoAnalysis.objects.filter(workout__in=workouts).order_by("-workout__date")
|
||||
|
||||
|
||||
paginator = Paginator(g,8)
|
||||
page = request.GET.get('page')
|
||||
|
||||
try:
|
||||
g = paginator.page(page)
|
||||
except PageNotAnInteger:
|
||||
g = paginator.page(1)
|
||||
except EmptyPage:
|
||||
g = paginator.page(paginator.num_pages)
|
||||
|
||||
return render(request, 'list_videos.html',
|
||||
{'analyses': g,
|
||||
'searchform':searchform,
|
||||
'active':'nav-analysis',
|
||||
'teams':get_my_teams(request.user),
|
||||
})
|
||||
|
||||
@login_required()
|
||||
def graphs_view(request):
|
||||
request.session['referer'] = reverse('graphs_view')
|
||||
@@ -5254,6 +5755,58 @@ def workout_summary_edit_view(request,id,message="",successmessage=""
|
||||
'formvalues':formvalues,
|
||||
})
|
||||
|
||||
class VideoDelete(DeleteView):
|
||||
login_required = True
|
||||
model = VideoAnalysis
|
||||
template_name = 'video_delete_confirm.html'
|
||||
|
||||
# extra parameters
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(VideoDelete, self).get_context_data(**kwargs)
|
||||
|
||||
breadcrumbs = [
|
||||
{
|
||||
'url':'/rowers/list-workouts/',
|
||||
'name':'Workouts'
|
||||
},
|
||||
{
|
||||
'url':get_workout_default_page(
|
||||
self.request,
|
||||
encoder.encode_hex(self.object.workout.id)),
|
||||
'name': self.object.workout.name
|
||||
},
|
||||
{
|
||||
'url':reverse('workout_video_view',kwargs={'id':encoder.encode_hex(self.object.id)}),
|
||||
'name': 'Video Analysis'
|
||||
},
|
||||
{ 'url':reverse('video_delete',kwargs={'pk':str(self.object.pk)}),
|
||||
'name': 'Delete'
|
||||
}
|
||||
|
||||
]
|
||||
|
||||
context['active'] = 'nav-workouts'
|
||||
context['rower'] = getrower(self.request.user)
|
||||
context['breadcrumbs'] = breadcrumbs
|
||||
|
||||
return context
|
||||
|
||||
|
||||
def get_success_url(self):
|
||||
w = self.object.workout
|
||||
try:
|
||||
w = Workout.objects.get(id=w.id)
|
||||
except Workout.DoesNotExist:
|
||||
return reverse('workouts_view')
|
||||
|
||||
return reverse('workout_edit_view',kwargs={'id':encoder.encode_hex(w.id)})
|
||||
|
||||
def get_object(self, *args, **kwargs):
|
||||
obj = super(VideoDelete, self).get_object(*args, **kwargs)
|
||||
if not checkaccessuser(self.request.user,obj.workout.user):
|
||||
raise PermissionDenied('You are not allowed to delete this analysis')
|
||||
|
||||
return obj
|
||||
|
||||
class GraphDelete(DeleteView):
|
||||
login_required = True
|
||||
|
||||
@@ -77,8 +77,12 @@ CACHES = {
|
||||
|
||||
|
||||
STATIC_URL = '/static/'
|
||||
#STATIC_ROOT = 'C:/python/rowsandallapp'
|
||||
STATIC_ROOT = BASE_DIR
|
||||
#STATIC_ROOT = os.path.join(BASE_DIR, 'static')
|
||||
|
||||
STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static'),
|
||||
os.path.join(BASE_DIR, 'static/plots'),]
|
||||
|
||||
|
||||
INTERNAL_IPS = ['127.0.0.1']
|
||||
|
||||
|
||||
@@ -96,10 +96,16 @@ django.views.i18n.javascript_catalog = None
|
||||
if settings.DEBUG:
|
||||
import debug_toolbar
|
||||
import django
|
||||
urlpatterns += [
|
||||
re_path(r'^static/plots/(?P<path>.*)$',
|
||||
django.views.static.serve,
|
||||
kwargs={'document_root': settings.STATIC_ROOT+'plots/'})
|
||||
]
|
||||
urlpatterns += [
|
||||
# re_path(r'^__debug__/','debug_toolbar.urls'),
|
||||
re_path(r'^static/(?P<path>.*)$',
|
||||
django.views.static.serve,
|
||||
kwargs={'document_root': settings.STATIC_ROOT,}
|
||||
kwargs={'document_root': settings.STATIC_ROOT,
|
||||
'show_indexes': True}
|
||||
)
|
||||
]
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
// var opts = {
|
||||
// lines: 12,
|
||||
// angle: 0.15,
|
||||
// lineWidth: 0.44,
|
||||
// pointer: {
|
||||
// length: 0.9,
|
||||
// strokeWidth: 0.035,
|
||||
// color: '#000000'
|
||||
// },
|
||||
// limitMax: 'false',
|
||||
// // percentColors: [[0.0, "#a9d70b" ], [0.50, "#a9d70b"], [1.0, "#a9d70b"]], // !!!!
|
||||
// strokeColor: '#E0E0E0',
|
||||
// generateGradient: true
|
||||
// };
|
||||
// var target = document.getElementById('angles');
|
||||
// var gauge = new Gauge(target).setOptions(opts);
|
||||
// gauge.maxValue = 90;
|
||||
// gauge.minValue = -90;
|
||||
// gauge.animationSpeed = 5;
|
||||
// gauge.set(-75);
|
||||
|
||||
// https://github.com/bernii/gauge.js/issues/193
|
||||
|
||||
|
||||
var opts = {
|
||||
angle: 0, // The span of the gauge arc
|
||||
lineWidth: 0.2, // The line thickness
|
||||
radiusScale: 0.89, // Relative radius
|
||||
pointer: {
|
||||
length: 0.54, // // Relative to gauge radius
|
||||
strokeWidth: 0.053, // The thickness
|
||||
color: '#000000' // Fill color
|
||||
},
|
||||
limitMax: false, // If false, max value increases automatically if value > maxValue
|
||||
limitMin: false, // If true, the min value of the gauge will be fixed
|
||||
colorStart: '#6FADCF', // Colors
|
||||
colorStop: '#8FC0DA', // just experiment with them
|
||||
strokeColor: '#E0E0E0', // to see which ones work best for you
|
||||
generateGradient: true,
|
||||
highDpiSupport: true, // High resolution support
|
||||
staticZones: [
|
||||
{strokeStyle: "#00FF00", min: 0, max: 2}, // Red from 100 to 60
|
||||
{strokeStyle: "#0000FF", min: 2, max: 3}, // Yellow
|
||||
{strokeStyle: "#00FFFF", min: 3, max: 4}, // Green
|
||||
{strokeStyle: "#FFDD00", min: 4, max: 5}, // Yellow
|
||||
{strokeStyle: "#FF0000", min: 5, max: 6} // Red
|
||||
],
|
||||
|
||||
};
|
||||
var target = document.getElementById('basic'); // your canvas element
|
||||
var gaugeboatspeed = new Gauge(target).setOptions(opts); // create sexy gauge!
|
||||
gaugeboatspeed.maxValue = 6; // set max gauge value
|
||||
gaugeboatspeed.setMinValue(0); // Prefer setter over gauge.minValue = 0
|
||||
gaugeboatspeed.animationSpeed = 10; // set animation speed (32 is default value)
|
||||
gaugeboatspeed.set(0); // set actual value
|
||||
|
||||
// Define set_basic(values) so that gauges can be set by metricsgroups
|
||||
function set_basic() {
|
||||
gaugeboatspeed.set(boatspeed_now);
|
||||
}
|
||||
Reference in New Issue
Block a user