<?php
namespace App\Controller\API;
use App\Controller\API\AbstractAPIController;
use App\Entity\Device;
use App\Entity\Exercise;
use App\Entity\DataVersion;
use App\Entity\WorkoutTemplate;
use App\Entity\FormCorrection;
use App\Entity\WorkoutData;
use App\Repository\WorkoutDataRepository;
use App\Repository\ExerciseRepository;
use App\ErrorLogRepository;
use App\Entity\ErrorLog;
use App\Repository\WorkoutTemplateRepository;
use App\Repository\FormCorrectionRepository;
use App\Repository\ClassifierParamsRepository;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Routing\Annotation\Route;
class WorkoutTemplateAPIController extends AbstractAPIController
{
#[Route("/api/error-log",methods: ["POST"])]
public function postErrorLog(Request $request, EntityManagerInterface $em): JsonResponse {
$data = json_decode($request->getContent(), true);
for ($i = 0; $i < count($data); $i++) {
$entry = $data[$i];
$errorLog = new ErrorLog();
$errorLog->setTimestamp($entry["timestamp"]);
$errorLog->setGymName($entry["gymName"]);
$errorLog->setLog(strval($entry["log"]));
$em->persist($errorLog);
$em->flush();
}
return $this->json(true);
}
#[Route("/api/leaderboard-list/{id}",methods: ["GET"])]
public function getWorkoutLeaderboardList(Request $request, WorkoutTemplate $workout, WorkoutDataRepository $repo): JsonResponse {
$sum = 0;
$max = 0;
$data = $repo->findAll();
$count = 0;
$entries = [];
foreach ($data as $entry) {
if ($entry == null || $entry->getJsonData() == null) continue;
$jsonData = json_decode($entry->getJsonData(), true);
// todo: only count entries that were not exited for the leaderboard
if ($jsonData["workoutCloudId"] != $workout->getId()) continue;
$count++;
$entries[] = intval($jsonData["btScore"]);
$sum = $sum + $jsonData["btScore"];
if ($jsonData["btScore"] > $max) $max = $jsonData["btScore"];
}
if ($sum == 0) return $this->json(["mean" => 0, "max" => 0, "count" => 0, "entries" => []]);
return $this->json([
"mean" => $sum / $count,
"max" => $max,
"count" => $count,
"entries" => $entries
]);
}
#[Route("/api/leaderboard/{id}",methods: ["GET"])]
public function getWorkoutLeaderboard(Request $request, WorkoutTemplate $workout, WorkoutDataRepository $repo): JsonResponse {
$sum = 0;
$max = 0;
$data = $repo->findAll();
$count = 0;
foreach ($data as $entry) {
if ($entry == null || $entry->getJsonData() == null) continue;
$jsonData = json_decode($entry->getJsonData(), true);
// todo: only count entries that were not exited for the leaderboard
if ($jsonData["workoutCloudId"] != $workout->getId()) continue;
$count++;
$sum = $sum + $jsonData["btScore"];
if ($jsonData["btScore"] > $max) $max = $jsonData["btScore"];
}
if ($sum == 0) return $this->json(["mean" => 0, "max" => 0, "count" => 0]);
return $this->json([
"mean" => $sum / $count,
"max" => $max,
"count" => $count
]);
}
#[Route("/api/workout-data", methods: ["POST"])]
public function postWorkoutData(
Request $request,
EntityManagerInterface $em,
): JsonResponse
{
// check jwt-authorization
$token = $this->authenticationService->verifyToken($request);
if (!$token) {
return $this->json([
'error' => 'Unauthorized access.'
], 401);
}
ini_set('memory_limit', '-1');
try {
$jsonData = json_decode($request->getContent(), true);
$workoutData = new WorkoutData();
$workoutData->setJsonData($jsonData);
$em->persist($workoutData);
$em->flush();
return $this->json([
'success' => true,
]);
} catch (Exception $e) {
if (!$token) {
return $this->json([
'error' => $e->getMessage()
], 422);
}
}
}
#[Route('/api/workout-templates/{language}', defaults: ['language' => 'en'], name: 'app_workout_get_templates', methods: ["GET"])]
public function index(
Request $request,
WorkoutTemplateRepository $workoutTemplateRepository,
ExerciseRepository $exerciseRepository,
ClassifierParamsRepository $paramsRepo,
EntityManagerInterface $em
): JsonResponse
{
// check jwt-authorization
$token = $this->authenticationService->verifyToken($request);
if (!$token) {
return $this->json([
'error' => 'Unauthorized access.'
], 401);
}
ini_set('memory_limit', '-1');
$language = $request->attributes->get("language");
// WORKOUT SECTION
$workouts = $workoutTemplateRepository->findAll();
$workoutObjects = [];
foreach ($workouts as $workout) {
$workoutObjects[] = [
'id' => $workout->getId(),
'deleted' => false,
'updated' => false,
'added' => true,
'version' => 0,
'data' => [
'name' => $workout->getName(),
'moduleType' => $workout->getModuleType(),
'description' => $workout->getDescription(),
'equipment' => array_map('strval', array_values((array) $workout->getEquipment())),
'targetArea' => $workout->getTargetArea(),
'level' => $workout->getLevel(),
'calories' => 0,
'image' => $this->getWorkoutImage($workout),
'exercises' => json_decode($workout->getExercises()),
],
];
}
// EXERCISE SECTION
$exercises = $exerciseRepository->findAll();
$exerciseObjects = [];
foreach ($exercises as $exercise) {
// was added
$exerciseObjects[] = [
'id' => $exercise->getId(),
'deleted' => false,
'updated' => false,
'added' => true,
'version' => 0,
'data' => $this->aggregateExerciseData($exercise, $language)
];
}
// PARAMETERS SECTION
$paramsList = [];
$params = $paramsRepo->findAll();
foreach ($params as $param) {
$paramsList[] = [
'romSmootherBufferSize' => $param->getRomSmootherBufferSize(),
'repetitionCountRange' => $param->getRepetitionCountRange(),
'versionHash' => $param->getVersionHash(),
];
}
return $this->json([
'workoutDeltas' => $workoutObjects,
'exerciseDeltas' => $exerciseObjects,
'parameters' => $paramsList,
'audioBaseFiles' => $this->getLocalizedBaseAudioFiles($language),
'dataVersion' => $em->getRepository(DataVersion::class)->findOneBy([], ['id' => 'ASC'])->getVersionCounter()
]);
}
#[Route('/api/workout-assets/{language}', defaults: ["language" => "en"], name: 'app_workout_assets', methods: ["POST"])]
public function exerciseAssets(
Request $request,
ExerciseRepository $exerciseRepository,
FormCorrectionRepository $formCorrectionRepository
): JsonResponse {
// Check JWT authorization
$token = $this->authenticationService->verifyToken($request);
if (!$token) {
return $this->json([
'error' => 'Unauthorized access.'
], 401);
}
ini_set('memory_limit', '-1');
$language = $request->attributes->get("language");
$data = json_decode($request->getContent(), true);
$idList = [];
if (is_array($data)) {
$idList = $data;
}
// EXERCISE SECTION
$exercises = $exerciseRepository->findBy(array('id' => $idList));
$exerciseObjects = [];
foreach ($exercises as $exercise) {
// form correction objects
$formCorrections = $formCorrectionRepository->findBy(["exerciseId" => $exercise->getId()]);
// Was added
$exerciseObjects[] = [
'id' => $exercise->getId(),
'deleted' => false,
'updated' => false,
'added' => true,
'version' => 0,
'data' => $this->aggregateExerciseAssets($exercise, $formCorrections, $language)
];
}
return $this->json([
'exerciseAssets' => $exerciseObjects,
]);
}
private function getWorkoutImage(WorkoutTemplate $workout)
{
$image = $workout->getImage();
if ($image != null) $image = file_get_contents($this->getParameter('target_directory') . '/' . $workout->getImage());
$image = base64_encode($image);
return $image;
}
private function aggregateExerciseData(Exercise $exercise, $language)
{
$gifFile = $exercise->getGif();
if ($gifFile != null) {
$gifFile = file_get_contents($this->getParameter('target_directory') . '/' . $exercise->getGif());
}
$base64GifFile = base64_encode($gifFile);
return [
'id' => $exercise->getId(),
'rotationRange' => $exercise->getRotationRange(),
'name' => $exercise->getLocalizedName('en'), // will always return the name of the exercise in english
"howTo" => $exercise->getLocalizedDescription($language),
'landmarkInputs' => $exercise->getLandmarkInputs(),
'formCorrection' => $exercise->isFormCorrection(),
'type' => $exercise->getType(),
'gif' => $base64GifFile,
];
}
private function getLocalizedBaseAudioFiles($language) {
$get_into_starting_position = base64_encode(file_get_contents($this->getParameter("target_directory") . "/" . "get_into_starting_position_" . $language . ".mp3"));
$great_follow_the_curve = base64_encode(file_get_contents($this->getParameter("target_directory") . "/" . "great_follow_the_curve_" . $language . ".mp3"));
$follow_the_curve = base64_encode(file_get_contents($this->getParameter("target_directory") . "/" . "follow_the_curve_" . $language . ".mp3"));
return [
"get_into_starting_position" => $get_into_starting_position,
"great_follow_the_curve" => $great_follow_the_curve,
"follow_the_curve" => $follow_the_curve,
];
}
private function aggregateExerciseAssets(Exercise $exercise, $formCorrections, $language)
{
$regModel = $exercise->getRegressorModelFileName();
$binModel = $exercise->getClassifierModelFileName();
// $errorModel = $exercise->getErrorModelFileName();
$exerciseDetector = $exercise->getExerciseDetector();
$audio0 = null;
$audio1 = null;
$audio2 = null;
$audio3 = null;
$audio4 = null;
$outlineFile = $exercise->getStartPoseOutlineImage();
if ($regModel != null) {
$regModel = file_get_contents($this->getParameter('target_directory') . '/' . $exercise->getRegressorModelFileName());
}
if ($binModel != null) {
$binModel = file_get_contents($this->getParameter('target_directory') . '/' . $exercise->getClassifierModelFileName());
}
// if ($errorModel != null) {
// $errorModel = file_get_contents($this->getParameter('target_directory') . '/' . $exercise->getErrorModelFileName());
//}
if ($exerciseDetector != null) {
$exerciseDetector = file_get_contents($this->getParameter('target_directory') . '/' . $exercise->getExerciseDetector());
}
$nextExerciseAudio = null;
if (file_exists($this->getParameter("target_directory") . "/" . $exercise->getNameEn() . "_" . $language . ".mp3")) {
$nextExerciseAudio = file_get_contents($this->getParameter("target_directory") . "/" . $exercise->getNameEn() . "_" . $language . ".mp3");
}
$audio0 = null;
try {
$audio0Path = $this->getParameter('target_directory') . '/' . $exercise->getNameEn() . "_form0_" . $language . ".mp3";
if (file_exists($audio0Path)) {
$audio0 = file_get_contents($audio0Path);
}
} catch (Exception $e) {
// no op
}
$audio1 = null;
try {
$audio1Path = $this->getParameter('target_directory') . '/' . $exercise->getNameEn() . "_form1_" . $language . ".mp3";
if (file_exists($audio1Path)) {
$audio1 = file_get_contents($audio1Path);
}
} catch (Exception $e) {
// no op
}
$audio2 = null;
try {
$audio2Path = $this->getParameter('target_directory') . '/' . $exercise->getNameEn() . "_form2_" . $language . ".mp3";
if (file_exists($audio2Path)) {
$audio2 = file_get_contents($audio2Path);
}
} catch (Exception $e) {
// no op
}
$audio3 = null;
try {
$audio3Path = $this->getParameter('target_directory') . '/' . $exercise->getNameEn() . "_form3_" . $language . ".mp3";
if (file_exists($audio3Path)) {
$audio3 = file_get_contents($audio3Path);
}
} catch (Exception $e) {
// no op
}
$audio4 = null;
try {
$audio4Path = $this->getParameter('target_directory') . '/' . $exercise->getNameEn() . "_form4_" . $language . ".mp3";
if (file_exists($audio4Path)) {
$audio4 = file_get_contents($audio4Path);
}
} catch (Exception $e) {
// no op
}
if ($outlineFile != null) {
$outlineFile = file_get_contents($this->getParameter('target_directory') . '/' . $exercise->getStartPoseOutlineImage());
}
$base64RegModel = base64_encode($regModel);
$base64BinModel = base64_encode($binModel);
//$base64ErrorModel = base64_encode($errorModel);
$base64ExerciseDetectorModel = base64_encode($exerciseDetector);
$baseAudio0 = base64_encode($audio0);
$baseAudio1 = base64_encode($audio1);
$baseAudio2 = base64_encode($audio2);
$baseAudio3 = base64_encode($audio3);
$baseAudio4 = base64_encode($audio4);
$base64OutlineImageFile = base64_encode($outlineFile);
$nextExerciseAudio = base64_encode($nextExerciseAudio);
// aggregate error correction objects
$formCorrectionObjects = [];
foreach ($formCorrections as $formCorrection) {
$image = $formCorrection->getImage();
$imageFile = null;
if ($image != null) {
$imageFile = file_get_contents($this->getParameter('target_directory') . '/' . $image);
}
$base64GifFile = base64_encode($imageFile);
$errorModelFilePath = $formCorrection->getErrorModelFilePath();
$errorModel = null;
if ($errorModelFilePath != null) {
$errorModel = file_get_contents($this->getParameter('target_directory') . '/' . $errorModelFilePath);
}
$base64ErrorModel = base64_encode($errorModel);
$formCorrectionObjects[] = [
"exerciseId" => $formCorrection->getExerciseId(),
"exerciseName" => $formCorrection->getExerciseName(),
"errorCode" => $formCorrection->getErrorCode(),
"description" => $formCorrection->getDescription(),
"image" => $base64GifFile,
"title" => $formCorrection->getTitle(),
"minRom" => $formCorrection->getMinRom(),
"maxRom" => $formCorrection->getMaxRom(),
"errorModel" => $base64ErrorModel
];
}
return [
"formCorrections" => $formCorrectionObjects,
'models' => [
'regressor' => $base64RegModel,
'classifier' => $base64BinModel,
//'errorCoder' => $base64ErrorModel,
'exerciseDetector' => $base64ExerciseDetectorModel,
],
'formCorrectionText1' => $exercise->getFormCorrectionText1(),
'formCorrectionText2' => $exercise->getFormCorrectionText2(),
'formCorrectionText3' => $exercise->getFormCorrectionText3(),
'formCorrectionText4' => $exercise->getFormCorrectionText4(),
'audio' => [
'a0' => $baseAudio0,
'a1' => $baseAudio1,
'a2' => $baseAudio2,
'a3' => $baseAudio3,
'a4' => $baseAudio4
],
'startPoseOutlineImage' => $base64OutlineImageFile,
"next_exercise_audio" => $nextExerciseAudio,
];
}
#[Route('/api/heartbeat', name: 'app_heartbeat', methods: ["POST"])]
public function updateHeartbeat(Request $request, EntityManagerInterface $entityManager): JsonResponse
{
// Decode JSON payload
$data = json_decode($request->getContent(), true);
// 1. Ensure "gym" key exists
if (!isset($data['gym'])) {
return new JsonResponse(['error' => 'Missing "gym" in payload'], 400);
}
// 2. Look up the Device by "gym"
$gymValue = $data['gym'];
$device = $entityManager->getRepository(Device::class)->findOneBy(['gym' => $gymValue]);
// 3. If no Device found, return an error
if (!$device) {
return new JsonResponse(['error' => 'No device found for gym: ' . $gymValue], 404);
}
// 4. Update lastHeartBeat to "now"
$device->setLastHeartBeat(
new \DateTime('now', new \DateTimeZone('Europe/Zurich'))
);
// 5. Persist changes
$entityManager->flush();
// Return success
return new JsonResponse(['status' => 'ok'], 200);
}
}