Covivo/mobicoop

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

Summary

Maintainability
D
1 day
Test Coverage
<?php

/**
 * Copyright (c) 2018, 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\Carpool\Service\ProposalMatcher;
use App\Geography\Interfaces\GeorouterInterface;
use App\Geography\Service\GeoRouter;
use App\Geography\Service\ZoneManager;
use App\Match\Entity\Candidate;
use Psr\Log\LoggerInterface;

/**
 * Geographical Matching service.
 *
 * @author Sylvain Briat <sylvain.briat@covivo.eu>
 */
class GeoMatcher
{
    private $geoRouter;
    private $zoneManager;
    private $logger;

    /**
     * Constructor.
     */
    public function __construct(GeoRouter $geoRouter, ZoneManager $zoneManager, LoggerInterface $logger)
    {
        $this->geoRouter = $geoRouter;
        $this->zoneManager = $zoneManager;
        $this->logger = $logger;
    }

    /**
     * Search for matchings between a candidate and multiple candidates.
     *
     * @param array $candidates An associative array representing the matches to test :
     *                          [
     *                          'candidate'     => Candidate        // the proposal candidate
     *                          'candidates'    => Candidates[0]    // the array of candidates to match with the proposal candidate
     *                          'master'        => bool             // if the candidate proposal is the master => we have to use its max detour (= driver !)
     *                          ]
     *
     * @return array The array of results
     */
    public function singleMatch(array $candidates): ?array
    {
        $matchesReturned = [];
        // we create the points for the routes alternatives for each candidate
        $addressesForRoutes = [];
        $variants = [];
        $i = 0;

        foreach ($candidates as $pears) {
            foreach ($pears['candidates'] as $candidate) {
                $role = 'driver';
                if ($pears['master']) {
                    $pointsArray = $this->generatePointsArray($pears['candidate'], $candidate);
                } else {
                    $pointsArray = $this->generatePointsArray($candidate, $pears['candidate']);
                    $role = 'passenger';
                }
                foreach ($pointsArray as $variant) {
                    $variants[$i] = [
                        'candidate' => $pears['candidate'],
                        'candidateToMatch' => $candidate,
                        'variantPoints' => $variant,
                        'role' => $role,
                    ];
                    $addressesForRoutes[$i][] = $variant;
                    ++$i;
                }
            }
        }

        // we get the routes for all candidates
        $this->logger->info('GeoMatcher : start multipleAsync '.(new \DateTime('UTC'))->format('Ymd H:i:s.u'));
        $ownerRoutes = $this->geoRouter->getMultipleAsyncRoutes($addressesForRoutes, true, false, GeorouterInterface::RETURN_TYPE_ARRAY);
        $this->logger->info('GeoMatcher : end multipleAsync '.(new \DateTime('UTC'))->format('Ymd H:i:s.u'));

        // we treat the routes to check if they match
        foreach ($ownerRoutes as $ownerId => $routes) {
            if ('driver' == $variants[$ownerId]['role']) {
                if ($match = $this->checkMatch(
                    $variants[$ownerId]['candidate'],
                    $variants[$ownerId]['candidateToMatch'],
                    $routes,
                    $variants[$ownerId]['variantPoints']
                )) {
                    $matchesReturned['driver'][$variants[$ownerId]['candidateToMatch']->getId()][] = $match;
                }
            } elseif ($match = $this->checkMatch(
                $variants[$ownerId]['candidateToMatch'],
                $variants[$ownerId]['candidate'],
                $routes,
                $variants[$ownerId]['variantPoints']
            )) {
                $matchesReturned['passenger'][$variants[$ownerId]['candidateToMatch']->getId()][] = $match;
            }
        }
        $this->logger->info('GeoMatcher : return matches '.(new \DateTime('UTC'))->format('Ymd H:i:s.u'));

        return $matchesReturned;
    }

