CycleView & more
This commit is contained in:
parent
23eb634c30
commit
39632d4eec
4
App.js
4
App.js
|
@ -31,7 +31,7 @@ export default class App extends React.Component {
|
|||
...Ionicons.font,
|
||||
});
|
||||
this.setState({ isReady: true });
|
||||
this.device = new DeviceReduxCoupling(store);
|
||||
this.device = new DeviceReduxCoupling(store);
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -41,7 +41,7 @@ export default class App extends React.Component {
|
|||
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<ThemedStackNavigation/>
|
||||
<ThemedStackNavigation />
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
|
@ -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"
|
||||
/>
|
||||
|
|
|
@ -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',
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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 }}>
|
||||
<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>
|
||||
</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,*/
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
const backgroundColors = {
|
||||
'hot': ['#830e5f', '#fd5139'],
|
||||
'darkBlue': ['#4265a3', '#cfada7'],
|
||||
'lightBlue': ['#50a4db', '#74bbe2'],
|
||||
//'lightBlue': ['#50a4db', '#74bbe2'],
|
||||
'lightBlue': ['#24acdc ', '#65fae6'],
|
||||
'foggy': ['#bc8db8', '#5d5e90'],
|
||||
};
|
||||
|
||||
|
|
|
@ -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
|
||||
|
@ -38,11 +47,13 @@ export default class DataAnalysis {
|
|||
const windowed = allMeasurements.slice(-windowNumDataPoints);
|
||||
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();
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
|
@ -30,11 +30,11 @@ const INITIAL_SETTINGS = {
|
|||
swimTrackerHost: "192.168.178.110",
|
||||
|
||||
analysis: {
|
||||
peaksPerLap: 30,
|
||||
peaksPerLap: 30,
|
||||
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,
|
||||
}
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in New Issue