Covivo/mobicoop

View on GitHub
api/src/Match/Service/MassPublicTransportPotentialManager.php

Summary

Maintainability
A
0 mins
Test Coverage
<?php

/**
 * Copyright (c) 2020, MOBICOOP. All rights reserved.
 * This project is dual licensed under AGPL and proprietary licence.
 ***************************
 *    This program is free software: you can redistribute it and/or modify
 *    it under the terms of the GNU Affero General Public License as
 *    published by the Free Software Foundation, either version 3 of the
 *    License, or (at your option) any later version.
 *
 *    This program is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *    GNU Affero General Public License for more details.
 *
 *    You should have received a copy of the GNU Affero General Public License
 *    along with this program.  If not, see <gnu.org/licenses>.
 ***************************
 *    Licence MOBICOOP described in the file
 *    LICENSE
 */

namespace App\Match\Service;

use App\Geography\Service\GeoTools;
use App\Match\Entity\Mass;
use App\Match\Entity\MassPerson;
use App\Match\Entity\MassPTJourney;
use App\Match\Event\MassPublicTransportSolutionsGatheredEvent;
use App\Match\Exception\MassException;
use App\Match\Repository\MassPTJourneyRepository;
use App\Match\Repository\MassRepository;
use App\PublicTransport\Entity\PTJourney;
use App\PublicTransport\Service\PTDataProvider;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

/**
 * Mass public transport potential manager.
 *
 * @author Maxime Bardot <maxime.bardot@mobicoop.org>
 */
class MassPublicTransportPotentialManager
{
    private const TIME_LIMIT = 6 * 60 * 60;
    private $massRepository;
    private $pTDataProvider;
    private $entityManager;
    private $massPTJourneyRepository;
    private $geoTools;
    private $params;
    private $eventDispatcher;

    private $exludedForPtMaxConnections;
    private $exludedForPtMaxDistanceWalkFromHome;
    private $exludedForPtMaxDistanceWalkFromWork;
    private $exludedForPtMaxNbCarDuration;

    private $logger;

    public function __construct(
        MassRepository $massRepository,
        PTDataProvider $pTDataProvider,
        EntityManagerInterface $entityManager,
        MassPTJourneyRepository $massPTJourneyRepository,
        GeoTools $geoTools,
        EventDispatcherInterface $eventDispatcher,
        LoggerInterface $logger,
        array $params
    ) {
        $this->massRepository = $massRepository;
        $this->pTDataProvider = $pTDataProvider;
        $this->entityManager = $entityManager;
        $this->massPTJourneyRepository = $massPTJourneyRepository;
        $this->geoTools = $geoTools;
        $this->eventDispatcher = $eventDispatcher;
        $this->params = $params;
        $this->exludedForPtMaxConnections = 0;
        $this->exludedForPtMaxDistanceWalkFromHome = 0;
        $this->exludedForPtMaxDistanceWalkFromWork = 0;
        $this->exludedForPtMaxNbCarDuration = 0;
        $this->logger = $logger;
    }

    /**
     * Get the public transport potential of a Mass from a PT Api.
     *
     * @param int $id Id of the Mass
     */
    public function getPublicTransportPotential(int $id): Mass
    {
        $this->logger->info('Mass PT Potential | Start '.(new \DateTime('UTC'))->format('Ymd H:i:s.u'));

        set_time_limit(self::TIME_LIMIT);

        $mass = $this->massRepository->find($id);

        if ($mass->getStatus() < 4) {
            throw new MassException(MassException::MASS_NOT_ANALYZED);
        }

        // Update the gettingPublicTransportationPotentialDate
        $mass->setGettingPublicTransportationPotentialDate(new \DateTime());
        $this->entityManager->flush();

        // We remove the previous PTJourneys
        $this->logger->info('Mass PT Potential | Remove previous Mass PTJourney '.(new \DateTime('UTC'))->format('Ymd H:i:s.u'));
        $this->massPTJourneyRepository->deleteMassPTJourneysOfAMass($id);

        $TPPotential = [];

        foreach ($mass->getPersons() as $person) {
            if (is_null($person->getPersonalAddress()->getLatitude()) || is_null($person->getPersonalAddress()->getLongitude())) {
                continue;
            }

            $this->logger->info('Mass PT Potential | Get PT potential for person id = '.$person->getId().' '.(new \DateTime('UTC'))->format('Ymd H:i:s.u'));
            $this->logger->info('Mass PT Potential | From = '.$person->getPersonalAddress()->getLatitude().':'.$person->getPersonalAddress()->getLongitude().' To : '.$person->getWorkAddress()->getLatitude().':'.$person->getWorkAddress()->getLongitude().' '.(new \DateTime('UTC'))->format('Ymd H:i:s.u'));

            /**
             * @var MassPerson $person
             */
            $results = $this->pTDataProvider->getJourneys(
                null,
                $person->getPersonalAddress()->getLatitude(),
                $person->getPersonalAddress()->getLongitude(),
                $person->getWorkAddress()->getLatitude(),
                $person->getWorkAddress()->getLongitude(),
                new \DateTime(date('Y-m-d').' '.$person->getOutwardTime()->format('H:i:s'), new \DateTimeZone('Europe/Paris'))
            );
            // var_dump($results);die;
            foreach ($results as $ptjourney) {
                $massPTJourney = $this->buildMassPTJourney($ptjourney);
                $massPTJourney->setMassPerson($person);
                $TPPotential[] = $massPTJourney;

                // We persist the MassPTJourney
                $this->entityManager->persist($massPTJourney);
            }
        }

        // Update the gotPublicTransportationPotentialDate
        $mass->setGotPublicTransportationPotentialDate(new \DateTime());

        $this->entityManager->flush();

        $mass->setPublicTransportPotential($TPPotential);

        // Send an email to the operator
        $event = new MassPublicTransportSolutionsGatheredEvent($mass);
        $this->eventDispatcher->dispatch(MassPublicTransportSolutionsGatheredEvent::NAME, $event);

        $this->logger->info('Mass PT Potential | End '.(new \DateTime('UTC'))->format('Ymd H:i:s.u'));

        return $mass;
    }

