Skip to content

Commit 7f0af90

Browse files
Added activity processing
1 parent cbd4753 commit 7f0af90

31 files changed

Lines changed: 414 additions & 51 deletions

package-lock.json

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"author": "Nora Söderlund",
99
"devDependencies": {
1010
"@cloudflare/workers-types": "^4.20230518.0",
11+
"@ridetracker/ridetrackertypes": "^0.9.7",
1112
"@types/geolib": "^2.0.23",
1213
"@types/itty-router-extras": "^0.4.0",
1314
"@types/react": "^18.2.6",
@@ -16,6 +17,7 @@
1617
"wrangler": "^3.0.1"
1718
},
1819
"dependencies": {
20+
"@googlemaps/polyline-codec": "^1.0.28",
1921
"@ridetracker/authservice": "^0.9.2",
2022
"@ridetracker/discordwebhooksclient": "^0.9.0",
2123
"geolib": "^3.3.4",
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { Activity } from "@ridetracker/ridetrackertypes";
2+
3+
export async function getActivityById(database: D1Database, id: string): Promise<Activity> {
4+
return await database.prepare("SELECT start_area AS startArea, finish_area AS finishArea, local_id AS localId, activities.* FROM activities WHERE id = ?").bind(id).first<Activity>();
5+
};
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { Recording } from "@ridetracker/ridetrackertypes";
2+
import getActivityRecordingFromArray from "./getActivityRecordingFromArray";
3+
4+
export default async function getActivityRecording(bucket: R2Bucket, activityId: string): Promise<Recording | null> {
5+
const response = await bucket.get(`/activities/${activityId}.json`);
6+
7+
if(!response)
8+
return null;
9+
10+
const result: any = await response.json();
11+
12+
const version = (Array.isArray(result))?(1):(result.version);
13+
14+
// We do not, ever, want to rewrite the original upload file;
15+
// instead, upgrade the recordings each time it has to be reprocessed;
16+
// or if reason be, migrate processed recordings to a new bucket folder.
17+
18+
switch(version) {
19+
// Version 1 is an array of sessions with coords objects.
20+
case 1:
21+
return getActivityRecordingFromArray(result);
22+
23+
// Version 2 is currently representated by the Recording type.
24+
case 2:
25+
return result;
26+
27+
default:
28+
return null;
29+
}
30+
};
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { Recording, RecordingSession, RecordingSessionAltitude, RecordingSessionBatteryState, RecordingSessionCoordinate, RecordingSessionSpeed, RecordingV1Session, RecordingV1SessionBatteryState } from "@ridetracker/ridetrackertypes";
2+
3+
export default function getActivityRecordingFromArray(sessions: RecordingV1Session[]): Recording | null {
4+
if(!sessions.length)
5+
return null;
6+
7+
const recording: Recording = {
8+
// localId was implemented in RideTrackerApp-0.9.3
9+
id: sessions[0].id,
10+
11+
version: 2,
12+
13+
sessions: sessions.map<RecordingSession>((session) => {
14+
return {
15+
id: session.id,
16+
17+
coordinates: session.locations.map<RecordingSessionCoordinate>((location) => {
18+
return {
19+
coordinate: {
20+
latitude: location.coords.latitude,
21+
longitude: location.coords.longitude
22+
},
23+
24+
accuracy: location.coords.accuracy,
25+
timestamp: location.timestamp
26+
};
27+
}),
28+
29+
altitudes: session.locations.map<RecordingSessionAltitude>((location) => {
30+
return {
31+
altitude: location.coords.altitude,
32+
accuracy: location.coords.altitudeAccuracy,
33+
timestamp: location.timestamp
34+
};
35+
}),
36+
37+
batteryStates: (session.battery ?? []).map<RecordingSessionBatteryState>((battery) => {
38+
let batteryState: RecordingSessionBatteryState["batteryState"]["batteryState"] = "UNKNOWN";
39+
40+
switch(battery.batteryState) {
41+
case RecordingV1SessionBatteryState.UNKNOWN: {
42+
batteryState = "UNKNOWN";
43+
44+
break;
45+
};
46+
47+
case RecordingV1SessionBatteryState.UNPLUGGED: {
48+
batteryState = "UNPLUGGED";
49+
50+
break;
51+
};
52+
53+
case RecordingV1SessionBatteryState.CHARGING: {
54+
batteryState = "CHARGING";
55+
56+
break;
57+
};
58+
59+
case RecordingV1SessionBatteryState.FULL: {
60+
batteryState = "FULL";
61+
62+
break;
63+
};
64+
}
65+
66+
return {
67+
batteryState: {
68+
batteryState,
69+
batteryLevel: battery.batteryLevel,
70+
lowPowerMode: battery.lowPowerMode
71+
},
72+
73+
timestamp: battery.timestamp
74+
}
75+
}),
76+
77+
speeds: session.locations.map<RecordingSessionSpeed>((location) => {
78+
return {
79+
speed: location.coords.speed,
80+
accuracy: location.coords.accuracy,
81+
timestamp: location.timestamp
82+
};
83+
}),
84+
85+
calories: [],
86+
87+
heartRates: []
88+
}
89+
})
90+
}
91+
92+
return recording;
93+
};
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { Recording } from "@ridetracker/ridetrackertypes";
2+
import { createActivitySummary } from "../../summaries/createActivitySummary";
3+
import { getDistance } from "geolib";
4+
5+
export default async function processActivityRecordingDistanceSummary(env: Env, activityId: string, recording: Recording) {
6+
let distance = 0;
7+
8+
recording.sessions.forEach((session) => {
9+
if(session.coordinates.length < 2)
10+
return;
11+
12+
for(let index = 1; index < session.coordinates.length; index++) {
13+
distance += getDistance(session.coordinates[index - 1].coordinate, session.coordinates[index].coordinate, session.coordinates[index].accuracy);
14+
}
15+
});
16+
17+
await createActivitySummary(env.DATABASE, activityId, "distance", distance);
18+
};
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { Recording } from "@ridetracker/ridetrackertypes";
2+
import { createActivitySummary } from "../../summaries/createActivitySummary";
3+
4+
export default async function processActivityRecordingElevationSummary(env: Env, activityId: string, recording: Recording) {
5+
let elevation = 0;
6+
7+
recording.sessions.forEach((session) => {
8+
if(session.altitudes.length < 2)
9+
return;
10+
11+
for(let index = 1; index < session.altitudes.length; index++) {
12+
elevation += Math.max(0, Math.min(session.altitudes[index].altitude - session.altitudes[index - 1].altitude, session.altitudes[index].accuracy));
13+
}
14+
});
15+
16+
await createActivitySummary(env.DATABASE, activityId, "elevation", elevation);
17+
};
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { Recording, RecordingSessionCoordinate } from "@ridetracker/ridetrackertypes";
2+
import getMapsAreaFromCoordinates from "../../../maps/getMapsAreaFromCoordinates";
3+
import getRecordingLastItem from "../../../recordings/getRecordingLastItem";
4+
import { updateActivityFinishArea } from "../../updateActivityFinishArea";
5+
6+
export default async function processActivityRecordingFinishArea(env: Env, activityId: string, recording: Recording) {
7+
const lastCoordinate = getRecordingLastItem<RecordingSessionCoordinate>(recording, "coordinates");
8+
9+
if(!lastCoordinate) {
10+
console.warn("Failed to find an initial coordinate.");
11+
12+
return;
13+
}
14+
15+
const result = await getMapsAreaFromCoordinates(env.GOOGLE_MAPS_API_KEY, lastCoordinate.coordinate);
16+
17+
await updateActivityFinishArea(env.DATABASE, activityId, result);
18+
};
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { Recording, RecordingSessionCoordinate } from "@ridetracker/ridetrackertypes";
2+
import getRecordingFirstItem from "../../../recordings/getRecordingFirstItem";
3+
import getMapsAreaFromCoordinates from "../../../maps/getMapsAreaFromCoordinates";
4+
import { updateActivityStartArea } from "../../updateActivityStartArea";
5+
import { Coordinate } from "../../../../models/Coordinate";
6+
import { encode } from "@googlemaps/polyline-codec";
7+
import { getDistance } from "geolib";
8+
import { updateActivityPolylines } from "../../updateActivityPolylines";
9+
10+
export default async function processActivityRecordingPolylines(env: Env, activityId: string, recording: Recording) {
11+
const polylines: string[] = [];
12+
13+
recording.sessions.forEach((session) => {
14+
const significantCoordinates: Coordinate[] = [];
15+
16+
if(session.coordinates.length >= 2) {
17+
significantCoordinates.push(session.coordinates[0].coordinate);
18+
19+
let distance = 0;
20+
21+
for(let index = 1; index < session.coordinates.length - 1; index++) {
22+
distance += getDistance(session.coordinates[index - 1].coordinate, session.coordinates[index].coordinate, session.coordinates[index].accuracy);
23+
24+
if(distance > 20)
25+
significantCoordinates.push(session.coordinates[index].coordinate);
26+
}
27+
28+
if(session.coordinates.length > 2)
29+
significantCoordinates.push(session.coordinates[session.coordinates.length - 1].coordinate);
30+
}
31+
32+
polylines.push(encode(significantCoordinates.map((coordinate) => [ coordinate.latitude, coordinate.longitude ])));
33+
});
34+
35+
updateActivityPolylines(env.DATABASE, activityId, JSON.stringify(polylines));
36+
};
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { Recording } from "@ridetracker/ridetrackertypes";
2+
import { createActivitySummary } from "../../summaries/createActivitySummary";
3+
4+
export default async function processActivityRecordingSpeedSummary(env: Env, activityId: string, recording: Recording) {
5+
const speeds = recording.sessions.flatMap((session) => session.speeds);
6+
7+
const maxSpeed = speeds.reduce((highest, current) => {
8+
if(current.speed > highest)
9+
return current.speed;
10+
11+
return highest;
12+
}, 0);
13+
14+
const speedSum = speeds.reduce((accumulated, current) => accumulated + current.speed, 0);
15+
const averageSpeed = (speedSum / speeds.length) || 0;
16+
17+
await Promise.allSettled([
18+
createActivitySummary(env.DATABASE, activityId, "average_speed", averageSpeed),
19+
createActivitySummary(env.DATABASE, activityId, "max_speed", maxSpeed)
20+
]);
21+
};

0 commit comments

Comments
 (0)