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

2
App.js
View File

@ -41,7 +41,7 @@ export default class App extends React.Component {
return (
<Provider store={store}>
<ThemedStackNavigation/>
<ThemedStackNavigation />
</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 { View, StyleSheet } from 'react-native';
import { useWindowDimensions } from 'react-native';
//import Svg, {Polyline, Polygon, Rect, G} from 'react-native-svg-web';
import Svg, { Polyline, Polygon, Rect, G, Text, Circle } from 'react-native-svg';
import Svg, { Polyline, Text, Circle } from 'react-native-svg';
import { connect } from 'react-redux';
@ -30,35 +27,33 @@ function computeTickMarks(largest, mostTicks) {
return result;
}
let isFirstRender = true;
const Graph = props => {
const graphHeight = 100;
const numPoints = 300;
const yLabelSpace = 40;
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 maxValueDeviceCoord = data.reduce((running, x) => Math.max(x, running), minKgScale / props.kgFactor);
const maxValueKg = Math.max(maxValueDeviceCoord * props.kgFactor, minKgScale);
const dataInDeviceCoordToSvgCoordX = x => yLabelSpace + x;
const dataInDeviceCoordToSvgCoordY = y => graphHeight - (y * 100 / maxValueDeviceCoord);
const dataInKgToSvgCoordX = dataInDeviceCoordToSvgCoordX;
const dataInKgToSvgCoordY = y => dataInDeviceCoordToSvgCoordY(y / props.kgFactor);
const devCoordToSvgX = x => yLabelSpace + x;
const devCoordToSvgY = y => graphHeight - (y * 100 / maxValueDeviceCoord);
const dataInKgToSvgCoordX = devCoordToSvgX;
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);
let viewBox = `0 0 ${graphWidth} ${graphHeight}`;
const cutOffIndex = Math.max(0, props.data.size - numPoints);
const peaksToDisplay = props.peaks.filter(p => p > cutOffIndex)
const peaksXCoords = peaksToDisplay.map(p => dataInDeviceCoordToSvgCoordX(p - cutOffIndex));
const peaksYCoords = peaksToDisplay.map(p => dataInDeviceCoordToSvgCoordY(props.data.get(p)));
const peaksXCoords = peaksToDisplay.map(p => devCoordToSvgX(p - cutOffIndex));
const peaksYCoords = peaksToDisplay.map(p => devCoordToSvgY(props.data.get(p)));
return (
<View style={{ aspectRatio: 3.4 }}>
<View style={{ aspectRatio: graphWidth / graphHeight }}>
<Svg height="100%" width="100%" viewBox={viewBox} preserveAspectRatio="none" >
<Polyline
points={coordStr}
@ -91,7 +86,6 @@ const Graph = props => {
key={`peak${peak[0]}`}
stroke="black"
fill="black"
//cy={dataInDeviceCoordToSvgCoord(0, props.data[peak])[1]}
cy={peak[1]}
r="3"
/>

View File

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

View File

@ -1,6 +1,6 @@
import React from 'react';
import { StyleSheet } from 'react-native';
import { Button, Content, Text } from 'native-base';
import React, { useRef, useState } from 'react';
import { StyleSheet, Animated } from 'react-native';
import { Button, Content, Text, View } from 'native-base';
import { LinearGradient } from 'expo-linear-gradient';
import IconCard from './IconCard';
import Graph from './Graph';
@ -8,6 +8,8 @@ import { connect } from 'react-redux';
import backgroundColors from './Themes';
import { useKeepAwake } from 'expo-keep-awake';
import { stopSession } from '../state/DeviceReduxCoupling';
import CycleView from './CycleView';
function LiveTrainingView(props) {
const analysis = props.session.analysis;
@ -28,16 +30,19 @@ function LiveTrainingView(props) {
style={{ flex: 1 }}
>
<Content padder contentContainerStyle={{ justifyContent: 'space-around', flex: 1, paddingTop: 60 }}>
<CycleView>
<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" />
</CycleView>
<IconCard label="KRAFT" value="120" iconName="ruler" iconType="Entypo" />
*/}
<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>
</Content>
</LinearGradient>
@ -52,16 +57,6 @@ const styles = StyleSheet.create({
padding: 5,
borderRadius: 3,
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 = {
'hot': ['#830e5f', '#fd5139'],
'darkBlue': ['#4265a3', '#cfada7'],
'lightBlue': ['#50a4db', '#74bbe2'],
//'lightBlue': ['#50a4db', '#74bbe2'],
'lightBlue': ['#24acdc ', '#65fae6'],
'foggy': ['#bc8db8', '#5d5e90'],
};

View File

@ -1,4 +1,5 @@
import { PeakDetectorSimple } from './PeakDetection';
import { MovingAverage} from './MovingAverage';
import { List } from 'immutable';
@ -25,8 +26,16 @@ export default class DataAnalysis {
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
const newPeaks = this.peakDetectorSimple.addVector(newData.toArray());
const newPeaks = this.peakDetectorSimple.addVector(newDataArr);
this.allPeaks = this.allPeaks.concat(List(newPeaks));
// aggregated sum/max
@ -39,10 +48,12 @@ export default class DataAnalysis {
const peakMaxWindow = windowed.reduce((running, x) => Math.max(x, running), 0);
const momentumWindow = windowed.reduce((sum, x) => sum + x, 0);
this.analyzedUpToIdx = allMeasurements.size;
return {
peaks: this.allPeaks,
totalTime: allMeasurements / analysisParameters.numMeasurementsPerSec,
activeTime: this.activeMeasurements / analysisParameters.numMeasurementsPerSec,
totalMomentum: this.aggregatedMomentum,
peakMax: this.peakMax,
@ -54,6 +65,9 @@ export default class DataAnalysis {
_resetCache(analysisParameters, sessionId) {
this.movingAverage = analysisParameters ? new MovingAverage(analysisParameters.movingAverageWindowSize) : null;
this.activeMeasurements = 0;
this.peakDetectorSimple = analysisParameters ? new PeakDetectorSimple(analysisParameters.peakDetectorSimpleThreshold) : null;
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

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