    /**
     * Search for matchings between candidates.
     *
     * @param array $candidates The array of candidates to match in the form :
     *                          [
     *                          0 => [
     *                          'driver'      => $driver,
     *                          'passengers'  => [$passenger1,$passenger2...]
     *                          ],
     *                          1 => [
     *                          'driver'      => $driver,
     *                          'passengers'  => [$passenger1,$passenger2...]
     *                          ],
     *                          ...
     *                          ]
     * @param bool  $forMass    The multimatch is for mass matching
     *
     * @return null|array The array of results
     */
    public function multiMatch(array $candidates, $forMass = false): ?array
    {
        $matchesReturned = [];

        // we create the points for the routes alternatives for each candidate
        $addressesForRoutes = [];
        $variants = [];
        $routesOwner = [];
        $i = 0;

        if (!$forMass) {
            foreach ($candidates as $keyActors => $actors) {
                foreach ($actors['passengers'] as $keyPassenger => $candidateToMatch) {
                    if ($pointsArray = $this->generatePointsArray($actors['driver'], $candidateToMatch)) {
                        foreach ($pointsArray as $variant) {
                            $variants[$i] = [
                                'candidate' => $actors['driver'],
                                'candidateToMatch' => $candidateToMatch,
                                'variantPoints' => $variant,
                            ];
                            $addressesForRoutes[$i][] = $variant;
                            ++$i;
                        }
                    }
                }
            }

            $ownerRoutes = $this->geoRouter->getMultipleAsyncRoutes($addressesForRoutes, true, false, GeorouterInterface::RETURN_TYPE_ARRAY);

            // we treat the routes to check if they match
            foreach ($ownerRoutes as $ownerId => $routes) {
                if ($match = $this->checkMatch(
                    $variants[$ownerId]['candidate'],
                    $variants[$ownerId]['candidateToMatch'],
                    $routes,
                    $variants[$ownerId]['variantPoints']
                )) {
                    // the following means : $matchesReturned[driverId][passengerId][] = $match;
                    $matchesReturned[$variants[$ownerId]['candidate']->getId()][$variants[$ownerId]['candidateToMatch']->getId()][] = $match;
                }

                //$this->logger->debug('Multi Match | Check matches for id #'.$ownerId);
                // if ($match = $this->checkMatch(
                //     $candidates[$routesOwner[$ownerId]['actors']]['driver'],
                //     $candidates[$routesOwner[$ownerId]['actors']]['passengers'][$routesOwner[$ownerId]['passenger']],
                //     $routes,
                //     $addressesForRoutes[$ownerId]
                // )) {
                //     $matchesReturned[] = [
                //             'driver' => $candidates[$routesOwner[$ownerId]['actors']]['driver'],
                //             'passenger' => $candidates[$routesOwner[$ownerId]['actors']]['passengers'][$routesOwner[$ownerId]['passenger']],
                //             'match' => $match
                //         ];
                // }
            }
        } else {
            $this->logger->debug('Multi Match | Check matches');
            foreach ($candidates as $keyActors => $actors) {
                foreach ($actors['passengers'] as $keyPassenger => $candidateToMatch) {
                    if ($pointsArray = $this->generatePointsArray($actors['driver'], $candidateToMatch)) {
                        $addressesForRoutes[$i] = $pointsArray;
                        $routesOwner[$i] = [
                            'actors' => $keyActors,
                            'passenger' => $keyPassenger,
                        ];
                        ++$i;
                    }
                }
            }

            $ownerRoutes = $this->geoRouter->getMultipleAsyncRoutes($addressesForRoutes, false, false, GeorouterInterface::RETURN_TYPE_ARRAY);
            $this->logger->debug('Multi Match | End multiple async routes for mass');
            // we treat the routes to check if they match
            foreach ($ownerRoutes as $ownerId => $routes) {
                if ($matches = $this->checkMassMultiMatch(
                    $candidates[$routesOwner[$ownerId]['actors']]['driver'],
                    $candidates[$routesOwner[$ownerId]['actors']]['passengers'][$routesOwner[$ownerId]['passenger']],
                    $routes,
                    $addressesForRoutes[$ownerId]
                )) {
                    $matchesReturned[] = [
                        'driver' => $candidates[$routesOwner[$ownerId]['actors']]['driver'],
                        'passenger' => $candidates[$routesOwner[$ownerId]['actors']]['passengers'][$routesOwner[$ownerId]['passenger']],
                        'matches' => $matches,
                    ];
                }
            }
            unset($ownerRoutes);
        }

        return $matchesReturned;
    }

    /**
     * Force the match between 2 candidates.
     * Used for example to compute the reverse route after a successful outward matching.
     */
    public function forceMatch(Candidate $candidate1, Candidate $candidate2): ?array
    {
        $result = [];

        $pointsArray = $this->generatePointsArray($candidate1, $candidate2);

        // for each possible route, we compute its results
        foreach ($pointsArray as $points) {
            if ($routes = $this->geoRouter->getRoutes(array_values($points), true, false, GeorouterInterface::RETURN_TYPE_OBJECT)) {
                if ($match = $this->getMatch($candidate1, $candidate2, $routes, $points)) {
                    $result[] = $match;
                }
            }
        }

        return $result;
    }

    /**
     * Generate waypoints for two candidates.
     * Returns all combinations of the points keeping the overall order of the points.
     */
    public function generatePointsArray(Candidate $candidate1, Candidate $candidate2): ?array
    {
        $pointsArray = [];

        // The first candidate has 2 or more waypoints, the first and last will always be the first and last
        // The second candidate has 2 or more waypoints
        // It's a Travel salesman problem (TSP) : https://en.wikipedia.org/wiki/Travelling_salesman_problem

        // we will write Axx for first candidate points, Bxx for second candidate points

        // for now, we use the routing engine and :
        // - we just try the classic ACDB path if we have only 2 points for the first candidate
        // - or we try the combinations of :
        //   - inner points of the first candidate (=> we exclude the start and end point as they must stay start and end)
        //   - the start and end point of the second candidate
        //   - note : we keep only the combinations that respect the order of the points for the 2 candidates (B0 will always be before B1, A1 will always be before A2 etc...)
        //   - TODO : maybe we should also try to remove one or more inner points of the first candidate and see if the whole route is faster
        //     (it could be the case for example if an AXX inner point and a B0/B1 point are close)

        // TODO : solve the TSP using the optimization engine

        if (2 == count($candidate1->getAddresses())) {
            $pointsArray[] = [
                'A0' => $candidate1->getAddresses()[0],                                       // A
                'B0' => $candidate2->getAddresses()[0],                                       // C
                'B1' => $candidate2->getAddresses()[count($candidate2->getAddresses()) - 1],    // D
                'A1' => $candidate1->getAddresses()[count($candidate1->getAddresses()) - 1],    // B
            ];
        } else {
            // innerAddresses will contain the first candidate addresses without the start and end point
            $innerAddresses = [];
            $addresses1 = $candidate1->getAddresses();
            array_pop($addresses1);     // get rid of the last point
            array_shift($addresses1);   // get rid of the first point
            for ($i = 1; $i <= count($addresses1); ++$i) {
                $innerAddresses['A'.$i] = $addresses1[$i - 1];
            }
            // we add the second candidate start and end point
            $innerAddresses['B0'] = $candidate2->getAddresses()[0];
            $innerAddresses['B1'] = $candidate2->getAddresses()[count($candidate2->getAddresses()) - 1];
            $generator = new CombinationsGenerator();
            foreach ($generator->generate($innerAddresses) as $combination) {
                $address = [];
                $address['A0'] = $candidate1->getAddresses()[0];    // first candidate start
                // we use the following array to check the order of points (eg. if B1 is before B0 we skip)
                $check = [];
                $take = false;
                foreach ($combination as $key => $value) {
                    $take = false;
                    if ('A' == $key[0] && in_array('A'.(substr($key, 1) + 1), $check)) {
                        // 'A' order not respected !
                        break;
                    }
                    if ('B' == $key[0] && in_array('B'.(substr($key, 1) + 1), $check)) {
                        // 'B' order not respected !
                        break;
                    }
                    $take = true;
                    $check[] = $key;
                    $address[$key] = $value;
                }
                if ($take) {
                    $address['A'.(count($candidate1->getAddresses()) - 1)] = $candidate1->getAddresses()[count($candidate1->getAddresses()) - 1]; // first candidate end
                    $pointsArray[] = $address;
                }
            }
        }

        return $pointsArray;
    }

    /**
     * Matching function between 2 candidates.
     * The first candidate handles the matching parameters :
     * - detour distance (in meters) or detour distance percent (of the original distance), the second is only used if the first is null
     * - detour duration (in milliseconds) or detour duration percent (of the original duration), the second is only used if the first is null.
     *
     * @param Candidate $candidate1 The first candidate
     * @param Candidate $candidate2 The second candidate
     *
     * @return null|array The array containing the result or null if candidates don't match
     */
    private function match(Candidate $candidate1, Candidate $candidate2): ?array
    {
        $result = [];

        $pointsArray = $this->generatePointsArray($candidate1, $candidate2);

        // for each possible route, we check if it's acceptable
        foreach ($pointsArray as $points) {
            if ($routes = $this->geoRouter->getRoutes(array_values($points), true)) {
                if ($match = $this->checkMatch($candidate1, $candidate2, $routes, $points)) {
                    $result[] = $match;
                }
            }
        }

        return $result;
    }

