Commit 54e4ea34 authored by Administrator's avatar Administrator

stride ratio on activity level

parent 2cfa760b
......@@ -97,6 +97,8 @@ const tableActivity = SqfEntityTable(
SqfEntityField('sdevFormPower', DbType.real),
SqfEntityField('avgPowerRatio', DbType.real),
SqfEntityField('sdevPowerRatio', DbType.real),
SqfEntityField('avgStrideRatio', DbType.real),
SqfEntityField('sdevStrideRatio', DbType.real),
SqfEntityField('avgStrydCadence', DbType.real),
SqfEntityField('sdevStrydCadence', DbType.real),
SqfEntityField('sdevVerticalOscillation', DbType.real),
......
This diff is collapsed.
......@@ -21,6 +21,8 @@ class Activity extends ChangeNotifier {
double glidingAvgPower;
double glidingAvgPowerPerHeartRate;
double glidingAvgSpeedPerHeartRate;
double glidingAvgPowerRatio;
double glidingAvgStrideRatio;
// intermediate data structures used for parsing
Lap currentLap;
......@@ -55,6 +57,10 @@ class Activity extends ChangeNotifier {
return (db.avgPower / db.avgHeartRate);
case ActivityAttr.avgSpeedPerHeartRate:
return 100 * (db.avgSpeed / db.avgHeartRate);
case ActivityAttr.avgPowerRatio:
return 100 * (db.avgPower - db.avgFormPower) / db.avgPower;
case ActivityAttr.avgStrideRatio:
return 10000 / 6 * db.avgSpeed / db.avgStrydCadence / db.avgVerticalOscillation;
}
}
......@@ -69,6 +75,12 @@ class Activity extends ChangeNotifier {
case ActivityAttr.avgSpeedPerHeartRate:
glidingAvgSpeedPerHeartRate = value;
break;
case ActivityAttr.avgPowerRatio:
glidingAvgPowerRatio = value;
break;
case ActivityAttr.avgStrideRatio:
glidingAvgStrideRatio = value;
break;
}
}
......@@ -309,6 +321,26 @@ class Activity extends ChangeNotifier {
return db.sdevPowerRatio;
}
Future<double> get avgStrideRatio async {
if (db.avgStrideRatio == null) {
List<Event> records = await this.records;
db.avgStrideRatio = Lap.calculateAverageStrideRatio(records: records);
await db.save();
notifyListeners();
}
return db.avgStrideRatio;
}
Future<double> get sdevStrideRatio async {
if (db.sdevStrideRatio == null) {
List<Event> records = await this.records;
db.sdevStrideRatio = Lap.calculateSdevStrideRatio(records: records);
await db.save();
notifyListeners();
}
return db.sdevStrideRatio;
}
Stream<int> parse({@required Athlete athlete}) async* {
int counter = 0;
int percentage;
......
......@@ -206,6 +206,15 @@ class Event {
case LapDoubleAttr.powerRatio:
sum = sum +
((record.db.power - record.db.formPower) / record.db.power * 100);
break;
case LapDoubleAttr.strideRatio:
sum = sum +
(10000 /
6 *
record.db.speed /
record.db.strydCadence /
record.db.verticalOscillation);
break;
}
if (index++ % amount == amount - 1) {
......
......@@ -345,4 +345,37 @@ class Lap {
(record.db.power - record.db.formPower) / record.db.power * 100);
return powerRatios.sdev();
}
static double calculateAverageStrideRatio({List<Event> records}) {
var powerRatios = records
.where((record) =>
record.db.strydCadence != null &&
record.db.verticalOscillation != null &&
record.db.verticalOscillation != 0)
.map((record) =>
10000 /
6 *
record.db.speed /
record.db.strydCadence /
record.db.verticalOscillation);
if (powerRatios.length > 0) {
return powerRatios.mean();
} else
return -1;
}
static double calculateSdevStrideRatio({List<Event> records}) {
var powerRatios = records
.where((record) =>
record.db.strydCadence != null &&
record.db.verticalOscillation != null &&
record.db.verticalOscillation != 0)
.map((record) =>
10000 /
6 *
record.db.speed /
record.db.strydCadence /
record.db.verticalOscillation);
return powerRatios.sdev();
}
}
......@@ -11,6 +11,7 @@ import 'package:encrateia/widgets/activity_widgets/activity_ground_time_widget.d
import 'package:encrateia/widgets/activity_widgets/activity_leg_spring_stiffness_widget.dart';
import 'package:encrateia/widgets/activity_widgets/activity_form_power_widget.dart';
import 'package:encrateia/widgets/activity_widgets/activity_stryd_cadence_widget.dart';
import 'package:encrateia/widgets/activity_widgets/activity_stride_ratio_widget.dart';
import 'package:encrateia/widgets/activity_widgets/activity_vertical_oscillation_widget.dart';
import 'package:encrateia/screens/show_activity_detail_screen.dart';
import 'package:flutter/material.dart';
......@@ -110,32 +111,41 @@ class ShowActivityScreen extends StatelessWidget {
),
]),
TableRow(children: [
detailTile(
title: "Cadence",
icon: MyIcon.cadence,
context: context,
nextWidget: ActivityStrydCadenceWidget(activity: activity),
),
detailTile(
title: "Vertical Oscillation",
icon: MyIcon.verticalOscillation,
context: context,
nextWidget: ActivityVerticalOscillationWidget(activity: activity),
),
detailTile(
title: "Stride Ratio",
icon: MyIcon.strideRatio,
context: context,
nextWidget: ActivityStrideRatioWidget(activity: activity),
),
]),
TableRow(children: [
detailTile(
title: "Cadence",
icon: MyIcon.cadence,
context: context,
nextWidget: ActivityStrydCadenceWidget(activity: activity),
),
detailTile(
title: "Leg Spring Stiffness",
icon: MyIcon.legSpringStiffness,
context: context,
nextWidget: ActivityLegSpringStiffnessWidget(activity: activity),
),
]),
TableRow(children: [
detailTile(
title: "Metadata",
icon: MyIcon.metaData,
context: context,
nextWidget: ActivityMetadataWidget(activity: activity),
),
Text(""),
]),
],
),
......
import 'package:encrateia/widgets/athlete_widgets/athlete_power_ratio_widget.dart';
import 'package:flutter/material.dart';
import 'package:encrateia/models/athlete.dart';
import 'package:encrateia/models/activity.dart';
......@@ -62,7 +63,7 @@ class _ShowAthleteScreenState extends State<ShowAthleteScreen> {
title: "Settings",
icon: MyIcon.settings,
nextWidget: AthleteSettingsWidget(athlete: widget.athlete),
)
),
]),
TableRow(children: [
detailTile(
......@@ -71,18 +72,22 @@ class _ShowAthleteScreenState extends State<ShowAthleteScreen> {
nextWidget: AthletePowerWidget(athlete: widget.athlete),
),
detailTile(
title: "Power / Heart Rate",
title: "Power Ratio",
icon: MyIcon.power,
nextWidget: AthletePowerPerHeartRateWidget(athlete: widget.athlete),
nextWidget: AthletePowerRatioWidget(athlete: widget.athlete),
),
]),
TableRow(children: [
detailTile(
title: "Power / Heart Rate",
icon: MyIcon.power,
nextWidget: AthletePowerPerHeartRateWidget(athlete: widget.athlete),
),
detailTile(
title: "Speed / Heart Rate",
icon: MyIcon.speed,
nextWidget: AthleteSpeedPerHeartRateWidget(athlete: widget.athlete),
),
Text(""),
]),
]),
);
......
......@@ -11,7 +11,8 @@ enum LapDoubleAttr {
strydCadence,
verticalOscillation,
legSpringStiffness,
powerRatio
powerRatio,
strideRatio,
}
enum ActivityAction {
......@@ -25,4 +26,6 @@ enum ActivityAttr {
avgPower,
avgPowerPerHeartRate,
avgSpeedPerHeartRate,
avgPowerRatio,
avgStrideRatio,
}
......@@ -74,6 +74,7 @@ class MyIcon {
static final stravaDownload = Icon(Icons.cloud_download);
static final stravaDownloadWhite =
Icon(Icons.cloud_download, color: Colors.white);
static final strideRatio = Icon(Icons.signal_cellular_null);
static final strides = Icon(Icons.directions_walk);
// T
static final temperature = Icon(Icons.ac_unit);
......
......@@ -3,7 +3,7 @@ import 'package:encrateia/models/activity.dart';
import 'package:encrateia/models/event.dart';
import 'package:encrateia/utils/list_utils.dart';
import 'package:encrateia/utils/num_utils.dart';
import '../charts/actitvity_charts/activity_power_ratio_chart.dart';
import 'package:encrateia/widgets/charts/actitvity_charts/activity_power_ratio_chart.dart';
import 'package:encrateia/utils/icon_utils.dart';
class ActivityPowerRatioWidget extends StatefulWidget {
......
import 'package:flutter/material.dart';
import 'package:encrateia/models/activity.dart';
import 'package:encrateia/models/event.dart';
import 'package:encrateia/utils/list_utils.dart';
import 'package:encrateia/utils/num_utils.dart';
import 'package:encrateia/widgets/charts/actitvity_charts/activity_stride_ratio_chart.dart';
import 'package:encrateia/utils/icon_utils.dart';
class ActivityStrideRatioWidget extends StatefulWidget {
final Activity activity;
ActivityStrideRatioWidget({this.activity});
@override
_ActivityStrideRatioWidgetState createState() => _ActivityStrideRatioWidgetState();
}
class _ActivityStrideRatioWidgetState extends State<ActivityStrideRatioWidget> {
List<Event> records = [];
String avgStrideRatioString = "Loading ...";
String sdevStrideRatioString = "Loading ...";
@override
void initState() {
getData();
super.initState();
}
@override
Widget build(context) {
if (records.length > 0) {
var strideRatioValues = records.map((value) => value.db.verticalOscillation).nonZeroDoubles();
if (strideRatioValues.length > 0) {
return ListTileTheme(
iconColor: Colors.deepOrange,
child: ListView(
padding: EdgeInsets.only(left: 25),
children: <Widget>[
ActivityStrideRatioChart(records: records, activity: widget.activity),
Text("stride ratio = \nstride length (cm) / vertical oscillation (cm)\n"),
Text("stridelength (cm) = \n10 000 / 6 * speed (km/h) / cadence (strides/min)"),
ListTile(
leading: MyIcon.strideRatio,
title: Text(avgStrideRatioString),
subtitle: Text("average stride ratio"),
),
ListTile(
leading: MyIcon.standardDeviation,
title: Text(sdevStrideRatioString),
subtitle: Text("standard deviation stride ratio "),
),
ListTile(
leading: MyIcon.amount,
title: Text(records.length.toString()),
subtitle: Text("number of measurements"),
),
],
),
);
} else {
return Center(
child: Text("No power ratio data available."),
);
}
} else {
return Center(
child: Text("Loading"),
);
}
}
getData() async {
Activity activity = widget.activity;
records = await activity.records;
double avg = await activity.avgStrideRatio;
setState(() {
avgStrideRatioString = avg.toStringOrDashes(1);
});
double sdev = await activity.sdevStrideRatio;
setState(() {
sdevStrideRatioString = sdev.toStringOrDashes(2);
});
}
}
......@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:encrateia/models/athlete.dart';
import 'package:encrateia/models/activity.dart';
import 'package:encrateia/utils/list_utils.dart';
import '../charts/athlete_charts/athlete_power_per_heart_rate_chart.dart';
import 'package:encrateia/widgets/charts/athlete_charts/athlete_power_per_heart_rate_chart.dart';
class AthletePowerPerHeartRateWidget extends StatefulWidget {
final Athlete athlete;
......
import 'package:flutter/material.dart';
import 'package:encrateia/models/athlete.dart';
import 'package:encrateia/models/activity.dart';
import 'package:encrateia/utils/list_utils.dart';
import 'package:encrateia/widgets/charts/athlete_charts/athlete_power_ratio_chart.dart';
class AthletePowerRatioWidget extends StatefulWidget {
final Athlete athlete;
AthletePowerRatioWidget({this.athlete});
@override
_AthletePowerRatioWidgetState createState() => _AthletePowerRatioWidgetState();
}
class _AthletePowerRatioWidgetState extends State<AthletePowerRatioWidget> {
List<Activity> activities = [];
@override
void initState() {
getData();
super.initState();
}
@override
Widget build(context) {
if (activities.length > 0) {
var powerValues = activities.map((value) => value.db.avgPower).nonZeroDoubles();
if (powerValues.length > 0) {
return ListTileTheme(
iconColor: Colors.deepOrange,
child: ListView(
padding: EdgeInsets.only(left: 25),
children: <Widget>[
AthletePowerRatioChart(activities: activities),
],
),
);
} else {
return Center(
child: Text("No power ratio data available."),
);
}
} else {
return Center(
child: Text("Loading"),
);
}
}
getData() async {
Athlete athlete = widget.athlete;
activities = await athlete.activities;
setState(() {});
}
}
import 'package:charts_flutter/flutter.dart';
import 'package:flutter/material.dart';
import 'package:encrateia/models/activity.dart';
import 'package:encrateia/models/event.dart';
import 'package:encrateia/models/lap.dart';
import 'package:encrateia/models/plot_point.dart';
import 'package:encrateia/utils/graph_utils.dart';
import 'package:encrateia/utils/my_line_chart.dart';
import 'package:encrateia/utils/enums.dart';
class ActivityStrideRatioChart extends StatelessWidget {
final List<Event> records;
final Activity activity;
ActivityStrideRatioChart({this.records, @required this.activity});
@override
Widget build(BuildContext context) {
var nonZero = records.where((value) =>
value.db.strydCadence != null &&
value.db.verticalOscillation != null &&
value.db.verticalOscillation != 0);
var smoothedRecords = Event.toDoubleDataPoints(
attribute: LapDoubleAttr.strideRatio,
records: nonZero,
amount: 30,
);
List<Series<dynamic, num>> data = [
Series<DoublePlotPoint, int>(
id: 'Stride Ratio',
colorFn: (_, __) => MaterialPalette.green.shadeDefault,
domainFn: (DoublePlotPoint record, _) => record.domain,
measureFn: (DoublePlotPoint record, _) => record.measure,
data: smoothedRecords,
)
];
return FutureBuilder<List<Lap>>(
future: activity.laps,
builder: (BuildContext context, AsyncSnapshot<List<Lap>> snapshot) {
if (snapshot.hasData) {
var laps = snapshot.data;
return Container(
height: 300,
child: MyLineChart(
data: data,
maxDomain: nonZero.last.db.distance,
laps: laps,
domainTitle: 'Stride Ratio (multiple)',
measureTickProviderSpec: BasicNumericTickProviderSpec(
zeroBound: false,
dataIsInWholeNumbers: false,
desiredTickCount: 5),
domainTickProviderSpec:
BasicNumericTickProviderSpec(desiredTickCount: 6),
),
);
} else
return GraphUtils.loadingContainer;
},
);
}
}
......@@ -15,7 +15,9 @@ class AthletePowerChart extends StatelessWidget {
var nonZeroActivities = activities
.where((activity) =>
activity.db.avgPower != null && activity.db.avgPower > 0)
activity.avgPower != null &&
activity.db.avgPower != null &&
activity.db.avgPower > 0)
.toList();
ActivityList(activities: nonZeroActivities).enrichGlidingAverage(
......
......@@ -15,6 +15,7 @@ class AthletePowerPerHeartRateChart extends StatelessWidget {
var nonZeroActivities = activities
.where((value) =>
value.avgPower != null &&
value.db.avgPower != null &&
value.db.avgPower > 0 &&
value.db.avgHeartRate != null &&
......
import 'package:charts_flutter/flutter.dart';
import 'package:flutter/material.dart';
import 'package:encrateia/models/activity.dart';
import 'package:encrateia/models/activity_list.dart';
import 'package:encrateia/utils/enums.dart';
class AthletePowerRatioChart extends StatelessWidget {
final List<Activity> activities;
AthletePowerRatioChart({@required this.activities});
@override
Widget build(BuildContext context) {
int xAxesDays = 60;
var nonZeroActivities = activities
.where((value) =>
value.avgPower != null &&
value.db.avgPower != null &&
value.db.avgPower > 0 &&
value.avgFormPower != null &&
value.db.avgFormPower != null &&
value.db.avgFormPower > 0)
.toList();
ActivityList(activities: nonZeroActivities).enrichGlidingAverage(
quantity: ActivityAttr.avgPowerRatio,
fullDecay: 30,
);
var nonZeroDateLimited = nonZeroActivities
.where((activity) =>
DateTime.now().difference(activity.db.timeCreated).inDays <
xAxesDays)
.toList();
var data = [
Series<Activity, DateTime>(
id: 'Average power ratio',
colorFn: (_, __) => MaterialPalette.blue.shadeDefault,
domainFn: (Activity activity, _) => activity.db.timeCreated,
measureFn: (Activity activity, _) =>
100 *
(activity.db.avgPower - activity.db.avgFormPower) /
activity.db.avgPower,
data: nonZeroDateLimited,
),
Series<Activity, DateTime>(
id: 'Gliding average power ratio',
colorFn: (_, __) => MaterialPalette.green.shadeDefault,
domainFn: (Activity activity, _) => activity.db.timeCreated,
measureFn: (Activity activity, _) => activity.glidingAvgPowerRatio,
data: nonZeroDateLimited,
)..setAttribute(rendererIdKey, 'glidingAverageRenderer'),
];
return new Container(
height: 300,
child: TimeSeriesChart(
data,
animate: false,
defaultRenderer: LineRendererConfig(
includePoints: true,
includeLine: false,
),
customSeriesRenderers: [
LineRendererConfig(
customRendererId: 'glidingAverageRenderer',
dashPattern: [1, 2],
),
],
primaryMeasureAxis: NumericAxisSpec(
tickProviderSpec: BasicNumericTickProviderSpec(
zeroBound: false,
dataIsInWholeNumbers: false,
desiredTickCount: 6,
),
),
behaviors: [
ChartTitle(
'Power Ratio (%)',
titleStyleSpec: TextStyleSpec(fontSize: 13),
behaviorPosition: BehaviorPosition.start,
titleOutsideJustification: OutsideJustification.end,
),
ChartTitle(
'Date',
titleStyleSpec: TextStyleSpec(fontSize: 13),
behaviorPosition: BehaviorPosition.bottom,
titleOutsideJustification: OutsideJustification.end,
),
],
),
);
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment