Commit 0e0b07f4 authored by Administrator's avatar Administrator

ground Time on activity level

parent dc0a46dc
......@@ -88,6 +88,8 @@ const tableActivity = SqfEntityTable(
SqfEntityField('minPower', DbType.integer),
SqfEntityField('maxPower', DbType.integer),
SqfEntityField('sdevPower', DbType.real),
SqfEntityField('avgGroundTime', DbType.real),
SqfEntityField('sdevGroundTime', DbType.real),
SqfEntityFieldRelationship(
parentTable: tableAthlete,
......@@ -177,6 +179,8 @@ const tableLap = SqfEntityTable(
SqfEntityField('minPower', DbType.integer),
SqfEntityField('maxPower', DbType.integer),
SqfEntityField('sdevPower', DbType.real),
SqfEntityField('avgGroundTime', DbType.real),
SqfEntityField('sdevGroundTime', DbType.real),
SqfEntityFieldRelationship(
......
This diff is collapsed.
......@@ -141,6 +141,27 @@ class Activity extends ChangeNotifier {
return db.maxPower;
}
Future<double> get avgGroundTime async {
if (db.avgGroundTime == null) {
List<Event> records = await this.records;
db.avgGroundTime = Lap.calculateAverageGroundTime(records: records);
await db.save();
notifyListeners();
}
return db.avgGroundTime;
}
Future<double> get sdevGroundTime async {
if (db.sdevGroundTime == null) {
List<Event> records = await this.records;
db.sdevGroundTime = Lap.calculateSdevGroundTime(records: records);
await db.save();
notifyListeners();
}
return db.sdevGroundTime;
}
parse({@required Athlete athlete}) async {
var appDocDir = await getApplicationDocumentsDirectory();
var fitFile = FitFile(path: appDocDir.path + '/${db.stravaId}.fit').parse();
......
......@@ -121,13 +121,17 @@ class Event {
return eventList;
}
static toDataPoints({Iterable<Event> records, int amount, @required String attribute}) {
static toIntDataPoints({
Iterable<Event> records,
int amount,
@required String attribute,
}) {
int index = 0;
List<PlotPoint> plotPoints = [];
List<IntPlotPoint> plotPoints = [];
int sum = 0;
for (var record in records) {
switch(attribute) {
switch (attribute) {
case "power":
sum = sum + record.db.power;
break;
......@@ -136,7 +140,7 @@ class Event {
}
if (index++ % amount == amount - 1) {
plotPoints.add(PlotPoint(
plotPoints.add(IntPlotPoint(
domain: record.db.distance.round(),
measure: (sum / amount).round(),
));
......@@ -146,4 +150,43 @@ class Event {
return plotPoints;
}
static toDoubleDataPoints({
Iterable<Event> records,
int amount,
@required String attribute,
}) {
int index = 0;
List<DoublePlotPoint> plotPoints = [];
double sum = 0.0;
for (var record in records) {
switch (attribute) {
case "groundTime":
sum = sum + record.db.groundTime;
break;
case "strydCadence":
sum = sum + record.db.strydCadence;
break;
case "verticalOscillation":
sum = sum + record.db.verticalOscillation;
break;
case "formPower":
sum = sum + record.db.formPower;
break;
case "legSpringStiffness":
sum = sum + record.db.legSpringStiffness;
}
if (index++ % amount == amount - 1) {
plotPoints.add(DoublePlotPoint(
domain: record.db.distance.round(),
measure: sum / amount,
));
sum = 0;
}
}
return plotPoints;
}
}
......@@ -54,7 +54,6 @@ class Lap {
}
Lap.fromDb(this.db);
Future<List<Event>> get records async {
if (_records == null) {
_records = await Event.recordsByLap(lap: this);
......@@ -81,7 +80,7 @@ class Lap {
}
Future<int> get minPower async {
if (db.minPower == null){
if (db.minPower == null) {
List<Event> records = await this.records;
db.minPower = calculateMinPower(records: records);
await db.save();
......@@ -90,7 +89,7 @@ class Lap {
}
Future<int> get maxPower async {
if (db.maxPower == null){
if (db.maxPower == null) {
List<Event> records = await this.records;
db.maxPower = calculateMaxPower(records: records);
await db.save();
......@@ -98,7 +97,6 @@ class Lap {
return db.maxPower;
}
Future<int> firstEventId() async {
if (index > 1) {
var lapList = await activity.laps;
......@@ -128,17 +126,17 @@ class Lap {
}
static String sdevHeartRate({List<Event> records}) {
var heartRates = records.map((record) => record.db.heartRate).nonZero();
var heartRates = records.map((record) => record.db.heartRate).nonZeroInts();
return heartRates.sdev().toStringAsFixed(2);
}
static String minHeartRate({List<Event> records}) {
var heartRates = records.map((record) => record.db.heartRate).nonZero();
var heartRates = records.map((record) => record.db.heartRate).nonZeroInts();
return heartRates.min().toString();
}
static double calculateAveragePower({List<Event> records}) {
var powers = records.map((record) => record.db.power).nonZero();
var powers = records.map((record) => record.db.power).nonZeroInts();
if (powers.length > 0) {
return powers.mean();
} else
......@@ -146,17 +144,32 @@ class Lap {
}
static double calculateSdevPower({List<Event> records}) {
var powers = records.map((record) => record.db.power).nonZero();
var powers = records.map((record) => record.db.power).nonZeroInts();
return powers.sdev();
}
static int calculateMinPower({List<Event> records}) {
var powers = records.map((record) => record.db.power).nonZero();
var powers = records.map((record) => record.db.power).nonZeroInts();
return powers.min();
}
static int calculateMaxPower({List<Event> records}) {
var powers = records.map((record) => record.db.power).nonZero();
var powers = records.map((record) => record.db.power).nonZeroInts();
return powers.max();
}
static double calculateAverageGroundTime({List<Event> records}) {
var groundTimes =
records.map((record) => record.db.groundTime).nonZeroDoubles();
if (groundTimes.length > 0) {
return groundTimes.mean();
} else
return -1;
}
static double calculateSdevGroundTime({List<Event> records}) {
var groundTimes =
records.map((record) => record.db.groundTime).nonZeroDoubles();
return groundTimes.sdev();
}
}
class PlotPoint {
class IntPlotPoint {
int domain;
int measure;
IntPlotPoint({this.domain, this.measure});
}
class DoublePlotPoint {
int domain;
double measure;
PlotPoint({this.measure, this.domain});
DoublePlotPoint({this.domain, this.measure});
}
......@@ -35,11 +35,11 @@ class PowerDuration {
}
}
List<PlotPoint> asList() {
List<PlotPoint> plotPoints = [];
List<IntPlotPoint> asList() {
List<IntPlotPoint> plotPoints = [];
powerMap.forEach((duration, power) {
plotPoints.add(PlotPoint(
plotPoints.add(IntPlotPoint(
domain: scaled(seconds: duration),
measure: power,
));
......
......@@ -4,6 +4,7 @@ import 'package:encrateia/widgets/laps_list_widget.dart';
import 'package:encrateia/widgets/activity_heart_rate_widget.dart';
import 'package:encrateia/widgets/activity_power_widget.dart';
import 'package:encrateia/widgets/activity_power_duration_widget.dart';
import 'package:encrateia/widgets/activity_ground_time_widget.dart';
import 'package:flutter/material.dart';
import 'package:encrateia/models/activity.dart';
......@@ -18,53 +19,39 @@ class ShowActivityScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 6,
length: 7,
child: Scaffold(
appBar: AppBar(
bottom: TabBar(
isScrollable: true,
tabs: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Tab(icon: Icon(Icons.directions_run)),
Text(" Overview"),
],
Tab(
icon: Icon(Icons.directions_run),
text: "Overview",
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Tab(icon: Icon(Icons.spa)),
Text(" Heart Rate"),
],
Tab(
icon: Icon(Icons.timer),
text: "Laps",
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Tab(icon: Icon(Icons.ev_station)),
Text(" Power"),
],
Tab(
icon: Icon(Icons.spa),
text: "HR",
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Tab(icon: Icon(Icons.multiline_chart)),
Text(" Power Duration"),
],
Tab(
icon: Icon(Icons.ev_station),
text: "Power",
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Tab(icon: Icon(Icons.timer)),
Text(" Laps"),
],
Tab(
icon: Icon(Icons.multiline_chart),
text: "Pow Dur",
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Tab(icon: Icon(Icons.storage)),
Text(" Metadata"),
],
Tab(
icon: Icon(Icons.vertical_align_bottom),
text: "Grnd.time",
),
Tab(
icon: Icon(Icons.storage),
text: "Metadata",
),
],
),
......@@ -75,10 +62,11 @@ class ShowActivityScreen extends StatelessWidget {
),
body: TabBarView(children: [
ActivityOverviewWidget(activity: activity),
LapsListWidget(activity: activity),
ActivityHeartRateWidget(activity: activity),
ActivityPowerWidget(activity: activity),
ActivityPowerDurationWidget(activity: activity),
LapsListWidget(activity: activity),
ActivityGroundTimeWidget(activity: activity),
ActivityMetadataWidget(activity: activity),
]),
),
......
......@@ -3,8 +3,7 @@ import 'package:encrateia/models/event.dart';
extension StatisticFunctions on Iterable {
double mean(){
List<int> values = this;
var sum = values.reduce((a, b) => a + b);
var sum = this.fold(0, (a, b) => a + b);
var number = this.length;
return sum / number;
}
......@@ -28,12 +27,19 @@ extension StatisticFunctions on Iterable {
return values.reduce(math.max);
}
List<int> nonZero() {
List<int> nonZeroInts() {
List<int> values = this.toList();
var nonZeroValues = values.where((value) => value != null && value != 0);
return nonZeroValues.toList();
}
List<double> nonZeroDoubles() {
List<double> values = this.toList();
var nonZeroValues = values.where((value) => value != null && value != 0);
return nonZeroValues.toList();
}
Iterable<Event> everyNth(int n) sync* {
List<Event> values = this.toList();
int i = 0;
......
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';
class ActivityGroundTimeChart extends StatelessWidget {
final List<Event> records;
final Activity activity;
final colorArray = [
MaterialPalette.white,
MaterialPalette.gray.shade200,
];
ActivityGroundTimeChart({this.records, @required this.activity});
@override
Widget build(BuildContext context) {
var nonZero = records.where((value) => value.db.groundTime > 0);
var smoothedRecords = Event.toDoubleDataPoints(
attribute: "groundTime",
records: nonZero,
amount: 30,
);
List<Series<dynamic, num>> data = [
new Series<DoublePlotPoint, int>(
id: 'Ground Time',
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: LineChart(
data,
domainAxis: NumericAxisSpec(
viewport: NumericExtents(0, nonZero.last.db.distance + 500),
tickProviderSpec: BasicNumericTickProviderSpec(
desiredTickCount: 6,
),
),
animate: false,
layoutConfig: LayoutConfig(
leftMarginSpec: MarginSpec.fixedPixel(60),
topMarginSpec: MarginSpec.fixedPixel(20),
rightMarginSpec: MarginSpec.fixedPixel(20),
bottomMarginSpec: MarginSpec.fixedPixel(40),
),
behaviors: [
RangeAnnotation(rangeAnnotations(laps: laps)),
ChartTitle(
'Ground Time (ms)',
titleStyleSpec: TextStyleSpec(fontSize: 13),
behaviorPosition: BehaviorPosition.start,
titleOutsideJustification: OutsideJustification.end,
),
ChartTitle(
'Distance (m)',
titleStyleSpec: TextStyleSpec(fontSize: 13),
behaviorPosition: BehaviorPosition.bottom,
titleOutsideJustification: OutsideJustification.end,
),
],
),
);
} else {
return Container(
height: 100,
child: Center(child: Text("Loading")),
);
}
},
);
}
rangeAnnotations({List<Lap> laps}) {
return [
for (int index = 0; index < laps.length; index++)
RangeAnnotationSegment(
laps
.sublist(0, index + 1)
.map((lap) => lap.db.totalDistance)
.reduce((a, b) => a + b) -
laps[index].db.totalDistance,
laps
.sublist(0, index + 1)
.map((lap) => lap.db.totalDistance)
.reduce((a, b) => a + b),
RangeAnnotationAxisType.domain,
color: colorArray[index % 2],
endLabel: 'Lap ${laps[index].index}',
)
];
}
}
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 'activity_ground_time_chart.dart';
class ActivityGroundTimeWidget extends StatefulWidget {
final Activity activity;
ActivityGroundTimeWidget({this.activity});
@override
_ActivityGroundTimeWidgetState createState() => _ActivityGroundTimeWidgetState();
}
class _ActivityGroundTimeWidgetState extends State<ActivityGroundTimeWidget> {
List<Event> records = [];
String avgGroundTimeString = "Loading ...";
String minGroundTimeString = "Loading ...";
String maxGroundTimeString = "Loading ...";
String sdevGroundTimeString = "Loading ...";
@override
void initState() {
getData();
super.initState();
}
@override
Widget build(context) {
if (records.length > 0) {
var powerValues = records.map((value) => value.db.groundTime).nonZeroDoubles();
if (powerValues.length > 0) {
return ListTileTheme(
iconColor: Colors.deepOrange,
child: ListView(
padding: EdgeInsets.only(left: 25),
children: <Widget>[
ActivityGroundTimeChart(records: records, activity: widget.activity),
ListTile(
leading: Icon(Icons.ev_station),
title: Text(avgGroundTimeString),
subtitle: Text("average ground time"),
),
ListTile(
leading: Icon(Icons.unfold_more),
title: Text(sdevGroundTimeString),
subtitle: Text("standard deviation ground time"),
),
ListTile(
leading: Icon(Icons.playlist_add),
title: Text(records.length.toString()),
subtitle: Text("number of measurements"),
),
],
),
);
} else {
return Center(
child: Text("No ground time data available."),
);
}
} else {
return Center(
child: Text("Loading"),
);
}
}
getData() async {
Activity activity = widget.activity;
records = await activity.records;
double avg = await activity.avgGroundTime;
setState(() {
avgGroundTimeString = avg.toStringOrDashes(1) + " ms";
});
double sdev = await activity.sdevGroundTime;
setState(() {
sdevGroundTimeString = sdev.toStringOrDashes(2) + " ms";
});
}
}
......@@ -19,18 +19,18 @@ class ActivityHeartRateChart extends StatelessWidget {
Widget build(BuildContext context) {
var nonZero = records.where(
(value) => value.db.heartRate != null && value.db.heartRate > 10);
var smoothedRecords = Event.toDataPoints(
var smoothedRecords = Event.toIntDataPoints(
attribute: "heartRate",
records: nonZero,
amount: 30,
);
List<Series<dynamic, num>> data = [
new Series<PlotPoint, int>(
new Series<IntPlotPoint, int>(
id: 'Heart Rate',
colorFn: (_, __) => MaterialPalette.red.shadeDefault,
domainFn: (PlotPoint point, _) => point.domain,
measureFn: (PlotPoint point, _) => point.measure,
domainFn: (IntPlotPoint point, _) => point.domain,
measureFn: (IntPlotPoint point, _) => point.measure,
data: smoothedRecords,
)
];
......
......@@ -18,7 +18,7 @@ class ActivityHeartRateWidget extends StatelessWidget {
builder: (BuildContext context, AsyncSnapshot<List<Event>> snapshot) {
if (snapshot.hasData) {
var heartRates =
snapshot.data.map((value) => value.db.heartRate).nonZero();
snapshot.data.map((value) => value.db.heartRate).nonZeroInts();
if (heartRates.length > 0) {
var records = snapshot.data;
return ListTileTheme(
......
......@@ -18,18 +18,18 @@ class ActivityPowerChart extends StatelessWidget {
@override
Widget build(BuildContext context) {
var nonZero = records.where((value) => value.db.power > 100);
var smoothedRecords = Event.toDataPoints(
var smoothedRecords = Event.toIntDataPoints(
attribute: "power",
records: nonZero,
amount: 30,
);
List<Series<dynamic, num>> data = [
new Series<PlotPoint, int>(
new Series<IntPlotPoint, int>(
id: 'Power',
colorFn: (_, __) => MaterialPalette.green.shadeDefault,
domainFn: (PlotPoint record, _) => record.domain,
measureFn: (PlotPoint record, _) => record.measure,
domainFn: (IntPlotPoint record, _) => record.domain,
measureFn: (IntPlotPoint record, _) => record.measure,
data: smoothedRecords,
)
];
......
......@@ -16,7 +16,7 @@ class ActivityPowerDurationWidget extends StatelessWidget {
builder: (BuildContext context, AsyncSnapshot<List<Event>> snapshot) {
if (snapshot.hasData) {
var powerValues =
snapshot.data.map((value) => value.db.power).nonZero();