diff --git a/App.js b/App.js index 6d12ee2..3588bce 100644 --- a/App.js +++ b/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 ( - + ); } diff --git a/components/CycleView.js b/components/CycleView.js new file mode 100644 index 0000000..f24b906 --- /dev/null +++ b/components/CycleView.js @@ -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 ( + + + { + React.Children.map(this.props.children, (c, i) => { + return + {c} + + }) + } + + + ); + } +} + +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, +} \ No newline at end of file diff --git a/components/Graph.js b/components/Graph.js index e24487b..d3f8dcf 100644 --- a/components/Graph.js +++ b/components/Graph.js @@ -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 ( - + { key={`peak${peak[0]}`} stroke="black" fill="black" - //cy={dataInDeviceCoordToSvgCoord(0, props.data[peak])[1]} cy={peak[1]} r="3" /> diff --git a/components/IconCard.js b/components/IconCard.js index dcae824..1c6e603 100644 --- a/components/IconCard.js +++ b/components/IconCard.js @@ -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', } }); diff --git a/components/LiveTrainingView.js b/components/LiveTrainingView.js index d4b1cac..b116381 100644 --- a/components/LiveTrainingView.js +++ b/components/LiveTrainingView.js @@ -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 }} > - - - - - {/* - - - - */} + + + + + + + + + + + + @@ -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,*/ } }); diff --git a/components/Themes.js b/components/Themes.js index 3280e0d..ccb1365 100644 --- a/components/Themes.js +++ b/components/Themes.js @@ -2,7 +2,8 @@ const backgroundColors = { 'hot': ['#830e5f', '#fd5139'], 'darkBlue': ['#4265a3', '#cfada7'], - 'lightBlue': ['#50a4db', '#74bbe2'], + //'lightBlue': ['#50a4db', '#74bbe2'], + 'lightBlue': ['#24acdc ', '#65fae6'], 'foggy': ['#bc8db8', '#5d5e90'], }; diff --git a/data_processing/DataAnalysis.js b/data_processing/DataAnalysis.js index 5b9e608..87deed9 100644 --- a/data_processing/DataAnalysis.js +++ b/data_processing/DataAnalysis.js @@ -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(); diff --git a/data_processing/MovingAverage.js b/data_processing/MovingAverage.js new file mode 100644 index 0000000..76c37fa --- /dev/null +++ b/data_processing/MovingAverage.js @@ -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; + } +}; \ No newline at end of file diff --git a/state/Reducer.js b/state/Reducer.js index c784ed9..e364ea6 100644 --- a/state/Reducer.js +++ b/state/Reducer.js @@ -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, } };