<repo_name>sylus-code/multisport-endomondo-wrapper<file_sep>/src/Exception/InvalidPathException.php
<?php


class InvalidPathException extends Exception
{

}<file_sep>/readme.md
# Endomondo Api Wrapper

#### Description
As Endomondo is going retired, I've decided to create a tool to manage trainings based on Endomondo training archive json files.

#### Quick start
```php
use SylusCode\MultiSport\EndomondoWrapper\WorkoutImporter;
use SylusCode\MultiSport\EndomondoWrapper\WorkoutParser as EndoParser;
use SylusCode\MultiSport\EndomondoWrapper\WorkoutTypeResolver as EndoTypeResolver;
use Symfony\Component\Finder\Finder;

$finder = new Finder();
$endoResolver = new EndoTypeResolver();
$endoParser = new EndoParser($endoResolver);
$endoWorkoutImporter = new WorkoutImporter($finder, $endoParser);

$path = 'endomondo-2020-11-18.zip';
$result = $endoWorkoutImporter->importFromZipFile($path);
  
var_dump($result);

// Example output:
array(1) {
[0]=>
object(SylusCode\MultiSport\Workout\Workout)#2064 (14) {
["time":"SylusCode\MultiSport\Workout\Workout":private]=>
NULL
["type":"SylusCode\MultiSport\Workout\Workout":private]=>
object(SylusCode\MultiSport\Workout\Type)#62947 (2) {
  ["id":"SylusCode\MultiSport\Workout\Type":private]=>
  int(5)
  ["name":"SylusCode\MultiSport\Workout\Type":private]=>
  string(9) "Siłownia"
}
["distance":"SylusCode\MultiSport\Workout\Workout":private]=>
float(0)
["calories":"SylusCode\MultiSport\Workout\Workout":private]=>
int(63)
["durationTotal":"SylusCode\MultiSport\Workout\Workout":private]=>
int(919)
["points":"SylusCode\MultiSport\Workout\Workout":private]=>
array(918) {
  [0]=>
  object(SylusCode\MultiSport\Workout\Point)#2066 (7) {
    ["time":"SylusCode\MultiSport\Workout\Point":private]=>
    object(DateTime)#2055 (3) {
      ["date"]=>
      string(26) "2020-11-17 09:16:32.000000"
      ["timezone_type"]=>
      int(2)
      ["timezone"]=>
      string(1) "Z"
    }
    ["latitude":"SylusCode\MultiSport\Workout\Point":private]=>
    NULL
    ["longtitude":"SylusCode\MultiSport\Workout\Point":private]=>
    NULL
    ["altitude":"SylusCode\MultiSport\Workout\Point":private]=>
    NULL
    ["distance":"SylusCode\MultiSport\Workout\Point":private]=>
    NULL
    ["heartRate":"SylusCode\MultiSport\Workout\Point":private]=>
    int(72)
    ["speed":"SylusCode\MultiSport\Workout\Point":private]=>
    NULL
  }
  ..
```
#### Disclaimer 
Endomondo workout parser based on archive json files. Hashtags are not included in this parser as Endomondo do not provide hashtagged wotkouts in archive workout pack.

#### What's next
Add unit tests
<file_sep>/src/WorkoutParser.php
<?php

namespace SylusCode\MultiSport\EndomondoWrapper;

use SylusCode\MultiSport\Workout\Point;
use SylusCode\MultiSport\Workout\Workout;

class WorkoutParser
{
    private $endoTypeResolver;

    public function __construct(WorkoutTypeResolverInterface $endoTypeResolver)
    {
        $this->endoTypeResolver = $endoTypeResolver;
    }

    public function parseFromJson(string $json): Workout
    {
        $json = json_decode($json, true);
        $jsonFlatten = $this->array_flatten($json);
        $workout = new Workout();

        if (isset($jsonFlatten['sport'])) {
            $workout->setType($this->endoTypeResolver->resolve($jsonFlatten['sport']));
        }
        if (isset($jsonFlatten['start_time'])) {
            $workout->setStart($this->resolveTimestamp($jsonFlatten['start_time']));
        }
        if (isset($jsonFlatten['distance_km'])) {
            $workout->setDistance($jsonFlatten['distance_km']);
        }
        if (isset($jsonFlatten['duration_s'])) {
            $workout->setDurationTotal($jsonFlatten['duration_s']);
        }
        if (isset($jsonFlatten['calories_kcal'])) {
            $workout->setCalories($jsonFlatten['calories_kcal']);
        }
        if (isset($jsonFlatten['speed_avg_kmh'])) {
            $workout->setAvgSpeed($jsonFlatten['speed_avg_kmh']);
        }
        if (isset($jsonFlatten['heart_rate_avg_bpm'])) {
            $workout->setAvgHeartRate($jsonFlatten['heart_rate_avg_bpm']);
        }
        if (isset($jsonFlatten['heart_rate_max_bpm'])) {
            $workout->setMaxHeartRate($jsonFlatten['heart_rate_max_bpm']);
        }
        if (isset($jsonFlatten['speed_max_kmh'])) {
            $workout->setMaxSpeed($jsonFlatten['speed_max_kmh']);
        }
        if (isset($jsonFlatten['steps'])) {
            $workout->setSteps($jsonFlatten['steps']);
        }
        if (isset($jsonFlatten['notes'])) {
            $workout->setMessage($jsonFlatten['notes']);
        }
        if (isset($jsonFlatten['message'])) {
            $workout->setMessage($jsonFlatten['message']);
        }
        if (isset($jsonFlatten['points'])) {
            $points = [];
            foreach ($jsonFlatten['points'] as $trackPoint) {

                $point = $this->createPoint($trackPoint);
                $points[] = $point;
            }
            $workout->setPoints($points);
        }

        return $workout;
    }