    /**
     * Build the MassPTJourney from a PTJourney.
     *
     * @param PTJourney $ptjourney The PTJourney
     */
    public function buildMassPTJourney(PTJourney $ptjourney): MassPTJourney
    {
        $massPTJourney = new MassPTJourney();

        $massPTJourney->setDuration($ptjourney->getDuration());

        $massPTJourney->setDistance($ptjourney->getDistance());

        $massPTJourney->setProvider($ptjourney->getProvider());

        // Duration from home
        $legFromHome = $ptjourney->getPTLegs()[0];
        $durationFromHome = $legFromHome->getDuration();
        $massPTJourney->setDurationWalkFromHome($durationFromHome);

        // Distance from home
        $distanceFromHome = 4000 * $durationFromHome / 3600;
        $massPTJourney->setDistanceWalkFromHome($distanceFromHome);

        // Duration from Work
        $legFromWork = $ptjourney->getPTLegs()[count($ptjourney->getPTLegs()) - 1];
        $durationFromWork = $legFromWork->getDuration();
        $massPTJourney->setDurationWalkFromWork($durationFromWork);

        // Distance from Work
        $distanceFromWork = 4000 * $durationFromWork / 3600;
        $massPTJourney->setDistanceWalkFromWork($distanceFromWork);

        // Number of changes
        $massPTJourney->setChangeNumber($ptjourney->getChangeNumber());

        return $massPTJourney;
    }

    /**
     * Check if a MassPTJourney is valid for public transport potential.
     *
     * @var MassPTJourney The potential journey to check
     */
    public function checkValidMassPTJourney(MassPTJourney $massPTJourney): bool
    {
        // Number of connections
        if ($massPTJourney->getChangeNumber() > $this->params['ptMaxConnections']) {
            ++$this->exludedForPtMaxConnections;

            return false;
        }

        // The maximum distance of walk from home to the last step
        if ($massPTJourney->getDistanceWalkFromHome() > $this->params['ptMaxDistanceWalkFromHome']) {
            ++$this->exludedForPtMaxDistanceWalkFromHome;

            return false;
        }

        // The maximum distance of walk to work from the last step
        if ($massPTJourney->getDistanceWalkFromWork() > $this->params['ptMaxDistanceWalkFromWork']) {
            ++$this->exludedForPtMaxDistanceWalkFromWork;

            return false;
        }

        // The maximum duration of PT journey must be < xN the duration in car
        if ($massPTJourney->getDuration() > ($massPTJourney->getMassPerson()->getDuration() * $this->params['ptMaxNbCarDuration'])) {
            ++$this->exludedForPtMaxNbCarDuration;

            return false;
        }

        return true;
    }

    /**
     * Compute the public transport potential of a Mass from a PT Api.
     *
     * @param int $id Id of the Mass
     *
     * @return Mass The mass with the publicTransportPotential property filled
     */
    public function computePublicTransportPotential(int $id): Mass
    {
        $mass = $this->massRepository->find($id);

        if (is_null($mass->getPersons())) {
            throw new MassException(MassException::NO_MASSPERSON);
        }

        // Total person of this Mass
        $persons = $mass->getPersons();

        $computedData = [
            'totalPerson' => count($persons),
            'totalPTSolutions' => 0,
            'totalPTSolutionsExcluded' => [],
            'totalPersonWithValidPTSolution' => 0,
            'PTPotential' => 0,
            'totalTravelDistance' => 0,
            'totalPTDistance' => 0,
            'totalTravelDistanceCO2' => 0,
            'totalTravelDistancePerYear' => 0,
            'totalTravelDistancePerYearCO2' => 0,
            'totalTravelDuration' => 0,
            'totalPTDuration' => 0,
            'totalTravelDurationPerYear' => 0,
            'savedDistanceByCar' => 0,
            'savedDurationByCar' => 0,
            'savedDistanceByCarPerYear' => 0,
            'savedDurationByCarPerYear' => 0,
            'savedCO2' => 0,
            'savedCO2PerYear' => 0,
            'humanReadableSavedDuration' => '',
            'criteria' => [
                'ptMaxConnections' => $this->params['ptMaxConnections'],
                'ptMaxDistanceWalkFromHome' => $this->params['ptMaxDistanceWalkFromHome'],
                'ptMaxDistanceWalkFromWork' => $this->params['ptMaxDistanceWalkFromWork'],
                'ptMaxNbCarDuration' => $this->params['ptMaxNbCarDuration'],
            ],
        ];

        foreach ($persons as $person) {
            // Original travel
            if (count($person->getMassPTJourneys()) > 0) {
                $ptjourneys = $person->getMassPTJourneys();
                $computedData['totalPTSolutions'] += count($person->getMassPTJourneys());

                foreach ($ptjourneys as $ptjourney) {
                    if ($this->checkValidMassPTJourney($ptjourney)) {
                        ++$computedData['totalPersonWithValidPTSolution'];

                        $computedData['totalTravelDistance'] += $person->getDistance();
                        $computedData['totalTravelDuration'] += $person->getDuration();

                        $computedData['totalPTDistance'] += $ptjourney->getDistance();
                        $computedData['totalPTDuration'] += $ptjourney->getDuration();

                        break;
                    }
                }
            }
        }

        // CO2 consumption of original travel
        $computedData['totalTravelDistanceCO2'] = $this->geoTools->getCO2($computedData['totalTravelDistance']);

        // PT Potential
        $computedData['PTPotential'] = $computedData['totalPersonWithValidPTSolution'] / $computedData['totalPerson'] * 100;

        // Co2 saved
        // It's a percentage of the total CO2 of a regular travel by car using the PT Potential.
        // We assume that PT travel consume 0 Co2
        $computedData['savedCO2'] = $computedData['totalTravelDistanceCO2'] * $computedData['PTPotential'];

        // Distance and Duration saved
        // It's a percentage of the total distance and duration of a regular travel by car using the PT Potential.
        // It's the distance and duration that are not made by car.
        $computedData['savedDurationByCar'] = $computedData['totalTravelDuration'] * $computedData['PTPotential'];
        $computedData['savedDistanceByCar'] = $computedData['totalTravelDistance'] * $computedData['PTPotential'];

        // Per year
        $computedData['totalTravelDistancePerYear'] = $computedData['totalTravelDistance'] * Mass::NB_WORKING_DAY;
        $computedData['totalTravelDurationPerYear'] = $computedData['totalTravelDuration'] * Mass::NB_WORKING_DAY;
        $computedData['savedCO2PerYear'] = $computedData['savedCO2'] * Mass::NB_WORKING_DAY;
        $computedData['savedDurationByCarPerYear'] = $computedData['savedDurationByCar'] * Mass::NB_WORKING_DAY;
        $computedData['savedDistanceByCarPerYear'] = $computedData['savedDistanceByCar'] * Mass::NB_WORKING_DAY;

        $computedData['totalTravelDistancePerYearCO2'] = $this->geoTools->getCO2($computedData['totalTravelDistancePerYear']);

        if ($this->params['roundTripCompute']) {
            $computedData['totalTravelDistanceCO2'] *= 2;
            $computedData['savedCO2'] *= 2;
            $computedData['savedDurationByCar'] *= 2;
            $computedData['savedDistanceByCar'] *= 2;
            $computedData['totalTravelDistancePerYear'] *= 2;
            $computedData['totalTravelDurationPerYear'] *= 2;
            $computedData['savedCO2PerYear'] *= 2;
            $computedData['savedDurationByCarPerYear'] *= 2;
            $computedData['savedDistanceByCarPerYear'] *= 2;

            $computedData['roundtripComputed'] = true;
        } else {
            $computedData['roundtripComputed'] = false;
        }

        $computedData['totalPTSolutionsExcluded'] = [
            'forPtMaxConnections' => $this->exludedForPtMaxConnections,
            'forPtMaxDistanceWalkFromHome' => $this->exludedForPtMaxDistanceWalkFromHome,
            'forPtMaxDistanceWalkFromWork' => $this->exludedForPtMaxDistanceWalkFromWork,
            'forPtMaxNbCarDuration' => $this->exludedForPtMaxNbCarDuration,
            'total' => $this->exludedForPtMaxConnections + $this->exludedForPtMaxDistanceWalkFromHome + $this->exludedForPtMaxDistanceWalkFromWork + $this->exludedForPtMaxNbCarDuration,
        ];

        $mass->setPublicTransportPotential($computedData);

        return $mass;
    }
}