From 2d563908080ca665ab7be501368225b6d92686db Mon Sep 17 00:00:00 2001 From: Martin Bauer Date: Sun, 9 Aug 2020 21:53:38 +0200 Subject: [PATCH] Last sessions view --- data_processing/DataAnalysis.js | 8 +- package-lock.json | 10 ++ package.json | 2 + state/Reducer.js | 2 +- utility/PromiseRequest.js | 27 +++++ utility/TimeUtils.js | 45 +++++++ views/LastSessionsView.js | 202 +++++++++++++++++++++++--------- views/TrainingView.js | 11 +- 8 files changed, 238 insertions(+), 69 deletions(-) create mode 100644 utility/PromiseRequest.js create mode 100644 utility/TimeUtils.js diff --git a/data_processing/DataAnalysis.js b/data_processing/DataAnalysis.js index 3703fa0..8b75bbe 100644 --- a/data_processing/DataAnalysis.js +++ b/data_processing/DataAnalysis.js @@ -25,8 +25,8 @@ export default class DataAnalysis { newData = allMeasurements; console.log("cache reset"); } - - const newDataArr = newData.toArray(); + const allMeasurementsSize = newData.size ? newData.size : newData.length; + const newDataArr = (typeof newData.toArray ==="function") ? newData.toArray() : newData; // active time const newAverages = this.movingAverage.addVector(newDataArr); @@ -49,10 +49,10 @@ export default class DataAnalysis { const momentumWindow = windowed.reduce((sum, x) => sum + x, 0); - this.analyzedUpToIdx = allMeasurements.size; + this.analyzedUpToIdx = allMeasurementsSize; return { peaks: this.allPeaks, - totalTime: allMeasurements.size / analysisParameters.numMeasurementsPerSec, + totalTime: allMeasurementsSize / analysisParameters.numMeasurementsPerSec, activeTime: this.activeMeasurements / analysisParameters.numMeasurementsPerSec, totalMomentum: this.aggregatedMomentum, diff --git a/package-lock.json b/package-lock.json index 10aa142..a4c48f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6631,6 +6631,11 @@ "minimist": "^1.2.5" } }, + "moment": { + "version": "2.27.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.27.0.tgz", + "integrity": "sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ==" + }, "morgan": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", @@ -8161,6 +8166,11 @@ "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": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", diff --git a/package.json b/package.json index 1592cd8..b506b49 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "expo-keep-awake": "^8.1.0", "expo-linear-gradient": "~8.1.0", "immutable": "^4.0.0-rc.12", + "moment": "^2.27.0", "msgpack-lite": "^0.1.26", "msgpack5": "^4.2.1", "native-base": "2.13.8", @@ -41,6 +42,7 @@ "react-native-unimodules": "~0.8.1", "react-native-web": "^0.11.7", "react-redux": "^7.2.0", + "react-xml-parser": "^1.1.6", "reconnecting-websocket": "^4.4.0", "redux": "^4.0.5" }, diff --git a/state/Reducer.js b/state/Reducer.js index 624a9e0..726c682 100644 --- a/state/Reducer.js +++ b/state/Reducer.js @@ -31,7 +31,7 @@ const INITIAL_SETTINGS = { swimTrackerHost: "192.168.178.110", // testgeraet analysis: { - peaksPerLap: 30, + peaksPerLap: 30, windowSizeInSecs: 5, numMeasurementsPerSec: 10, diff --git a/utility/PromiseRequest.js b/utility/PromiseRequest.js new file mode 100644 index 0000000..86cfeda --- /dev/null +++ b/utility/PromiseRequest.js @@ -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; diff --git a/utility/TimeUtils.js b/utility/TimeUtils.js new file mode 100644 index 0000000..cb34b27 --- /dev/null +++ b/utility/TimeUtils.js @@ -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 }; diff --git a/views/LastSessionsView.js b/views/LastSessionsView.js index 358b7b8..bceebad 100644 --- a/views/LastSessionsView.js +++ b/views/LastSessionsView.js @@ -1,10 +1,12 @@ import React from "react"; import { + ActivityIndicator, StyleSheet, View, StatusBar, Text, TouchableOpacity, + RefreshControl, } from "react-native"; import themeColors from './themeColors'; 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 ImageHeader from "./ImageHeader"; 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) { return ( @@ -43,7 +51,7 @@ function SessionCardBehindSwipe(props) { deleteRow(rowMap, data.item.key)} + onPress={props.onDelete} > Löschen @@ -123,60 +131,146 @@ const sessionCardStyles = StyleSheet.create({ // --------------------------------------------------------------------------------------------- +function parsePropfind(text) { + const parser = new XMLParser(); + const xmlDoc = parser.parseFromString(text); -function LastSessionsView(props) { - const data = [ - { - 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 parser = new DOMParser(); + //const xmlDoc = parser.parseFromString(text, "text/xml"); - const renderHiddenItem = (data, rowMap) => ( - - ); - - return ( - - - ) + const responses = xmlDoc.getElementsByTagName("D:response"); + let result = []; + for (let i = 0; i < responses.length; ++i) { + const e = responses[i]; + const name = e.getElementsByTagName("D:href")[0].value; + const size = e.getElementsByTagName("D:getcontentlength")[0].value; + result.push({ + name: name, + size: parseInt(size), + startTime: parseInt(name.split(".")[0]) + }); + } + return result; } -export default LastSessionsView; \ No newline at end of file +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 = } + style={{ width: "100%" }} + keyExtractor={item => item.startTime.toString()} + disableRightSwipe={true} + data={this.state.sessions.reverse()} + renderItem={(data, rowMap) => ( + + )} + renderHiddenItem={(data, rowMap) => { deleteSession(data.item.name) }} />} + leftOpenValue={0} + rightOpenValue={-120} + stopRightSwipe={-145} + /> + } + else { + innerView = ( + + + + ); + } + + return ( + + + ) + } +} + +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); + diff --git a/views/TrainingView.js b/views/TrainingView.js index ea6f0bc..0648b9d 100644 --- a/views/TrainingView.js +++ b/views/TrainingView.js @@ -16,6 +16,7 @@ import { stopSession } from '../state/DeviceReduxCoupling'; import CycleView from '../components/CycleView'; import IconCard from '../components/IconCard'; import Graph from '../components/Graph'; +import {toTimeStr} from '../utility/TimeUtils'; 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) {