Last sessions view

This commit is contained in:
Martin Bauer 2020-08-09 21:53:38 +02:00
parent c6b517cfe4
commit 2d56390808
8 changed files with 238 additions and 69 deletions

View File

@ -25,8 +25,8 @@ export default class DataAnalysis {
newData = allMeasurements; newData = allMeasurements;
console.log("cache reset"); console.log("cache reset");
} }
const allMeasurementsSize = newData.size ? newData.size : newData.length;
const newDataArr = newData.toArray(); const newDataArr = (typeof newData.toArray ==="function") ? newData.toArray() : newData;
// active time // active time
const newAverages = this.movingAverage.addVector(newDataArr); const newAverages = this.movingAverage.addVector(newDataArr);
@ -49,10 +49,10 @@ export default class DataAnalysis {
const momentumWindow = windowed.reduce((sum, x) => sum + x, 0); const momentumWindow = windowed.reduce((sum, x) => sum + x, 0);
this.analyzedUpToIdx = allMeasurements.size; this.analyzedUpToIdx = allMeasurementsSize;
return { return {
peaks: this.allPeaks, peaks: this.allPeaks,
totalTime: allMeasurements.size / analysisParameters.numMeasurementsPerSec, totalTime: allMeasurementsSize / analysisParameters.numMeasurementsPerSec,
activeTime: this.activeMeasurements / analysisParameters.numMeasurementsPerSec, activeTime: this.activeMeasurements / analysisParameters.numMeasurementsPerSec,
totalMomentum: this.aggregatedMomentum, totalMomentum: this.aggregatedMomentum,

10
package-lock.json generated
View File

@ -6631,6 +6631,11 @@
"minimist": "^1.2.5" "minimist": "^1.2.5"
} }
}, },
"moment": {
"version": "2.27.0",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.27.0.tgz",
"integrity": "sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ=="
},
"morgan": { "morgan": {
"version": "1.10.0", "version": "1.10.0",
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz",
@ -8161,6 +8166,11 @@
"tween-functions": "^1.0.1" "tween-functions": "^1.0.1"
} }
}, },
"react-xml-parser": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/react-xml-parser/-/react-xml-parser-1.1.6.tgz",
"integrity": "sha512-m/DU8CIXMJ3KvSAacgGhXYeTtfUKc8XCju/xAlncv3+uXTaOmuSpZI1z1ZTpV6PkDXre+1SJQRyRZY8N5MUp4g=="
},
"read-pkg": { "read-pkg": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz",

View File

@ -23,6 +23,7 @@
"expo-keep-awake": "^8.1.0", "expo-keep-awake": "^8.1.0",
"expo-linear-gradient": "~8.1.0", "expo-linear-gradient": "~8.1.0",
"immutable": "^4.0.0-rc.12", "immutable": "^4.0.0-rc.12",
"moment": "^2.27.0",
"msgpack-lite": "^0.1.26", "msgpack-lite": "^0.1.26",
"msgpack5": "^4.2.1", "msgpack5": "^4.2.1",
"native-base": "2.13.8", "native-base": "2.13.8",
@ -41,6 +42,7 @@
"react-native-unimodules": "~0.8.1", "react-native-unimodules": "~0.8.1",
"react-native-web": "^0.11.7", "react-native-web": "^0.11.7",
"react-redux": "^7.2.0", "react-redux": "^7.2.0",
"react-xml-parser": "^1.1.6",
"reconnecting-websocket": "^4.4.0", "reconnecting-websocket": "^4.4.0",
"redux": "^4.0.5" "redux": "^4.0.5"
}, },

View File

@ -31,7 +31,7 @@ const INITIAL_SETTINGS = {
swimTrackerHost: "192.168.178.110", // testgeraet swimTrackerHost: "192.168.178.110", // testgeraet
analysis: { analysis: {
peaksPerLap: 30, peaksPerLap: 30,
windowSizeInSecs: 5, windowSizeInSecs: 5,
numMeasurementsPerSec: 10, numMeasurementsPerSec: 10,

27
utility/PromiseRequest.js Normal file
View File

@ -0,0 +1,27 @@
let request = obj => {
return new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest();
xhr.open(obj.method || "GET", obj.url);
if (obj.headers) {
Object.keys(obj.headers).forEach(key => {
xhr.setRequestHeader(key, obj.headers[key]);
});
}
if(obj.responseType) {
xhr.responseType = obj.responseType;
}
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(xhr.response);
} else {
reject(xhr.statusText);
}
};
xhr.onerror = () => reject(xhr.statusText);
xhr.send(obj.body);
});
};
export default request;

45
utility/TimeUtils.js Normal file
View File