    private function checkMatch(Candidate $candidate1, Candidate $candidate2, array $routes, ?array $points): ?array
    {
        $result = null;

        $detourDistance = false;
        $detourDuration = false;
        $commonDistance = false;

        $distance = null;
        $duration = null;

        if (is_array($routes[0])) {
            if (isset($routes[0]['distance'])) {
                $distance = $routes[0]['distance'];
            }
            if (isset($routes[0]['duration'])) {
                $duration = $routes[0]['duration'];
            }
        } else {
            $distance = $routes[0]->getDistance();
            $duration = $routes[0]->getDuration();
        }

        // we check the detour distance
        if ($candidate1->getDirection()) {
            if ($candidate1->getMaxDetourDistance()) {
                // in meters
                if ($distance <= ($candidate1->getDirection()->getDistance() + $candidate1->getMaxDetourDistance())) {
                    $detourDistance = true;
                }
            } elseif ($candidate1->getMaxDetourDistancePercent()) {
                // in percentage
                if ($distance <= (($candidate1->getDirection()->getDistance() * ($candidate1->getMaxDetourDistancePercent() / 100)) + $candidate1->getDirection()->getDistance())) {
                    $detourDistance = true;
                }
            }
        } else {
            if ($candidate1->getMaxDetourDistance()) {
                // in meters
                if ($distance <= ($candidate1->getDistance() + $candidate1->getMaxDetourDistance())) {
                    $detourDistance = true;
                }
            } elseif ($candidate1->getMaxDetourDistancePercent()) {
                // in percentage
                if ($distance <= (($candidate1->getDistance() * ($candidate1->getMaxDetourDistancePercent() / 100)) + $candidate1->getDistance())) {
                    $detourDistance = true;
                }
            }
        }

        // we check the detour duration
        if ($candidate1->getDirection()) {
            if ($candidate1->getMaxDetourDuration()) {
                // in seconds
                if ($duration <= ($candidate1->getDirection()->getDuration() + $candidate1->getMaxDetourDuration())) {
                    $detourDuration = true;
                }
            } elseif ($candidate1->getMaxDetourDurationPercent()) {
                // in percentage
                if ($duration <= (($candidate1->getDirection()->getDuration() * ($candidate1->getMaxDetourDurationPercent() / 100)) + $candidate1->getDirection()->getDuration())) {
                    $detourDuration = true;
                }
            }
        } else {
            if ($candidate1->getMaxDetourDuration()) {
                // in seconds
                if ($duration <= ($candidate1->getDuration() + $candidate1->getMaxDetourDuration())) {
                    $detourDuration = true;
                }
            } elseif ($candidate1->getMaxDetourDurationPercent()) {
                // in percentage
                if ($duration <= (($candidate1->getDuration() * ($candidate1->getMaxDetourDurationPercent() / 100)) + $candidate1->getDuration())) {
                    $detourDuration = true;
                }
            }
        }

        // we check the common distance (if the distance is not 0, that can happen for solidary proposals without destination)
        if ($candidate2->getDirection() && $candidate2->getDirection()->getDistance() > 0) {
            if ($candidate1->getDirection()) {
                if (($candidate1->getDirection()->getDistance() < ProposalMatcher::getMinCommonDistanceCheck())
                    || (($candidate2->getDirection()->getDistance() * 100 / $candidate1->getDirection()->getDistance()) > ProposalMatcher::getMinCommonDistancePercent())) {
                    $commonDistance = true;
                }
            } else {
                if (($candidate1->getDistance() < ProposalMatcher::getMinCommonDistanceCheck())
                    || (($candidate2->getDirection()->getDistance() * 100 / $candidate1->getDistance()) > ProposalMatcher::getMinCommonDistancePercent())) {
                    $commonDistance = true;
                }
            }
        } elseif ($candidate2->getDistance() > 0) {
            if ($candidate1->getDirection()) {
                if (($candidate1->getDirection()->getDistance() < ProposalMatcher::getMinCommonDistanceCheck())
                    || (($candidate2->getDistance() * 100 / $candidate1->getDirection()->getDistance()) > ProposalMatcher::getMinCommonDistancePercent())) {
                    $commonDistance = true;
                }
            } else {
                if (($candidate1->getDistance() < ProposalMatcher::getMinCommonDistanceCheck())
                    || (($candidate2->getDistance() * 100 / $candidate1->getDistance()) > ProposalMatcher::getMinCommonDistancePercent())) {
                    $commonDistance = true;
                }
            }
        } else {
            $commonDistance = true;
        }
        // if the detour is acceptable we keep the candidate
        if ($detourDistance && $detourDuration && $commonDistance) {
            // we deserialize the direction if needed
            $direction = $routes[0];
            if (is_array($routes[0])) {
                $direction = $this->geoRouter->getRouter()->deserializeDirection($routes[0]);
            }

            $result = [
                'route' => is_array($points) ? $this->generateRoute($points, $direction->getDurations()) : null,
                'originalDistance' => $candidate1->getDirection() ? $candidate1->getDirection()->getDistance() : $candidate1->getDistance(),
                'acceptedDetourDistance' => $candidate1->getMaxDetourDistance(),
                'newDistance' => $distance,
                'detourDistance' => $candidate1->getDirection() ? ($distance - $candidate1->getDirection()->getDistance()) : ($distance - $candidate1->getDistance()),
                'detourDistancePercent' => $candidate1->getDirection() ? round($distance * 100 / $candidate1->getDirection()->getDistance() - 100, 2) : round($distance * 100 / $candidate1->getDistance() - 100, 2),
                'originalDuration' => $candidate1->getDirection() ? $candidate1->getDirection()->getDuration() : $candidate1->getDuration(),
                'acceptedDetourDuration' => $candidate1->getMaxDetourDuration(),
                'newDuration' => $duration,
                'detourDuration' => $candidate1->getDirection() ? ($duration - $candidate1->getDirection()->getDuration()) : ($duration - $candidate1->getDuration()),
                'detourDurationPercent' => $candidate1->getDirection() ? round($duration * 100 / $candidate1->getDirection()->getDuration() - 100, 2) : round($duration * 100 / $candidate1->getDuration() - 100, 2),
                'commonDistance' => $candidate2->getDirection() ? $candidate2->getDirection()->getDistance() : $candidate2->getDistance(),
                'candidate1' => $candidate1->getId(),
                'candidate2' => $candidate2->getId(),
            ];
        }

        return $result;
    }

