CycleView & more

This commit is contained in:
Martin Bauer 2020-07-15 15:53:16 +02:00
parent 23eb634c30
commit 39632d4eec
9 changed files with 165 additions and 46 deletions

4
App.js
View File

@ -31,7 +31,7 @@ export default class App extends React.Component {
...Ionicons.font, ...Ionicons.font,
}); });
this.setState({ isReady: true }); this.setState({ isReady: true });
this.device = new DeviceReduxCoupling(store); this.device = new DeviceReduxCoupling(store);
} }
render() { render() {
@ -41,7 +41,7 @@ export default class App extends React.Component {
return ( return (
<Provider store={store}> <Provider store={store}>
<ThemedStackNavigation/> <ThemedStackNavigation />
</Provider> </Provider>
); );
} }

83
components/CycleView.js Normal file
View File

@ -0,0 +1,83 @@
import React from 'react';
import { Animated, TouchableWithoutFeedback } from 'react-native';
import { View } from 'native-base';
import PropTypes from 'prop-types';
export default class CycleView extends React.Component {
constructor(props) {
super(props);
this.state = {
activeView: props.initialView
};
this.opacityArr = React.Children.map(props.children,
(c, i) => { console.log("iter", c, i); return new Animated.Value(i === props.initialView ? 1 : 0); });
this.fadeInAnimations = this.opacityArr.map(a => Animated.timing(a, {
toValue: 1,
duration: props.fadeDuration,
useNativeDriver: true
}));
this.fadeOutAnimations = this.opacityArr.map(a => Animated.timing(a, {
toValue: 0,
duration: props.fadeDuration,
useNativeDriver: true
}));
console.log("opacity arr length", this.opacityArr.length, React.Children.count(props.children));
}
_onTouch = () => {
const currentViewIdx = this.state.activeView;
const nextViewIdx = (currentViewIdx + 1) % React.Children.count(this.props.children);
this.fadeOutAnimations[currentViewIdx].start();
this.fadeInAnimations[nextViewIdx].start();
this.setState({ activeView: nextViewIdx });
this.props.onViewChange(nextViewIdx);
}
render() {
const children = React.Children.map(this.props.children, (c, i) => c);
return (
<TouchableWithoutFeedback onPress={this._onTouch} styles={{ flex: 1 }}>
<View styles={{ flex: 1, flexDirection: 'row', width: "100%" }}>
{
React.Children.map(this.props.children, (c, i) => {
return <Animated.View style={{
position: i === 0 ? 'relative' : 'absolute',
left: 0,
right: 0,
opacity: this.opacityArr[i],
transform: [{
scale: this.opacityArr[i].interpolate({
inputRange: [0, 1],
outputRange: [this.props.minScaleOnFade, 1],
})
}]
}}>
{c}
</Animated.View>
})
}
</View>
</TouchableWithoutFeedback>
);
}
}
CycleView.propTypes = {
initialView: PropTypes.number,
fadeDuration: PropTypes.number,
minScaleOnFade: PropTypes.number,
onViewChange: PropTypes.func,
}
CycleView.defaultProps = {
initialView: 0,
fadeDuration: 600,
minScaleOnFade: 0.4,
onViewChange: viewIdx => null,
}

View File

@ -1,9 +1,6 @@
import React from 'react'; import React from 'react';
import { View, StyleSheet } from 'react-native'; import { View, StyleSheet } from 'react-native';
import { useWindowDimensions } from 'react-native'; import Svg, { Polyline, Text, Circle } from 'react-native-svg';
//import Svg, {Polyline, Polygon, Rect, G} from 'react-native-svg-web';
import Svg, { Polyline, Polygon, Rect, G, Text, Circle } from 'react-native-svg';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
@ -30,35 +27,33 @@ function computeTickMarks(largest, mostTicks) {
return result; return result;
} }
let isFirstRender = true;
const Graph = props => { const Graph = props => {
const graphHeight = 100; const graphHeight = 100;
const numPoints = 300; const numPoints = 300;
const yLabelSpace = 40; const yLabelSpace = 40;
const graphWidth = numPoints + yLabelSpace; const graphWidth = numPoints + yLabelSpace;
const minKgScale = 3; // scale such that upper graph value is n kg const minKgScale = 4; // scale such that upper graph value is n kg
const data = props.data.slice(-numPoints); const data = props.data.slice(-numPoints);
const maxValueDeviceCoord = data.reduce((running, x) => Math.max(x, running), minKgScale / props.kgFactor); const maxValueDeviceCoord = data.reduce((running, x) => Math.max(x, running), minKgScale / props.kgFactor);
const maxValueKg = Math.max(maxValueDeviceCoord * props.kgFactor, minKgScale); const maxValueKg = Math.max(maxValueDeviceCoord * props.kgFactor, minKgScale);
const dataInDeviceCoordToSvgCoordX = x => yLabelSpace + x; const devCoordToSvgX = x => yLabelSpace + x;
const dataInDeviceCoordToSvgCoordY = y => graphHeight - (y * 100 / maxValueDeviceCoord); const devCoordToSvgY = y => graphHeight - (y * 100 / maxValueDeviceCoord);
const dataInKgToSvgCoordX = dataInDeviceCoordToSvgCoordX; const dataInKgToSvgCoordX = devCoordToSvgX;
const dataInKgToSvgCoordY = y => dataInDeviceCoordToSvgCoordY(y / props.kgFactor); const dataInKgToSvgCoordY = y => devCoordToSvgY(y / props.kgFactor);
const coordStr = data.map((element, i) => `${dataInDeviceCoordToSvgCoordX(i)}, ${dataInDeviceCoordToSvgCoordY(element)}`).join(" "); const coordStr = data.map((element, i) => `${devCoordToSvgX(i)}, ${devCoordToSvgY(element)}`).join(" ");
const ticks = computeTickMarks(maxValueKg * 0.9, 4); const ticks = computeTickMarks(maxValueKg * 0.9, 4);
let viewBox = `0 0 ${graphWidth} ${graphHeight}`; let viewBox = `0 0 ${graphWidth} ${graphHeight}`;
const cutOffIndex = Math.max(0, props.data.size - numPoints); const cutOffIndex = Math.max(0, props.data.size - numPoints);
const peaksToDisplay = props.peaks.filter(p => p > cutOffIndex) const peaksToDisplay = props.peaks.filter(p => p > cutOffIndex)
const peaksXCoords = peaksToDisplay.map(p => dataInDeviceCoordToSvgCoordX(p - cutOffIndex)); const peaksXCoords = peaksToDisplay.map(p => devCoordToSvgX(p - cutOffIndex));
const peaksYCoords = peaksToDisplay.map(p => dataInDeviceCoordToSvgCoordY(props.data.get(p))); const peaksYCoords = peaksToDisplay.map(p => devCoordToSvgY(props.data.get(p)));
return ( return (
<View style={{ aspectRatio: 3.4 }}> <View style={{ aspectRatio: graphWidth / graphHeight }}>
<Svg height="100%" width="100%" viewBox={viewBox} preserveAspectRatio="none" > <Svg height="100%" width="100%" viewBox={viewBox} preserveAspectRatio="none" >
<Polyline <Polyline
points={coordStr} points={coordStr}
@ -91,7 +86,6 @@ const Graph = props => {
key={`peak${peak[0]}`} key={`peak${peak[0]}`}
stroke="black" stroke="black"
fill="black" fill="black"
//cy={dataInDeviceCoordToSvgCoord(0, props.data[peak])[1]}
cy={peak[1]} cy={peak[1]}
r="3" r="3"
/> />

View File

@ -21,10 +21,10 @@ const IconCard = props => {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
card: { card: {
flexDirection: 'row', flexDirection: 'row',
backgroundColor: 'rgba(0, 0, 0, 0.2)', backgroundColor: 'rgba(0, 0, 0, 0.3)',
margin: 5, margin: 5,
padding: 5, padding: 5,
borderRadius: 3, borderRadius: 6,
justifyContent: 'space-between', justifyContent: 'space-between',
} }
}); });

View File

@ -1,6 +1,6 @@
import React from 'react'; import React, { useRef, useState } from 'react';
import { StyleSheet } from 'react-native'; import { StyleSheet, Animated } from 'react-native';
import { Button, Content, Text } from 'native-base'; import { Button, Content, Text, View } from 'native-base';
import { LinearGradient } from 'expo-linear-gradient'; import { LinearGradient } from 'expo-linear-gradient';
import IconCard from './IconCard'; import IconCard from './IconCard';
import Graph from './Graph'; import Graph from './Graph';
@ -8,6 +8,8 @@ import { connect } from 'react-redux';
import backgroundColors from './Themes'; import backgroundColors from './Themes';
import { useKeepAwake } from 'expo-keep-awake'; import { useKeepAwake } from 'expo-keep-awake';
import { stopSession } from '../state/DeviceReduxCoupling'; import { stopSession } from '../state/DeviceReduxCoupling';
import CycleView from './CycleView';
function LiveTrainingView(props) { function LiveTrainingView(props) {
const analysis = props.session.analysis; const analysis = props.session.analysis;
@ -28,16 +30,19 @@ function LiveTrainingView(props) {
style={{ flex: 1 }} style={{ flex: 1 }}
> >
<Content padder contentContainerStyle={{ justifyContent: 'space-around', flex: 1, paddingTop: 60 }}> <Content padder contentContainerStyle={{ justifyContent: 'space-around', flex: 1, paddingTop: 60 }}>
<IconCard label="BAHNEN" value={laps} iconName="retweet" iconType="AntDesign" />
<IconCard label="ZÜGE" value={analysis.peaks.size} iconName="dashboard" iconType="AntDesign" />
<IconCard label="KRAFT" value={totalMomentum} iconName="ruler" iconType="Entypo" />
<Graph></Graph>
{/*
<IconCard label="ZÜGE" value={this.state.numPeaks} iconName="dashboard" iconType="AntDesign" />
<IconCard label="BAHNEN" value={this.state.numLaps} iconName="retweet" iconType="AntDesign" />
<IconCard label="KRAFT" value="120" iconName="ruler" iconType="Entypo" /> <CycleView>
*/} <IconCard label="BAHNEN" value={laps} iconName="retweet" iconType="AntDesign" />
<IconCard label="ZÜGE" value={analysis.peaks.size} iconName="dashboard" iconType="AntDesign" />
</CycleView>
<CycleView>
<IconCard label="DAUER" value="00:42" iconName="clock" iconType="FontAwesome5" />
<IconCard label="AKTIVE DAUER" value="42:00" iconName="stopwatch" iconType="FontAwesome5" />
</CycleView>
<IconCard label="KRAFT" value={totalMomentum} iconName="ruler" iconType="Entypo" />
<Graph></Graph>
<Button block secondary onPress={onStopClick}><Text>Stop</Text></Button> <Button block secondary onPress={onStopClick}><Text>Stop</Text></Button>
</Content> </Content>
</LinearGradient> </LinearGradient>
@ -52,16 +57,6 @@ const styles = StyleSheet.create({
padding: 5, padding: 5,
borderRadius: 3, borderRadius: 3,
justifyContent: 'space-between', justifyContent: 'space-between',
/*
shadowColor: "#000",
shadowOffset: {
width: 0,
height: 1,
},
shadowOpacity: 0.18,
shadowRadius: 1.00,
elevation: 1,*/
} }
}); });

View File

@ -2,7 +2,8 @@
const backgroundColors = { const backgroundColors = {
'hot': ['#830e5f', '#fd5139'], 'hot': ['#830e5f', '#fd5139'],
'darkBlue': ['#4265a3', '#cfada7'], 'darkBlue': ['#4265a3', '#cfada7'],
'lightBlue': ['#50a4db', '#74bbe2'], //'lightBlue': ['#50a4db', '#74bbe2'],
'lightBlue': ['#24acdc ', '#65fae6'],
'foggy': ['#bc8db8', '#5d5e90'], 'foggy': ['#bc8db8', '#5d5e90'],
}; };

View File

@ -1,4 +1,5 @@
import { PeakDetectorSimple } from './PeakDetection'; import { PeakDetectorSimple } from './PeakDetection';
import { MovingAverage} from './MovingAverage';
import { List } from 'immutable'; import { List } from 'immutable';
@ -25,8 +26,16 @@ export default class DataAnalysis {
console.log("cache reset"); console.log("cache reset");
} }
const newDataArr = newData.toArray();
// active time
const newAverages = this.movingAverage.addVector(newDataArr);
this.activeMeasurements += newAverages.reduce((n, val) => {
return n + (val >= analysisParameters.activeTimeThreshold);
});
// peaks // peaks
const newPeaks = this.peakDetectorSimple.addVector(newData.toArray()); const newPeaks = this.peakDetectorSimple.addVector(newDataArr);
this.allPeaks = this.allPeaks.concat(List(newPeaks)); this.allPeaks = this.allPeaks.concat(List(newPeaks));
// aggregated sum/max // aggregated sum/max
@ -38,11 +47,13 @@ export default class DataAnalysis {
const windowed = allMeasurements.slice(-windowNumDataPoints); const windowed = allMeasurements.slice(-windowNumDataPoints);
const peakMaxWindow = windowed.reduce((running, x) => Math.max(x, running), 0); const peakMaxWindow = windowed.reduce((running, x) => Math.max(x, running), 0);
const momentumWindow = windowed.reduce((sum, x) => sum + x, 0); const momentumWindow = windowed.reduce((sum, x) => sum + x, 0);
this.analyzedUpToIdx = allMeasurements.size; this.analyzedUpToIdx = allMeasurements.size;
return { return {
peaks: this.allPeaks, peaks: this.allPeaks,
totalTime: allMeasurements / analysisParameters.numMeasurementsPerSec, totalTime: allMeasurements / analysisParameters.numMeasurementsPerSec,
activeTime: this.activeMeasurements / analysisParameters.numMeasurementsPerSec,
totalMomentum: this.aggregatedMomentum, totalMomentum: this.aggregatedMomentum,
peakMax: this.peakMax, peakMax: this.peakMax,
@ -54,6 +65,9 @@ export default class DataAnalysis {
_resetCache(analysisParameters, sessionId) { _resetCache(analysisParameters, sessionId) {
this.movingAverage = analysisParameters ? new MovingAverage(analysisParameters.movingAverageWindowSize) : null;
this.activeMeasurements = 0;
this.peakDetectorSimple = analysisParameters ? new PeakDetectorSimple(analysisParameters.peakDetectorSimpleThreshold) : null; this.peakDetectorSimple = analysisParameters ? new PeakDetectorSimple(analysisParameters.peakDetectorSimpleThreshold) : null;
this.allPeaks = List(); this.allPeaks = List();

View File

@ -0,0 +1,29 @@
/**
* A moving average computation
*/
export class MovingAverage {
constructor(windowSize) {
this._windowSize = windowSize;
this._queue = [];
this._queueSum = 0;
}
windowSize() {
return this._windowSize;
}
addVector(vec) {
return vec.map(this.add.bind(this));
}
add(value) {
this._queueSum += value;
this._queue.push(value);
if(this._queue.length > this._windowSize) {
this._queueSum -= this._queue[0];
this._queue.shift();
}
return this._queueSum / this._queue.length;
}
};

View File

@ -30,11 +30,11 @@ const INITIAL_SETTINGS = {
swimTrackerHost: "192.168.178.110", swimTrackerHost: "192.168.178.110",
analysis: { analysis: {
peaksPerLap: 30, peaksPerLap: 30,
windowSizeInSecs: 5, windowSizeInSecs: 5,
numMeasurementsPerSec: 10, numMeasurementsPerSec: 10,
kgFactor: 1.0 / 700.0, kgFactor: 1.0 / 701.0,
peakDetector: 'SIMPLE', // either 'SIMPLE' or 'ZSCORE' peakDetector: 'SIMPLE', // either 'SIMPLE' or 'ZSCORE'
peakDetectorSimpleThreshold: 2000, peakDetectorSimpleThreshold: 2000,
@ -42,6 +42,9 @@ const INITIAL_SETTINGS = {
peakDetectorZScoreLag: 8, // peak detector z-score values peakDetectorZScoreLag: 8, // peak detector z-score values
peakDetectorZScoreThreshold: 2, peakDetectorZScoreThreshold: 2,
peakDetectorZScoreInfluence: 0.1, peakDetectorZScoreInfluence: 0.1,
activeTimeThreshold: 300,
movingAverageWindowSize: 10*5,
} }
}; };