@ -0,0 +1,45 @@
import moment from 'moment/min/moment-with-locales';
function timeSince(timeStamp, lang = 'de') {
moment.locale(lang);
const now = Math.floor((new Date()).getTime() / 1000);
const secondsPast = now - timeStamp;
if (secondsPast <= 6 * 3600) {
return moment().seconds(-secondsPast).fromNow();
}
else{
const timeStampDate = new Date(timeStamp * 1000);
const dateNow = new Date();
const timeStampMoment = moment.unix(timeStamp);
let dateStr = "";
if (timeStampDate.getDate() == dateNow.getDate())
dateStr = "Heute, " + timeStampMoment.format("HH:mm");
else if (timeStampDate.getDate() + 1 == dateNow.getDate())
dateStr = "Gestern, " + timeStampMoment.format("HH:mm");
else {
dateStr = timeStampMoment.format("ddd, DD.MM.YY um HH:mm");
}
return dateStr;
}
}
const toTimeStr = seconds => {
let minuteStr = String(Math.floor(seconds / 60));
if (minuteStr.length < 2)
minuteStr = "0" + minuteStr;
let secondStr = String(Math.floor(seconds % 60));
if (secondStr.length < 2)
secondStr = "0" + secondStr;
return minuteStr + ":" + secondStr;
}
export { toTimeStr, timeSince };

View File