    private function checkMultiMatch(Candidate $candidate1, Candidate $candidate2, array $routes, ?array $points): ?array
    {
        $result = null;

        $detourDistance = false;
        $detourDuration = false;
        $commonDistance = false;

        // we check the detour distance
        if ($candidate1->getMaxDetourDistance()) {
            // in meters
            if ($routes[0]->getDistance() <= ($candidate1->getDirection()->getDistance() + $candidate1->getMaxDetourDistance())) {
                $detourDistance = true;
            }
        } elseif ($candidate1->getMaxDetourDistancePercent()) {
            // in percentage
            if ($routes[0]->getDistance() <= (($candidate1->getDirection()->getDistance() * ($candidate1->getMaxDetourDistancePercent() / 100)) + $candidate1->getDirection()->getDistance())) {
                $detourDistance = true;
            }
        }
        // we check the detour duration
        if ($candidate1->getMaxDetourDuration()) {
            // in seconds
            if ($routes[0]->getDuration() <= ($candidate1->getDirection()->getDuration() + $candidate1->getMaxDetourDuration())) {
                $detourDuration = true;
            }
        } elseif ($candidate1->getMaxDetourDurationPercent()) {
            // in percentage
            if ($routes[0]->getDuration() <= (($candidate1->getDirection()->getDuration() * ($candidate1->getMaxDetourDurationPercent() / 100)) + $candidate1->getDirection()->getDuration())) {
                $detourDuration = true;
            }
        }
        // we check the common distance
        if ($candidate2->getDirection()) {
            if (($candidate1->getDirection()->getDistance() < ProposalMatcher::getMinCommonDistanceCheck())
                || (($candidate2->getDirection()->getDistance() * 100 / $candidate1->getDirection()->getDistance()) > ProposalMatcher::getMinCommonDistancePercent())) {
                $commonDistance = true;
            }
        } else {
            if (($candidate1->getDirection()->getDistance() < ProposalMatcher::getMinCommonDistanceCheck())
                || (($candidate2->getDistance() * 100 / $candidate1->getDirection()->getDistance()) > ProposalMatcher::getMinCommonDistancePercent())) {
                $commonDistance = true;
            }
        }

        // if the detour is acceptable we keep the candidate
        if ($detourDistance && $detourDuration && $commonDistance) {
            $result[] = [
                'route' => is_array($points[0]) ? $this->generateRoute($points[0], null) : null,
                'originalDistance' => $candidate1->getDirection()->getDistance(),
                'acceptedDetourDistance' => $candidate1->getMaxDetourDistance(),
                'newDistance' => $routes[0]->getDistance(),
                'detourDistance' => ($routes[0]->getDistance() - $candidate1->getDirection()->getDistance()),
                'detourDistancePercent' => round($routes[0]->getDistance() * 100 / $candidate1->getDirection()->getDistance() - 100, 2),
                'originalDuration' => $candidate1->getDirection()->getDuration(),
                'acceptedDetourDuration' => $candidate1->getMaxDetourDuration(),
                'newDuration' => $routes[0]->getDuration(),
                'detourDuration' => ($routes[0]->getDuration() - $candidate1->getDirection()->getDuration()),
                'detourDurationPercent' => round($routes[0]->getDuration() * 100 / $candidate1->getDirection()->getDuration() - 100, 2),
                'commonDistance' => $candidate2->getDirection() ? $candidate2->getDirection()->getDistance() : $candidate2->getDistance(),
                'direction' => $routes[0],
                'id' => $candidate2->getId(),
            ];
        }

        return $result;
    }