    private function array_flatten(array $json): array
    {
        $return = array();
        foreach ($json as $key => $value) {
            if (is_array($value)) {
                if (isset($value['points'])) {
                    foreach ($value['points'] as $point) {
                        $return['points'][] = $this->array_flatten($point);
                    }
                } else {
                    $return = array_merge($return, $this->array_flatten($value));
                }
            } else {
                $return[$key] = $value;
            }
        }

        return $return;
    }

    private function resolveTimestamp(string $timestamp)
    {
        $datetime = \DateTime::createFromFormat('D M d H:i:s T Y', $timestamp);

        if ($datetime == false) {
            $datetime = substr($timestamp, 0, -2);
            $datetime = \DateTime::createFromFormat('Y-m-d H:i:s', $datetime);
        }

        return $datetime;
    }

    private function createPoint($trackPoint): Point
    {
        $point = new Point();
        if (isset($trackPoint['latitude'])) {
            $point->setLatitude($trackPoint['latitude']);
        }
        if (isset($trackPoint['longitude'])) {
            $point->setLongtitude($trackPoint['longitude']);
        }
        if (isset($trackPoint['timestamp'])) {
            $point->setTime($this->resolveTimestamp($trackPoint['timestamp']));
        }
        if (isset($trackPoint['distance_km'])) {
            $point->setDistance($trackPoint['distance_km']);
        }
        if (isset($trackPoint['altitude'])) {
            $point->setAltitude($trackPoint['altitude']);
        }
        if (isset($trackPoint['heart_rate_bpm'])) {
            $point->setHeartRate($trackPoint['heart_rate_bpm']);
        }

        return $point;
    }
}
<file_sep>/src/WorkoutImporterInterface.php
<?php

namespace SylusCode\MultiSport\EndomondoWrapper;

interface WorkoutImporterInterface
{
    public function importFromZipFile(string $filePath, string $temporaryExtractPath = null ): array;
}
<file_sep>/src/WorkoutTypeResolver.php
<?php

namespace SylusCode\MultiSport\EndomondoWrapper;

use SylusCode\MultiSport\Workout\Type;

class WorkoutTypeResolver implements WorkoutTypeResolverInterface
{
    public function resolve(string $endomondoWorkoutTypeName): Type
    {
        return  new Type($endomondoWorkoutTypeName);
    }
}<file_sep>/src/WorkoutImporter.php
<?php

namespace SylusCode\MultiSport\EndomondoWrapper;

use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Finder\Finder;

class WorkoutImporter implements WorkoutImporterInterface
{
    private $finder;
    private $workoutParser;

    public function __construct(Finder $finder, WorkoutParser $workoutParser)
    {
        $this->finder = $finder;
        $this->workoutParser = $workoutParser;
    }

    public function importFromZipFile(string $filePath, string $temporaryExtractPath = null): array
    {
        $path = dirname($filePath);

        $temporaryExtractPath = $temporaryExtractPath ?? $path;
        $this->extractFiles($filePath,$temporaryExtractPath);

        $this->finder->in($temporaryExtractPath . '/TempData/Workouts')->name('*.json');

        $workouts = [];

        foreach ($this->finder as $file) {
            $string = $file->getContents();
            $workout = $this->workoutParser->parseFromJson($string);
            array_push($workouts, $workout);
        }

        $filesystem = new Filesystem();
        $filesystem->remove([$temporaryExtractPath . '/TempData']);

        return $workouts;
    }

    private function extractFiles(string $filePath, string $temporaryExtractPath): void
    {
        $zip = new \ZipArchive();
        $res = $zip->open($filePath);

        if ($res == !true) {
            throw new \InvalidPathException(sprintf('Could not open %s', $filePath));
        }
        $zip->extractTo($temporaryExtractPath . '/TempData');
        $zip->close();
    }
}
<file_sep>/src/WorkoutTypeResolverInterface.php
<?php

namespace SylusCode\MultiSport\EndomondoWrapper;

use SylusCode\MultiSport\Workout\Type;

interface WorkoutTypeResolverInterface
{
    public function resolve(string $endomondoWorkoutTypeName): Type;
}