@ -1,10 +1,12 @@
import React from "react"; import React from "react";
import { import {
ActivityIndicator,
StyleSheet, StyleSheet,
View, View,
StatusBar, StatusBar,
Text, Text,
TouchableOpacity, TouchableOpacity,
RefreshControl,
} from "react-native"; } from "react-native";
import themeColors from './themeColors'; import themeColors from './themeColors';
import EntypoIcon from "react-native-vector-icons/Entypo"; import EntypoIcon from "react-native-vector-icons/Entypo";
@ -12,6 +14,12 @@ import AntDesignIcon from "react-native-vector-icons/AntDesign";
import FaIcon from "react-native-vector-icons/FontAwesome5"; import FaIcon from "react-native-vector-icons/FontAwesome5";
import ImageHeader from "./ImageHeader"; import ImageHeader from "./ImageHeader";
import { SwipeListView } from 'react-native-swipe-list-view'; import { SwipeListView } from 'react-native-swipe-list-view';
import { connect } from 'react-redux';
import request from '../utility/PromiseRequest';
import DataAnalysis from '../data_processing/DataAnalysis';
import * as msgpack from 'msgpack-lite';
import { timeSince } from '../utility/TimeUtils';
import XMLParser from 'react-xml-parser';
function SessionCard(props) { function SessionCard(props) {
return ( return (
@ -43,7 +51,7 @@ function SessionCardBehindSwipe(props) {
<View style={sessionCardStyles.rowBack}> <View style={sessionCardStyles.rowBack}>
<TouchableOpacity <TouchableOpacity
style={sessionCardStyles.deleteButton} style={sessionCardStyles.deleteButton}
onPress={() => deleteRow(rowMap, data.item.key)} onPress={props.onDelete}
> >
<Text style={{ fontSize: 18, color: "white" }}>Löschen</Text> <Text style={{ fontSize: 18, color: "white" }}>Löschen</Text>
</TouchableOpacity> </TouchableOpacity>
@ -123,60 +131,146 @@ const sessionCardStyles = StyleSheet.create({
// --------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------
function parsePropfind(text) {
const parser = new XMLParser();
const xmlDoc = parser.parseFromString(text);
function LastSessionsView(props) { //const parser = new DOMParser();
const data = [ //const xmlDoc = parser.parseFromString(text, "text/xml");
{
textFirstLine: "Gestern 19:12 Uhr",
laps: "31",
activeTime: "26:13",
laps: "35",
momentum: "120",
},
{
textFirstLine: "Montag 18:10 Uhr",
laps: "27",
activeTime: "26:13",
laps: "35",
momentum: "120",
},
]
const renderHiddenItem = (data, rowMap) => ( const responses = xmlDoc.getElementsByTagName("D:response");
<SessionCardBehindSwipe /> let result = [];
); for (let i = 0; i < responses.length; ++i) {
const e = responses[i];
return ( const name = e.getElementsByTagName("D:href")[0].value;
<View style={{ flex: 1 }}> const size = e.getElementsByTagName("D:getcontentlength")[0].value;
<StatusBar hidden={true} /> result.push({
<View style={{ flex: 1 }}> name: name,
<ImageHeader size: parseInt(size),
text="LETZTE SESSIONS" startTime: parseInt(name.split(".")[0])
navigation={props.navigation} });
image={require("../assets/swimmer.jpg")} }
/> return result;
<View style={{ flex: 1, backgroundColor: themeColors["BELIZE HOLE"] }}>
<SwipeListView
style={{ width: "100%" }}
disableRightSwipe="true"
data={data}
renderItem={(data, rowMap) => (
<SessionCard
textFirstLine={data.item.textFirstLine}
laps={data.item.laps}
momentum={data.item.momentum}
activeTime={data.item.activeTime}
laps={data.item.laps} />
)}
renderHiddenItem={renderHiddenItem}
leftOpenValue={0}
rightOpenValue={-120}
stopRightSwipe={-145}
/>
</View>
</View>
</View>
)
} }
export default LastSessionsView; const msgpackCodec = msgpack.createCodec();
msgpackCodec.addExtUnpacker(205, function (byteArr) {
const buffer = byteArr.buffer.slice(byteArr.byteOffset, byteArr.byteLength + byteArr.byteOffset);
const result = new Int16Array(buffer);
return result;
});
async function getSessionDetails(swimTrackerHost, sessionFileName) {
const url = "http://" + swimTrackerHost + "/webdav/" + sessionFileName;
const arrayBuffer = await request({ url: url, responseType: 'arraybuffer' });
return msgpack.decode(new Uint8Array(arrayBuffer), { codec: msgpackCodec });
}
async function getSessionsFromDevice(swimTrackerHost) {
const data = await request({ url: "http://" + swimTrackerHost + "/webdav/", method: "PROPFIND" });
return parsePropfind(data);
}
async function getFullData(swimTrackerHost, analysisSettings) {
const parsed = await getSessionsFromDevice(swimTrackerHost);
for (let index = 0; index < parsed.length; index++) {
const e = parsed[index];
const sessionDetails = await getSessionDetails(swimTrackerHost, e.name);
e.values = sessionDetails.values;
const da = new DataAnalysis();
e.analysis = da.analyze(analysisSettings, e.startTime, e.values);
}
return parsed;
}
// ---------------------------------------------------------------------------------------------
class LastSessionsView extends React.Component {
constructor() {
super();
this.state = { sessions: null, refreshing: false };
}
componentDidMount() {
getFullData(this.props.swimTrackerHost, this.props.analysisSettings).then(
e => this.setState({ sessions: e })
);
}
render() {
const deleteSession = async sessionFileName => {
this.setState({ sessions: null });
await request({ url: "http://" + this.props.swimTrackerHost + "/webdav/" + sessionFileName, method: "DELETE" });
this.setState({ sessions: await getFullData(this.props.swimTrackerHost, this.props.analysisSettings) });
};
const onRefresh = async () => {
this.setState({ refreshing: true });
const newSessions = await getFullData(this.props.swimTrackerHost, this.props.analysisSettings);
this.setState({ sessions: newSessions, refreshing: false });
};
let innerView;
if (this.state.sessions) {
innerView = <SwipeListView
refreshControl={<RefreshControl refreshing={this.state.refreshing} onRefresh={onRefresh} /> }
style={{ width: "100%" }}
keyExtractor={item => item.startTime.toString()}
disableRightSwipe={true}
data={this.state.sessions.reverse()}
renderItem={(data, rowMap) => (
<SessionCard
textFirstLine={timeSince(data.item.startTime)}
laps={(data.item.analysis.peaks.size / this.props.peaksPerLap).toFixed(1)}
momentum={Math.trunc(data.item.analysis.totalMomentum * this.props.kgFactor / 10 / 60)}
activeTime={data.item.analysis.activeTime} />
)}
renderHiddenItem={(data, rowMap) => <SessionCardBehindSwipe onDelete={() => { deleteSession(data.item.name) }} />}
leftOpenValue={0}
rightOpenValue={-120}
stopRightSwipe={-145}
/>
}
else {
innerView = (
<View style={{
flex: 1,
justifyContent: "center",
flexDirection: "row",
justifyContent: "space-around",
}}>
<ActivityIndicator size="large" color="#aaa"></ActivityIndicator>
</View>
);
}
return (
<View style={{ flex: 1 }}>
<StatusBar hidden={true} />
<View style={{ flex: 1 }}>
<ImageHeader
text="LETZTE SESSIONS"
navigation={this.props.navigation}
image={require("../assets/swimmer.jpg")}
/>
<View style={{ flex: 1, backgroundColor: themeColors["BELIZE HOLE"] }}>
{innerView}
</View>
</View>
</View>
)
}
}
const mapStateToProps = (state) => {
return {
swimTrackerHost: state.settings.swimTrackerHost,
analysisSettings: state.settings.analysis,
kgFactor: state.settings.analysis.kgFactor,
peaksPerLap: state.settings.analysis.peaksPerLap,
};
};
export default connect(mapStateToProps)(LastSessionsView);

View File

@ -16,6 +16,7 @@ import { stopSession } from '../state/DeviceReduxCoupling';
import CycleView from '../components/CycleView'; import CycleView from '../components/CycleView';
import IconCard from '../components/IconCard'; import IconCard from '../components/IconCard';
import Graph from '../components/Graph'; import Graph from '../components/Graph';
import {toTimeStr} from '../utility/TimeUtils';
function SmallHeaderView(props) { function SmallHeaderView(props) {
@ -64,16 +65,6 @@ const smallHeaderStyles = StyleSheet.create({
}, },
}); });
const toTimeStr = seconds => {
let minuteStr = String(Math.floor(seconds / 60));
if (minuteStr.length < 2)
minuteStr = "0" + minuteStr;
let secondStr = String(Math.floor(seconds % 60));
if (secondStr.length < 2)
secondStr = "0" + secondStr;
return minuteStr + ":" + secondStr;
}
// --------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------
function TrainingView(props) { function TrainingView(props) {