    private function checkMassMultiMatch(Candidate $candidate1, Candidate $candidate2, array $routes, ?array $points): ?array
    {
        $result = null;

        $detourDistance = false;
        $detourDuration = false;
        $commonDistance = false;

        // we check the detour distance
        if ($candidate1->getMaxDetourDistance()) {
            // in meters
            if ($routes[0]['distance'] <= ($candidate1->getMassPerson()->getDistance() + $candidate1->getMaxDetourDistance())) {
                $detourDistance = true;
            }
        } elseif ($candidate1->getMaxDetourDistancePercent()) {
            // in percentage
            if ($routes[0]['distance'] <= (($candidate1->getMassPerson()->getDistance() * ($candidate1->getMaxDetourDistancePercent() / 100)) + $candidate1->getMassPerson()->getDistance())) {
                $detourDistance = true;
            }
        }
        // we check the detour duration
        if ($candidate1->getMaxDetourDuration()) {
            // in seconds
            if ($routes[0]['duration'] <= ($candidate1->getMassPerson()->getDuration() + $candidate1->getMaxDetourDuration())) {
                $detourDuration = true;
            }
        } elseif ($candidate1->getMaxDetourDurationPercent()) {
            // in percentage
            if ($routes[0]['duration'] <= (($candidate1->getMassPerson()->getDuration() * ($candidate1->getMaxDetourDurationPercent() / 100)) + $candidate1->getMassPerson()->getDuration())) {
                $detourDuration = true;
            }
        }
        // we check the common distance
        if (($candidate1->getMassPerson()->getDistance() < ProposalMatcher::getMinCommonDistanceCheck())
            || (($candidate2->getMassPerson()->getDistance() * 100 / $candidate1->getMassPerson()->getDistance()) > ProposalMatcher::getMinCommonDistancePercent())) {
            $commonDistance = true;
        }

        // if the detour is acceptable we keep the candidate
        if ($detourDistance && $detourDuration && $commonDistance) {
            $result[] = [
                'route' => is_array($points) ? $this->generateRoute($points, null) : null,
                'originalDistance' => $candidate1->getMassPerson()->getDistance(),
                'acceptedDetourDistance' => $candidate1->getMaxDetourDistance(),
                'newDistance' => $routes[0]['distance'],
                'detourDistance' => ($routes[0]['distance'] - $candidate1->getMassPerson()->getDistance()),
                'detourDistancePercent' => round($routes[0]['distance'] * 100 / $candidate1->getMassPerson()->getDistance() - 100, 2),
                'originalDuration' => $candidate1->getMassPerson()->getDuration(),
                'acceptedDetourDuration' => $candidate1->getMaxDetourDuration(),
                'newDuration' => $routes[0]['duration'],
                'detourDuration' => ($routes[0]['duration'] - $candidate1->getMassPerson()->getDuration()),
                'detourDurationPercent' => round($routes[0]['duration'] * 100 / $candidate1->getMassPerson()->getDuration() - 100, 2),
                'commonDistance' => $candidate2->getMassPerson()->getDistance(),
                'id' => $candidate2->getId(),
            ];
        }

        return $result;
    }

