<?php
/**
* @license Silk Software House Sp. Z O.O.
*/
declare(strict_types=1);
namespace App\Controller\Panel;
use App\Annotation\ApplySettings;
use App\Assembler\DataSampleAssembler;
use App\Controller\Panel\Traits\FormatableTwigDataTrait;
use App\DataBridge\DataAbstraction;
use App\DataBridge\DataTransformers\AgeOfMaturityTransformer;
use App\DataBridge\DataTransformers\TemperatureTransformer;
use App\DataBridge\DataTransformers\StrengthTransformer;
use App\DataBridge\Implementations\CanvasJsTransformer;
use App\DataBridge\Implementations\DataTransformerInterface;
use App\DataBridge\StrengthPointsAddHelper;
use App\Entity\ChannelSetting;
use App\Entity\Measurement;
use App\Entity\Notification;
use App\Entity\User;
use App\Entity\UserSetting;
use App\Factory\MeasurementViewFactory;
use App\Helpers\DatabaseAwareTrait;
use App\Helpers\MessagesTrait;
use App\Helpers\SecurityAwareTrait;
use App\Model\View\MeasurementView;
use App\Helpers\RecipeResolver;
use App\Repository\UserSettingRepository;
use Exception;
use Doctrine\ORM\EntityManagerInterface;
use FOS\RestBundle\Controller\Annotations as Rest;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* Device list controller.
*/
class ChartsController extends AbstractController
{
use DatabaseAwareTrait;
use FormatableTwigDataTrait;
use SecurityAwareTrait;
use MessagesTrait;
/** @var DataAbstraction */
private DataAbstraction $chartDataAbstraction;
/** @var DataSampleAssembler */
private DataSampleAssembler $dataSampleAssembler;
/** @var RecipeResolver */
private RecipeResolver $recipeResolver;
/** @var array */
private array $strengthPoints = [];
/** @var TranslatorInterface */
private TranslatorInterface $translator;
/** @var MeasurementViewFactory */
private MeasurementViewFactory $factory;
/**
* todo: refactor. base controller or trait injection
* TableController constructor.
*
* @param DataAbstraction $chartDataAbstraction
* @param DataSampleAssembler $dataSampleAssembler
* @param RecipeResolver $recipeResolver
* @param TranslatorInterface $translator
* @param EntityManagerInterface $entityManager
* @param MeasurementViewFactory $factory
*/
public function __construct(
DataAbstraction $chartDataAbstraction,
DataSampleAssembler $dataSampleAssembler,
RecipeResolver $recipeResolver,
TranslatorInterface $translator,
EntityManagerInterface $entityManager,
MeasurementViewFactory $factory
) {
$this->chartDataAbstraction = $chartDataAbstraction;
$this->dataSampleAssembler = $dataSampleAssembler;
$this->recipeResolver = $recipeResolver;
$this->translator = $translator;
$this->entityManager = $entityManager;
$this->factory = $factory;
}
/**
* @IsGranted("show", subject="measurement")
*
* @Rest\Get(
* "/panel/device/measurement/{id}/chart/strength/{isRelative}",
* name="device_measurement_chart_strength",
* requirements={"isRelative"="(true|false)"},
* defaults={"isRelative"=false}
* )
* @Rest\Post(
* "/panel/device/measurement/{id}/chart/strength/{isRelative}",
* name="device_measurement_chart_strength_post",
* requirements={"isRelative"="(true|false)"},
* defaults={"isRelative"=false}
* )
*
* @Template("panel/device-list/canvasjs_chart.html.twig")
*
* @param Measurement $measurement
* @param string|null $isRelative
*
* @return array
*
* @throws Exception
*/
public function strengthChart(Measurement $measurement, ?string $isRelative = null): array
{
$form = $this->createAndHandleNameForm($measurement);
$relative = ('true' === $isRelative);
return [
'formName' => $form->createView(),
'channelNameForm' => $this->createAndHandleChannelNameForm()->createView(),
'channelRecipeForm' => $this->createAndHandleChannelRecipeForm()->createView(),
'device' => $this->formatDeviceData($measurement),
'measurement_meta' => $this->formatMeasurementData($measurement),
'measurement' => $measurement,
'channels_meta' => $this->formatChannelsData($measurement),
'type' => 'strength',
'title' => $this->chartDataAbstraction->getTitle(),
'unit' => $this->chartDataAbstraction->getUnit(),
'legend' => $relative ? 'Relative time [h]' : 'Real time',
'yLabel' => 'Strength [MPa]',
'isRelativeTimescale' => $relative ? 1 : 0,
'startDate' => $measurement->getStartedAt(),
] + $this->getFontSize();
}
/**
* @IsGranted("show", subject="measurement")
*
* @Rest\Post("/panel/device/measurement/{id}/strength-notification", name="device_measurement_strength_notification_create")
*
* @param Measurement $measurement
* @param Request $request
* @param EntityManagerInterface $entityManager
* @param TranslatorInterface $translator
*
* @return RedirectResponse
*/
public function createStrengthNotification(Measurement $measurement, Request $request, EntityManagerInterface $entityManager, TranslatorInterface $translator): RedirectResponse
{
$redirect = new RedirectResponse($request->headers->get('referer') ?: $this->generateUrl('device_measurement_chart_strength', ['id' => $measurement->getId()]));
if (!$this->isCsrfTokenValid('strength_notification', $request->request->get('token'))) {
$this->addFlash('danger', $translator->trans('Wrong CSRF token provided. Please try to perform this action again.'));
return $redirect;
}
$email = trim((string) $request->request->get('email'));
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$this->addFlash('danger', $translator->trans('strength-notification.error.email'));
return $redirect;
}
$strengthTrigger = str_replace(',', '.', trim((string) $request->request->get('strengthTrigger')));
if (!is_numeric($strengthTrigger) || (float) $strengthTrigger <= 0) {
$this->addFlash('danger', $translator->trans('strength-notification.error.target'));
return $redirect;
}
$selectedChannelIds = array_map('intval', (array) $request->request->get('channels', []));
if (!$measurement->getSetting() || 0 === count($selectedChannelIds)) {
$this->addFlash('danger', $translator->trans('strength-notification.error.channels'));
return $redirect;
}
$created = 0;
$duplicates = 0;
$repository = $entityManager->getRepository(Notification::class);
foreach ($measurement->getSetting()->getChannelSettings() as $channelSetting) {
if (!in_array($channelSetting->getId(), $selectedChannelIds, true) || $channelSetting->isDeleted()) {
continue;
}
$existing = $repository->findOneBy([
'user' => $this->getUser(),
'channelSetting' => $channelSetting,
'triggeredAt' => null,
'strengthTrigger' => (float) $strengthTrigger,
'email' => $email,
]);
if ($existing) {
++$duplicates;
continue;
}
$notification = (new Notification())
->setUser($this->getUser())
->setChannelSetting($channelSetting)
->setStrengthTrigger((float) $strengthTrigger)
->setEmail($email);
$entityManager->persist($notification);
++$created;
}
if (0 === $created && 0 === $duplicates) {
$this->addFlash('danger', $translator->trans('strength-notification.error.channels'));
return $redirect;
}
if ($created > 0) {
$entityManager->flush();
}
if ($created > 0) {
$this->addFlash('success', $translator->trans('strength-notification.success', ['%count%' => $created]));
}
if ($duplicates > 0) {
$this->addFlash('info', $translator->trans('strength-notification.duplicates', ['%count%' => $duplicates]));
}
return $redirect;
}
/**
* @IsGranted("show", subject="measurement")
*
* @Rest\Get(
* "/panel/device/measurement/{id}/chart/maturity/{isRelative}",
* name="device_measurement_chart_maturity",
* requirements={"isRelative"="(true|false)"},
* defaults={"isRelative"=false}
* )
* @Rest\Post(
* "/panel/device/measurement/{id}/chart/maturity/{isRelative}",
* name="device_measurement_chart_maturity_post",
* requirements={"isRelative"="(true|false)"},
* defaults={"isRelative"=false}
* )
*
* @Template("panel/device-list/canvasjs_chart.html.twig")
*
* @param Measurement $measurement
* @param string|null $isRelative
*
* @return array
*
* @throws Exception
*/
public function maturityChart(Measurement $measurement, ?string $isRelative = null): array
{
$form = $this->createAndHandleNameForm($measurement);
$relative = ('true' === $isRelative);
return [
'formName' => $form->createView(),
'channelNameForm' => $this->createAndHandleChannelNameForm()->createView(),
'channelRecipeForm' => $this->createAndHandleChannelRecipeForm()->createView(),
'device' => $this->formatDeviceData($measurement),
'measurement_meta' => $this->formatMeasurementData($measurement),
'measurement' => $measurement,
'channels_meta' => $this->formatChannelsData($measurement),
'type' => 'maturity',
'title' => $this->chartDataAbstraction->getTitle(),
'unit' => $this->chartDataAbstraction->getUnit(),
'legend' => $relative ? 'Relative time [h]' : 'Real time',
'yLabel' => 'Age of maturity [h]',
'isRelativeTimescale' => $relative ? 1 : 0,
'startDate' => $measurement->getStartedAt(),
] + $this->getFontSize();
}
/**
* #AppCache()
*
* @IsGranted("show", subject="measurement")
*
* @ApplySettings()
*
* @Rest\Get(
* "/panel/device/measurement/{id}/chart/{type}/{isRelative}/data",
* name="device_measurement_chart_api",
* requirements={"isRelative"="(true|false)"},
* defaults={"isRelative"=false}
* )
*
* @param Measurement $measurement
* @param string $type
* @param string|null $isRelative
*
* @return Response
*
* @throws Exception
*/
public function chartData(Measurement $measurement, string $type, ?string $isRelative = null): Response
{
$implementation = $this->getValueTransformer(('true' === $isRelative), $type);
$chartDataAbstraction = $this->getDataAbstraction($this->factory->create($measurement), $implementation);
try {
if (in_array($type, ['temperature', 'strength'], true)) {
$dataArray = $chartDataAbstraction->getDataArray();
if ($measurement->getSetting() && $measurement->getSetting()->getIsAutomaticStrengthPointsEnabled()) {
$usedRecipes = $chartDataAbstraction->getUsedRecipes();
$strengthPoints = $this->buildAutomaticStrengthPoints($dataArray, $usedRecipes);
if ($strengthPoints) {
$helper = (new StrengthPointsAddHelper($dataArray));
if ('temperature' === $type) {
$startingPointForRelativeTimePoints = ('true' === $isRelative) ? $implementation->getFirstDatePoint() : null;
$helper->setUsedRecipe($usedRecipes);
$helper->setFirstDateForRelativeTimesets($startingPointForRelativeTimePoints);
$data = $helper->addStrengthPoints($strengthPoints)->getJson();
} else {
$data = $helper->addStrengthValuePoints($strengthPoints)->getJson();
}
} else {
$data = json_encode($dataArray, JSON_PRESERVE_ZERO_FRACTION);
}
} else {
$data = json_encode($dataArray, JSON_PRESERVE_ZERO_FRACTION);
}
} else {
$data = $chartDataAbstraction->getJson();
}
} catch (Exception $e) {
$this->addMessage($e->getMessage(), 'danger');
throw $e;
}
$data = str_replace(
['{{Channel}}', '{{default recipe}}', '{{CalculatedChannel}}', '{{AddedChannel}}'],
[$this->translator->trans('Channel'), $this->translator->trans('Default recipe'), $this->translator->trans('calculated-channel'), $this->translator->trans('added-channel-legend')],
$data
);
if ($implementation->isError()) {
$this->addMessageArray($implementation->getMessages(), 'info');
}
$data = $this->mergeInMessagesJson($data);
$response = new Response($data);
$response->headers->set('Content-Type', 'application/json');
return $response;
}
/**
* @IsGranted("edit", subject="measurement")
*
* @ApplySettings()
*
* @Rest\Post("/panel/device/measurement/{id}/chart/automatic-strength-points-toggle", name="device_measurement_automatic_strength_points_toggle")
* @Rest\Post("/panel/device/measurement/{id}/chart/temperature/automatic-strength-points-toggle", name="device_measurement_temperature_automatic_strength_points_toggle")
*/
public function toggleAutomaticStrengthPoints(Measurement $measurement, Request $request): Response
{
if (!$this->isCsrfTokenValid('automatic_strength_points', $request->get('token'))) {
return $this->json([
'success' => false,
'message' => $this->translator->trans('Wrong CSRF token provided. Please try to perform this action again.'),
], Response::HTTP_NOT_FOUND);
}
if (!$measurement->getSetting()) {
return $this->json([
'success' => false,
'message' => $this->translator->trans('Entity was not found.'),
], Response::HTTP_NOT_FOUND);
}
$enabled = filter_var($request->get('enabled'), FILTER_VALIDATE_BOOLEAN);
$setting = $measurement->getSetting();
$setting->setIsAutomaticStrengthPointsEnabled($enabled);
$this->entityManager->persist($setting);
$this->entityManager->flush();
return $this->json([
'success' => true,
'enabled' => $setting->getIsAutomaticStrengthPointsEnabled(),
'message' => $this->translator->trans(
$setting->getIsAutomaticStrengthPointsEnabled()
? 'temperature-chart.strength-points.enabled'
: 'temperature-chart.strength-points.disabled'
),
], Response::HTTP_OK);
}
/**
* @IsGranted("edit", subject="measurement")
*
* @ApplySettings()
*
* @Rest\Post("/panel/device/measurement/{id}/chart/temperature/split-toggle", name="device_measurement_temperature_split_toggle")
*/
public function toggleTemperatureSplit(Measurement $measurement, Request $request): Response
{
if (!$this->isCsrfTokenValid('temperature_split_view', $request->get('token'))) {
return $this->json([
'success' => false,
'message' => $this->translator->trans('Wrong CSRF token provided. Please try to perform this action again.'),
], Response::HTTP_NOT_FOUND);
}
if (!$measurement->getSetting()) {
return $this->json([
'success' => false,
'message' => $this->translator->trans('Entity was not found.'),
], Response::HTTP_NOT_FOUND);
}
$setting = $measurement->getSetting();
$setting->setIsTemperatureSplitView(!$setting->getIsTemperatureSplitView());
$this->entityManager->persist($setting);
$this->entityManager->flush();
return $this->json([
'success' => true,
'enabled' => $setting->getIsTemperatureSplitView(),
'message' => $this->translator->trans(
$setting->getIsTemperatureSplitView()
? 'temperature-chart.split.enabled'
: 'temperature-chart.split.disabled'
),
], Response::HTTP_OK);
}
/**
* @Rest\Post(
* "/panel/device/channel/{id}/diagram/{group}",
* name="device_measurement_temperature_channel_move",
* requirements={"group"="(1|2)"}
* )
*/
public function moveTemperatureChannel(ChannelSetting $channel, int $group, Request $request): Response
{
if (!$this->isCsrfTokenValid('temperature_split_move', $request->get('token'))) {
return $this->json([
'success' => false,
'message' => $this->translator->trans('Wrong CSRF token provided. Please try to perform this action again.'),
], Response::HTTP_NOT_FOUND);
}
$measurementSetting = $channel->getMeasurementSetting();
$measurement = $measurementSetting ? $measurementSetting->getMeasurement() : null;
if (!$measurement) {
return $this->json([
'success' => false,
'message' => $this->translator->trans('Entity was not found.'),
], Response::HTTP_NOT_FOUND);
}
$this->denyAccessUnlessGranted('edit', $measurement);
$channel->setDiagramGroup($group);
$this->entityManager->persist($channel);
$hasSecondaryChannels = false;
foreach ($measurementSetting->getChannelSettings() as $setting) {
if (!$setting->isDeleted() && 2 === $setting->getDiagramGroup()) {
$hasSecondaryChannels = true;
break;
}
}
$measurementSetting->setIsTemperatureSplitView($hasSecondaryChannels);
$this->entityManager->persist($measurementSetting);
$this->entityManager->flush();
return $this->json([
'success' => true,
'splitEnabled' => $measurementSetting->getIsTemperatureSplitView(),
'message' => $this->translator->trans('temperature-chart.split.moved'),
], Response::HTTP_OK);
}
private function buildAutomaticStrengthPoints(array $dataArray, array $usedRecipes): array
{
$strengthPoints = [];
foreach ($dataArray['chartData'] ?? [] as $index => $channel) {
if (!empty($channel['isDeleted']) || empty($usedRecipes[$index])) {
continue;
}
$fcc28 = $usedRecipes[$index]->getFcc();
if (!$fcc28 || $fcc28 <= 0) {
continue;
}
$strengthPoints[$index] = [
round($fcc28 * 0.25, 2),
round($fcc28 * 0.50, 2),
round($fcc28 * 0.75, 2),
];
}
return $strengthPoints;
}
/**
* @IsGranted("show", subject="measurement")
*
* @ApplySettings()
*
* @Rest\Get(
* "/panel/device/measurement/{id}/chart/temperature/{isRelative}",
* name="device_measurement_chart_temperature",
* requirements={"isRelative"="(true|false)"},
* defaults={"isRelative"=false}
* )
* @Rest\Post(
* "/panel/device/measurement/{id}/chart/temperature/{isRelative}",
* name="device_measurement_chart_temperature_post",
* requirements={"isRelative"="(true|false)"},
* defaults={"isRelative"=false}
* )
*
* @Template("panel/device-list/canvasjs_chart.html.twig")
*
* @param Measurement $measurement
* @param string|null $isRelative
*
* @return array
*
* @throws Exception
*/
public function temperatureChart(Measurement $measurement, ?string $isRelative = null): array
{
$form = $this->createAndHandleNameForm($measurement);
$relative = ('true' === $isRelative);
return [
'formName' => $form->createView(),
'channelNameForm' => $this->createAndHandleChannelNameForm()->createView(),
'channelRecipeForm' => $this->createAndHandleChannelRecipeForm()->createView(),
'device' => $this->formatDeviceData($measurement),
'measurement_meta' => $this->formatMeasurementData($measurement),
'measurement' => $measurement,
'measurement_extended_name' => $measurement->getNameWithRelativeNumberPerDevice(),
'channels_meta' => $this->formatChannelsData($measurement),
'type' => 'temperature',
'title' => $this->chartDataAbstraction->getTitle(),
'unit' => $this->chartDataAbstraction->getUnit(),
'legend' => $relative ? 'Relative time [h]' : 'Real time',
'yLabel' => 'Temperature [°C]',
'isRelativeTimescale' => $relative ? 1 : 0,
'startDate' => $measurement->getStartedAt(),
] + $this->getFontSize();
}
/**
*
* @Rest\Put("/panel/channel/edit/{id}/{type}", name="panel_channel_edit_put")
*
* @param ChannelSetting $channel
* @param string $type
*
* @return Response
*
* @throws Exception
*/
public function channelEdit(ChannelSetting $channel, string $type): Response
{
if ('name' === $type) {
$response = $this->createAndHandleChannelNameForm($channel);
} elseif ('recipe' === $type) {
$response = $this->createAndHandleChannelRecipeForm($channel);
} else {
throw new Exception('Invalid request type');
}
return new Response($response);
}
/**
* @param MeasurementView $view
* @param DataTransformerInterface $implementation
*
* @return DataAbstraction
*/
private function getDataAbstraction(MeasurementView $view, DataTransformerInterface $implementation): DataAbstraction
{
return $this->chartDataAbstraction
->setImplementation($implementation)
->setMeasurementView($view);
}
/**
* @param bool $isRelative
* @param string|null $type
* todo : handle other transformers
*
* @return DataTransformerInterface
*/
private function getValueTransformer(bool $isRelative, ?string $type = null): DataTransformerInterface
{
$typeToValueTransformer = [
'default' => new TemperatureTransformer(),
'temperature' => new TemperatureTransformer(),
'maturity' => new AgeOfMaturityTransformer(),
'strength' => new StrengthTransformer(),
];
$valueTransformer = $typeToValueTransformer[$type] ?? $typeToValueTransformer['default'];
$implementation = (new CanvasJsTransformer($valueTransformer, $this->recipeResolver, $this->translator))
->setRelativeTimeAxis($isRelative);
$implementation->setRelativeTimeAxis($isRelative);
return $implementation;
}
/**
* @return array
*/
private function getFontSize(): array
{
/** @var User $user */
$user = $this->getUser();
/** @var UserSettingRepository $userSettingRepository */
$userSettingRepository = $this->getRepository(UserSetting::class);
$userSetting = $userSettingRepository->getOrCreateForUser($user);
return ['fontSizeCoefficient' => ($userSetting->getFontSizeCoefficient())];
}
}