Skip to content

Commit

Permalink
WIP: Feat/elevation profile (#81)
Browse files Browse the repository at this point in the history
* merge master into branch + fix style issue

* add specific component (elevation profile).

* WIP - implement elevation profile component.

* WIP - implement elevation profile component.

* add d3 dependencies

* WIP - create graph

* code clean up

* remove location sharing

* code clean up

* update race saga. now dealing with edges and nodes

* WIP - Update race views and components

* Merge branch 'feat/elevationProfile' of https://github.com/totorototo/strava into feat/elevationProfile

# Please enter a commit message to explain why this merge is necessary,
# especially if it merges an updated upstream into a topic branch.
#
# Lines starting with '#' will be ignored, and an empty message aborts
# the commit.

* fix elevation profile area issue.

* make location sharing great again

* WIP - spot current athlete on race map

* split race path into partitions (uphill, downhill).

* WIP - define elevations grades

* fix bit issue

* WIP - split profile into section.

* code clean-up

* code clean-up

* code clean-up

* add region computation

* update region initialisation

* code clean-up

* add service for race/event

* code clean-up

* prettier

* remove deprecated properties

* remove deprecated helper functions

* code clean-up

* code clean-up

* code clean-up

* code clean-up

* code clean-up

* code clean-up

* typo

* code cleanup

* fix strava api update issues

* add helper function

* upgrade to rn 55

* code cleanup

* fix gitignore

* fix ios project file merge issue
  • Loading branch information
totorototo authored May 16, 2018
1 parent f187b50 commit 9a1fb30
Show file tree
Hide file tree
Showing 30 changed files with 6,449 additions and 2,991 deletions.
4 changes: 1 addition & 3 deletions .flowconfig
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,5 @@ suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(<VERSION>\\)? *\\(site=[a-z,_]*
suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy
suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError

unsafe.enable_getters_and_setters=true

[version]
^0.61.0
^0.67.0
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ buck-out/
# For more information about the recommended setup visit:
# https://docs.fastlane.tools/best-practices/source-control/


fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
Expand All @@ -55,3 +54,7 @@ fastlane/screenshots
*.tar
android/keystore.properties


# Bundle artifact
*.jsbundle

10 changes: 2 additions & 8 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,21 +1,15 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.lybitos"
android:versionCode="1"
android:versionName="1.0">
package="com.lybitos">

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

<uses-sdk
android:minSdkVersion="16"
android:targetSdkVersion="22" />

<application
android:name=".MainApplication"
android:allowBackup="true"
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:allowBackup="false"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity"
Expand Down
30 changes: 15 additions & 15 deletions app/components/common/cardList/CardList.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,23 +34,23 @@ export default class CardList extends Component {
render() {
return (
<Card title={this.props.title}>
{this.props.list.map(item =>
{this.props.list.map(item => (
<View key={item.key} style={styles.item}>
{typeof item.image === "string"
? <Image
style={styles.image}
source={{ uri: item.image.toString() }}
/>
: <Icon
style={styles.image}
color={item.image.color}
name={item.image.name}
/>}
<Paragraph>
{item.text}
</Paragraph>
{typeof item.image === "string" ? (
<Image
style={styles.image}
source={{ uri: item.image.toString() }}
/>
) : (
<Icon
iconStyle={styles.image}
color={item.image.color}
name={item.image.name}
/>
)}
<Paragraph>{item.text}</Paragraph>
</View>
)}
))}
</Card>
);
}
Expand Down
10 changes: 8 additions & 2 deletions app/components/specific/cards/AthleteCard.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,20 @@ export default class AthleteCard extends Component {
athlete: PropTypes.shape({
firstname: PropTypes.string,
lastname: PropTypes.string,
profil: PropTypes.string
profile: PropTypes.string
}).isRequired
};

render() {
const { athlete } = this.props;
return (
<Card title="ATHLETE" image={{ uri: athlete.profile }}>
<Card
title="ATHLETE"
image={{
uri:
"https://blog.strava.com/wp-content/uploads/2017/12/Strava_YearInStats_Header_0_1x-1.jpg"
}}
>
<Paragraph>
Anything you need to know about {athlete.firstname} {athlete.lastname}.
performance, data, predictions and much more!
Expand Down
4 changes: 3 additions & 1 deletion app/components/specific/cards/ClubActivitiesCard.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import React, { Component } from "react";
import PropTypes from "prop-types";

import CardList from "../../common/cardList/CardList";
import theme from "../../../theme/theme";
import { getIconName } from "../../../routes/main/clubFeed/helper";

export default class ClubActivitiesCard extends Component {
static propTypes = {
Expand All @@ -19,7 +21,7 @@ export default class ClubActivitiesCard extends Component {
title="ACTIVITIES"
list={activities.map((activity, index) => ({
key: index,
image: activity.athlete.profile,
image: { name: getIconName("runner"), color: theme.PrimaryColor },
text: activity.name
}))}
/>
Expand Down
4 changes: 3 additions & 1 deletion app/components/specific/cards/ClubMembersCard.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import React, { Component } from "react";
import PropTypes from "prop-types";

import CardList from "../../common/cardList/CardList";
import theme from "../../../theme/theme";
import { getIconName } from "../../../routes/main/clubFeed/helper";

export default class ClubMembersCard extends Component {
static propTypes = {
Expand All @@ -22,7 +24,7 @@ export default class ClubMembersCard extends Component {
title="MEMBERS"
list={clubMembers.map(member => ({
key: member.id,
image: member.profile,
image: { name: getIconName("runner"), color: theme.PrimaryColor },
text: member.firstname
}))}
/>
Expand Down
206 changes: 206 additions & 0 deletions app/components/specific/elevationProfile/ElevationProfile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
import React, { Component } from "react";
import { xor, concat } from "lodash";
import PropTypes from "prop-types";
import { View, ART, Dimensions } from "react-native";
import * as scale from "d3-scale";
import * as shape from "d3-shape";
import * as d3Array from "d3-array";

import gps from "../../../store/services/helpers/gps";
import styles from "./styles";
import {
ELEVATION_COLORS,
ELEVATION_GRADE
} from "../../../store/constants/elevation";

const { Surface, Shape } = ART;
const d3 = {
scale,
shape,
d3Array
};

// TODO: how? where? what?
const getRange = percent => {
if (Math.abs(percent) < 5) {
return ELEVATION_GRADE.SMALL;
} else if (Math.abs(percent) >= 5 && Math.abs(percent) < 7) {
return ELEVATION_GRADE.EASY;
} else if (Math.abs(percent) >= 7 && Math.abs(percent) < 10) {
return ELEVATION_GRADE.MEDIUM;
} else if (Math.abs(percent) >= 10 && Math.abs(percent) < 15) {
return ELEVATION_GRADE.DIFFICULT;
} else if (Math.abs(percent) >= 15) {
return ELEVATION_GRADE.HARD;
}
return ELEVATION_GRADE.UNKNOWN;
};

// eslint-disable-next-line no-extend-native
Array.prototype.groupBy = function(fn) {
return this.reduce((accu, item, index, array) => {
const key = fn(item, item, array);
// eslint-disable-next-line no-param-reassign
accu[key] = accu[key] || [];
accu[key].push(item);
return accu;
}, {});
};

export default class ElevationProfile extends Component {
static propTypes = {
path: PropTypes.shape({
edges: PropTypes.arrayOf(
PropTypes.shape({
src: PropTypes.shape({
longitude: PropTypes.number,
latitude: PropTypes.number,
altitude: PropTypes.number
}),
dest: PropTypes.shape({
longitude: PropTypes.number,
latitude: PropTypes.number,
altitude: PropTypes.number
}),
length: PropTypes.number
})
)
}).isRequired
};

static createXScale(start, end, rangeWidth) {
return d3.scale
.scaleLinear()
.domain([start, end])
.range([0, rangeWidth]);
}

static createYScale(minY, maxY, rangeHeight) {
return (
d3.scale
.scaleLinear()
.domain([minY, maxY])
// We invert our range so it outputs using the axis that React uses.
.range([rangeHeight, 0])
);
}

static createColorScale() {
return d3.scale
.scaleThreshold()
.domain([1, 2, 3, 4])
.range([
ELEVATION_COLORS.SMALL,
ELEVATION_COLORS.EASY,
ELEVATION_COLORS.MEDIUM,
ELEVATION_COLORS.DIFFICULT,
ELEVATION_COLORS.HARD
]);
}

static createAxis(edges, graphWidth, graphHeight) {
const totalDistance = edges[edges.length - 1].distanceDone;
const intervalBetweenCheckPoints = 10;
const totalCheckPoints = Math.floor(
totalDistance / intervalBetweenCheckPoints
);

const checkPoints = [];
for (let i = 0; i <= totalCheckPoints; i += 1) {
checkPoints.push(intervalBetweenCheckPoints * i);
}

const ticksIndices = gps.getCheckpointsIndices(checkPoints, 0.1, ...edges);
const AxisData = ticksIndices.map(tickIndex => edges[tickIndex]);
// TODO: draw line dans ta face!

const scaleX = ElevationProfile.createXScale(0, edges.length, graphWidth);
const scaleY = ElevationProfile.createYScale(0, 0, graphHeight);

const lineShape = d3.shape
.line()
// For every x and y-point in our line shape we are given an item from our
// array which we pass through our scale function so we map the domain value
// to the range value.
.x(d => scaleX(d.index))
.y(scaleY(0));

return {
// Pass in our array of data to our line generator to produce the `d={}`
// attribute value that will go into our `<Shape />` component.
path: lineShape(AxisData)
};
}

static createAreaGraph(edges, graphWidth, graphHeight) {
const groupedEdgesByRange = edges.groupBy(item => getRange(item.percent));

// eslint-disable-next-line
const data = Object.entries(groupedEdgesByRange).map(([grade, section]) => {
const indices = section.map(item => item.index);
const missingValueIndices = xor(
Array.from(Array(edges.length).keys()),
indices
);
const fakeData = missingValueIndices.map(item => ({
index: item,
fake: true
}));
return concat(section, fakeData).sort((a, b) => a.index - b.index);
});

// Create our x-scale.
const scaleX = ElevationProfile.createXScale(0, edges.length, graphWidth);

const colorScale = ElevationProfile.createColorScale();

// Collect all y values.
const altitudes = edges.map(location => location.src.altitude);

// Get the min and max y value.
const extentY = d3Array.extent(altitudes);

// Create our y-scale.
const scaleY = ElevationProfile.createYScale(0, extentY[1], graphHeight);

return Object.entries(data).map(([grade, section]) => {
const areaShape = d3.shape
.area()
// For every x and y-point in our line shape we are given an item from our
// array which we pass through our scale function so we map the domain value
// to the range value.
.x(d => scaleX(d.index))
.y1(d => scaleY(d.src.altitude))
.y0(extentY[1])
.defined(d => !d.fake)
.curve(d3.shape.curveLinear);

return {
path: areaShape(section),
color: colorScale(grade)
};
});
}

render() {
const { path } = this.props;
const { width } = Dimensions.get("window");

const areas = ElevationProfile.createAreaGraph(path.edges, width, 100);

return (
<View style={styles.container}>
<Surface width={width} height={100}>
{areas.map(area => (
<Shape
d={area.path}
stroke={area.color}
fill={area.color}
strokeWidth={0.15}
/>
))}
</Surface>
</View>
);
}
}
9 changes: 9 additions & 0 deletions app/components/specific/elevationProfile/styles.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { StyleSheet } from "react-native";

export default StyleSheet.create({
container: {
flex: 1,
justifyContent: "center",
alignItems: "center"
}
});
Loading

0 comments on commit 9a1fb30

Please sign in to comment.