Private
Public Access
1
0
This commit is contained in:
Sander Roosendaal
2019-12-02 08:24:34 +01:00
27 changed files with 2060 additions and 349 deletions
+131 -2
View File
@@ -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:
+3
View File
@@ -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
View File
@@ -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
+229
View File
@@ -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>&nbsp;</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
View File
@@ -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
+15
View File
@@ -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
+360
View File
@@ -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>&nbsp;</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 %}
+3 -2
View File
@@ -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 %}
+90
View File
@@ -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 %}
+18
View File
@@ -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>&nbsp;Video Analysis
</label>
<ul>
<li id="videos-analyses">
<a href="/rowers/videos/">
<i class="fas fa-film fa-fw"></i>&nbsp;Video Analyses
</a>
</li>
<li id="video-add">
<a href="/rowers/add-video/">
<i class="fas fa-video-plus fa-fw"></i>&nbsp;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">
+5
View File
@@ -45,6 +45,11 @@
<i class="fas fa-balance-scale fa-fw"></i>&nbsp;Compare
</a>
</li>
<li id="video-analysis">
<a href="/rowers/workout/{{ workout.id|encode }}/video/">
<i class="fas fa-video-plus fa-fw"></i>&nbsp;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/">
-3
View File
@@ -8,6 +8,3 @@
</div>
</li>
</ul>
+1
View File
@@ -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 %}
+117
View File
@@ -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 %}
+8
View File
@@ -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 %}
+10
View File
@@ -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')
+3 -3
View File
@@ -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)
File diff suppressed because one or more lines are too long
+1
View File
@@ -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": ""}}
+1
View File
@@ -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
View File
@@ -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'),
+6 -5
View File
@@ -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
+553
View File
@@ -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&amp;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
+5 -1
View File
@@ -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']
+7 -1
View File
@@ -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}
)
]
+60
View File
@@ -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);
}