Workouts   A
last analyzed

Complexity

Total Complexity 17

Size/Duplication

Total Lines 306
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 8

Importance

Changes 0
Metric Value
wmc 17
lcom 1
cbo 8
dl 0
loc 306
rs 10
c 0
b 0
f 0

9 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
A getWorkout() 0 23 2
A listWorkouts() 0 21 1
B postTrack() 0 33 4
A flattenEndWorkoutTrackPoint() 0 20 2
B postWorkoutData() 0 35 2
A flattenTrackPoint() 0 16 2
B formatEndomondoTrackPoint() 0 27 1
A generateDeviceWorkoutId() 0 10 2
1
<?php
2
3
declare(strict_types = 1);
4
5
namespace SportTrackerConnector\Endomondo\API;
6
7
use GuzzleHttp\Client;
8
use SportTrackerConnector\Core\Workout\Extension\HR;
9
use SportTrackerConnector\Core\Workout\Track;
10
use SportTrackerConnector\Core\Workout\TrackPoint;
11
use SportTrackerConnector\Endomondo\API\Exception\BadResponseException;
12
13
/**
14
 * Class for working with Endomondo API.
15
 */
16
class Workouts
17
{
18
    private const URL_BASE = 'https://api.mobile.endomondo.com/mobile';
19
    private const URL_WORKOUTS = 'https://api.mobile.endomondo.com/mobile/api/workouts';
20
    private const URL_WORKOUT_GET = 'https://api.mobile.endomondo.com/mobile/api/workout/get';
21
    private const URL_WORKOUT_POST = 'https://api.mobile.endomondo.com/mobile/api/workout/post';
22
    private const URL_TRACK = 'https://api.mobile.endomondo.com/mobile/track';
23
    private const URL_FRIENDS = 'https://api.mobile.endomondo.com/mobile/friends';
24
25
    private const INSTRUCTION_PAUSE = 0;
26
    private const INSTRUCTION_RESUME = 1;
27
    private const INSTRUCTION_START = 2;
28
    private const INSTRUCTION_STOP = 3;
29
    private const INSTRUCTION_NONE = 4;
30
    private const INSTRUCTION_GPS_OFF = 5;
31
    private const INSTRUCTION_LAP = 6;
32
33
    /**
34
     * Endomondo authentication token.
35
     *
36
     * @var string
37
     */
38
    protected $authentication;
39
40
    /**
41
     * @var Client
42
     */
43
    protected $client;
44
45
    /**
46
     * @param Authentication $authentication
47
     * @param Client $client
48
     */
49
    public function __construct(Authentication $authentication, Client $client)
50
    {
51
        $this->authentication = $authentication;
0 ignored issues
show
Documentation Bug introduced by
It seems like $authentication of type object<SportTrackerConne...ndo\API\Authentication> is incompatible with the declared type string of property $authentication.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
52
        $this->client = $client;
53
    }
54
55
    /**
56
     * Get the details of a workout.
57
     *
58
     * Possible fields when getting the workout:
59
     *  device,simple,basic,motivation,interval,hr_zones,weather,polyline_encoded_small,points,lcp_count,tagged_users,pictures,feed
60
     *
61
     * @param string $idWorkout The ID of the workout.
62
     * @return array
63
     * @throws \RuntimeException
64
     */
65
    public function getWorkout($idWorkout): array
66
    {
67
        $response = $this
68
            ->client
69
            ->get(
70
                self::URL_WORKOUT_GET,
71
                array(
72
                    'query' => array(
73
                        'authToken' => $this->authentication->token(),
0 ignored issues
show
Bug introduced by
The method token cannot be called on $this->authentication (of type string).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
74
                        'fields' => 'device,simple,basic,motivation,interval,weather,polyline_encoded_small,points,lcp_count,tagged_users,pictures',
75
                        'workoutId' => $idWorkout
76
                    )
77
                )
78
            );
79
80
81
        $json = \GuzzleHttp\json_decode($response->getBody(), true);
82
        if (isset($json['error'])) {
83
            throw new BadResponseException('Endomondo returned an unexpected error :"' . json_encode($json['error']));
84
        }
85
86
        return $json;
87
    }
88
89
    /**
90
     * Get a list of workouts in a date interval.
91
     *
92
     * @param \DateTimeImmutable $startDate The start date for the workouts.
93
     * @param \DateTimeImmutable $endDate The end date for the workouts.
94
     * @return array
95
     */
96
    public function listWorkouts(\DateTimeImmutable $startDate, \DateTimeImmutable $endDate): array
97
    {
98
        $response = $this
99
            ->client
100
            ->get(
101
                self::URL_WORKOUTS,
102
                array(
103
                    'query' => array(
104
                        'authToken' => $this->authentication->token(),
0 ignored issues
show
Bug introduced by
The method token cannot be called on $this->authentication (of type string).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
105
                        'fields' => 'simple',
106
                        'maxResults' => 100000, // Be lazy and fetch everything in one request.
107
                        'after' => $startDate->format('Y-m-d H:i:s \U\T\C'),
108
                        'before' => $endDate->format('Y-m-d H:i:s \U\T\C')
109
                    )
110
                )
111
            );
112
113
        $json = \GuzzleHttp\json_decode($response->getBody(), true);
114
115
        return $json['data'];
116
    }
117
118
    /**
119
     * Post one workout track to Endomondo.
120
     *
121
     * @param Track $track
122
     * @param string $sport
123
     * @return null|string
124
     */
125
    public function postTrack(Track $track, string $sport)
126
    {
127
        $deviceWorkoutId = $this->generateDeviceWorkoutId();
128
        $duration = $track->duration()->totalSeconds();
129
130
        $workoutId = null;
0 ignored issues
show
Unused Code introduced by
$workoutId is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
131
        $previousPoint = null;
132
        $distance = 0;
133
        $speed = 0;
134
        // Split in chunks of 100 points like the mobile app.
135
        foreach (array_chunk($track->trackPoints(), 100) as $trackPoints) {
136
            $data = array();
137
            /** @var TrackPoint[] $trackPoints */
138
            foreach ($trackPoints as $trackPoint) {
139
                if ($previousPoint !== null) {
140
                    $distance += $trackPoint->distanceFromPoint($previousPoint);
141
                    $speed = $trackPoint->speed($previousPoint);
142
                }
143
144
                $data[] = $this->flattenTrackPoint($trackPoint, $distance, $speed);
145
146
                $previousPoint = $trackPoint;
147
            }
148
149
            $this->postWorkoutData($deviceWorkoutId, $sport, $duration, $data);
150
        }
151
152
        // End of workout data.
153
        $data = $this->flattenEndWorkoutTrackPoint($track, $speed);
154
        $workoutId = $this->postWorkoutData($deviceWorkoutId, $sport, $duration, array($data));
155
156
        return $workoutId;
157
    }
158
159
    /**
160
     * Post the workout end data.
161
     *
162
     * @param Track $track The track.
163
     * @param float $speed The speed for the last point.
164
     * @return string The workout ID.
165
     */
166
    private function flattenEndWorkoutTrackPoint(Track $track, $speed)
167
    {
168
        $endDateTime = clone $track->endDateTime();
169
        $endDateTime->setTimezone(new \DateTimeZone('UTC'));
170
        $distance = $track->length();
171
        $lastTrackPoint = $track->lastTrackPoint();
172
173
        $totalAscent = $lastTrackPoint->elevation(); // TODO Compute it from the track, this is not correct.
174
175
        return $this->formatEndomondoTrackPoint(
176
            $endDateTime,
177
            self::INSTRUCTION_STOP,
178
            $lastTrackPoint->latitude(),
179
            $lastTrackPoint->longitude(),
180
            $distance,
181
            $speed,
182
            $totalAscent,
183
            $lastTrackPoint->hasExtension(HR::ID()) ? $lastTrackPoint->extension(HR::ID())->value() : ''
184
        );
185
    }
186
187
    /**
188
     * Post workout data chunk.
189
     *
190
     * @param string $deviceWorkoutId The workout ID in progress of the device.
191
     * @param string $sport The sport.
192
     * @param integer $duration The duration in seconds.
193
     * @param array $data The data points to post.
194
     * @return string The workout ID.
195
     * @throws \RuntimeException
196
     */
197
    private function postWorkoutData($deviceWorkoutId, $sport, $duration, array $data): string
198
    {
199
        $body = \GuzzleHttp\Psr7\stream_for(gzencode(implode("\n", $data)));
200
201
        $response = $this
202
            ->client
203
            ->post(
204
                self::URL_TRACK,
205
                array(
206
                    'query' => array(
207
                        'authToken' => $this->authentication->token(),
0 ignored issues
show
Bug introduced by
The method token cannot be called on $this->authentication (of type string).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
208
                        'gzip' => 'true',
209
                        'workoutId' => $deviceWorkoutId,
210
                        'sport' => $sport,
211
                        'duration' => $duration,
212
                        'audioMessage' => 'false',
213
                        'goalType' => 'BASIC',
214
                        'extendedResponse' => 'true'
215
                    ),
216
                    'headers' => array(
217
                        'Content-Type' => 'application/octet-stream'
218
                    ),
219
                    'body' => $body
220
                )
221
            );
222
223
        $responseBody = $response->getBody()->getContents();
224
        $response = parse_ini_string($responseBody);
225
226
        if (array_key_exists('workout.id', $response)) {
227
            return $response['workout.id'];
228
        }
229
230
        throw new \RuntimeException('Unexpected response from Endomondo. Data may be partially uploaded. Response was: ' . $responseBody);
231
    }
232
233
    /**
234
     * Flatten a track point to be posted on Endomondo.
235
     *
236
     * @param TrackPoint $trackPoint The track point to flatten.
237
     * @param float $distance The total distance the point in meters.
238
     * @param float $speed The speed the point in km/h from the previous point.
239
     * @return string
240
     */
241
    private function flattenTrackPoint(TrackPoint $trackPoint, $distance, $speed): string
242
    {
243
        $dateTime = clone $trackPoint->dateTime();
244
        $dateTime->setTimezone(new \DateTimeZone('UTC'));
245
246
        return $this->formatEndomondoTrackPoint(
247
            $dateTime,
248
            self::INSTRUCTION_START,
249
            $trackPoint->latitude(),
250
            $trackPoint->longitude(),
251
            $distance,
252
            $speed,
253
            $trackPoint->elevation(),
254
            $trackPoint->hasExtension(HR::ID()) ? $trackPoint->extension(HR::ID())->value() : ''
255
        );
256
    }
257
258
    /**
259
     * Format a point to send to Endomondo when posting a new workout.
260
     *
261
     * Type:
262
     *  0 - pause
263
     *  1 - running ?
264
     *  2 - running
265
     *  3 - stop
266
     *
267
     * @param \DateTimeImmutable $dateTime
268
     * @param integer $type The post type (0-6). Don't know what they mean.
269
     * @param string $lat The latitude of the point.
270
     * @param string $lon The longitude of the point.
271
     * @param string $distance The distance in meters.
272
     * @param string $speed The speed in km/h.
273
     * @param string $elevation The elevation
274
     * @param string $heartRate The heart rate.
275
     * @param string $cadence The cadence (in rpm).
276
     * @return string
277
     */
278
    private function formatEndomondoTrackPoint(
279
        \DateTimeImmutable $dateTime,
280
        $type,
281
        $lat = null,
282
        $lon = null,
283
        $distance = null,
284
        $speed = null,
285
        $elevation = null,
286
        $heartRate = null,
287
        $cadence = null
288
    ): string {
289
        $dateTime = clone $dateTime;
290
        $dateTime->setTimezone(new \DateTimeZone('UTC'));
291
292
        return sprintf(
293
            '%s;%s;%s;%s;%s;%s;%s;%s;%s;',
294
            $dateTime->format('Y-m-d H:i:s \U\T\C'),
295
            $type,
296
            $lat,
297
            $lon,
298
            $distance / 1000,
299
            $speed,
300
            $elevation,
301
            $heartRate,
302
            $cadence
303
        );
304
    }
305
306
    /**
307
     * Generate a big number of specified length.
308
     *
309
     * @return string
310
     */
311
    private function generateDeviceWorkoutId()
312
    {
313
        $randNumber = '-';
314
315
        for ($i = 0; $i < 19; $i++) {
316
            $randNumber .= random_int(0, 9);
317
        }
318
319
        return $randNumber;
320
    }
321
}
322