    private function getMatch(Candidate $candidate1, Candidate $candidate2, array $routes, ?array $points): ?array
    {
        $result = null;

        // we add the zones to the direction
        // $direction = $this->zoneManager->createZonesForDirection($routes[0]);
        $direction = $routes[0];

        // /!\ detour can be empty has we "force" the match, all directions may not be computed /!\
        return [
            'route' => is_array($points) ? $this->generateRoute($points, $routes[0]->getDurations()) : null,
            'originalDistance' => !is_null($candidate1->getDirection()) ? $candidate1->getDirection()->getDistance() : null,
            'acceptedDetourDistance' => $candidate1->getMaxDetourDistance(),
            'newDistance' => $routes[0]->getDistance(),
            'detourDistance' => !is_null($candidate1->getDirection()) ? ($routes[0]->getDistance() - $candidate1->getDirection()->getDistance()) : null,
            'detourDistancePercent' => !is_null($candidate1->getDirection()) ? round($routes[0]->getDistance() * 100 / $candidate1->getDirection()->getDistance() - 100, 2) : null,
            'originalDuration' => !is_null($candidate1->getDirection()) ? $candidate1->getDirection()->getDuration() : null,
            'acceptedDetourDuration' => $candidate1->getMaxDetourDuration(),
            'newDuration' => $routes[0]->getDuration(),
            'detourDuration' => !is_null($candidate1->getDirection()) ? ($routes[0]->getDuration() - $candidate1->getDirection()->getDuration()) : null,
            'detourDurationPercent' => !is_null($candidate1->getDirection()) ? round($routes[0]->getDuration() * 100 / $candidate1->getDirection()->getDuration() - 100, 2) : null,
            'commonDistance' => !is_null($candidate2->getDirection()) ? $candidate2->getDirection()->getDistance() : null,
            'direction' => $direction,
            'id' => $candidate2->getId(),
        ];
    }

    /**
     * Returns the route (order of the points).
     *
     * @param array $points    The points in order
     * @param array $durations The duration of each part
     */
    private function generateRoute(array $points, ?array $durations)
    {
        $route = [];
        $i = 0;
        $curDuration = 0;
        foreach ($points as $key => $point) {
            $curDuration = isset($durations[$i]['duration']) ? $durations[$i]['duration'] : $curDuration;
            $route[] = [
                'candidate' => ('A' == substr($key, 0, 1)) ? 1 : 2,
                'position' => substr($key, 1),
                'duration' => isset($durations[$i]) ? $durations[$i]['duration'] : $curDuration,
                'approx_duration' => isset($durations[$i]) ? $durations[$i]['approx_duration'] : null,    // approx_duration : if the duration to the waypoint isn't strictly returned by the SIG
                'approx_point' => isset($durations[$i]) ? $durations[$i]['approx_point'] : null,       // approx_point : if the position of the waypoint isn't strictly returned by the SIG
                'address' => $point,
            ];
            ++$i;
        }

        return $route;
    }
}

// Class used temporarily to generate path combinations
// Will be dumped when optimization engine will work
class CombinationsGenerator
{
    /**
     * Generate combinations for an array.
     */
    public function generate(array $list): \Generator
    {
        if (count($list) > 2) {
            for ($i = 0; $i < count($list); ++$i) {
                $listCopy = $list;

                $entry = array_splice($listCopy, $i, 1);
                foreach ($this->generate($listCopy) as $combination) {
                    yield array_merge($entry, $combination);
                }
            }
        } elseif (count($list) > 0) {
            yield $list;

            if (count($list) > 1) {
                yield array_reverse($list);
            }
        }
    }
}