feat(field_sales) : calcul de trajet, optimisation, duplication & roadbook PDF (ERP-125)
- RouteEngineInterface (computeMatrix/optimizeOrder/estimateLegDurations) + HaversineRouteEngine V1 (vitesse moyenne parametrable, plus proche voisin)
- TourRouteCalculator : resolution coords, ETA (RG-6.11), exclusion sans coords (RG-6.05), totaux ; optimize = reorder + recompute
- Endpoints API Platform POST /tours/{id}/compute, /optimize, /duplicate (TourDuplicator, RG-6.13) + Processors, security manage
- Feuille de route PDF GET /tours/{id}/roadbook.pdf (Dompdf + Twig) via PdfRendererInterface (Shared), controller priority:1, security view
- TierAddressResolver etendu (coords + location DBAL)
- Tests : HaversineRouteEngine (unit), compute/optimize/duplicate/roadbook (API)
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Module\FieldSales\Domain\Route;
|
||||
|
||||
use App\Module\FieldSales\Domain\Route\RoutePoint;
|
||||
use App\Module\FieldSales\Infrastructure\Route\HaversineRouteEngine;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Test unitaire du moteur de trajet V1 (M6 § 3.4). Couvre :
|
||||
* - la matrice de distances (symetrie, diagonale nulle, ordre de grandeur) ;
|
||||
* - l'ordre « plus proche voisin » depuis le depart (RG : heuristique gratuite) ;
|
||||
* - l'estimation distance/duree d'un segment a partir de la vitesse moyenne.
|
||||
*
|
||||
* Jeu de points aligne sur le meme meridien (longitude constante) : la distance
|
||||
* ne depend alors que de l'ecart de latitude (~111,2 km par degre), ce qui rend
|
||||
* l'ordre du plus proche voisin deterministe et verifiable a la main.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class HaversineRouteEngineTest extends TestCase
|
||||
{
|
||||
/** 1° de latitude ≈ 111,2 km. Vitesse de test ronde pour des durees lisibles. */
|
||||
private const float SPEED_KMH = 60.0;
|
||||
|
||||
private function engine(): HaversineRouteEngine
|
||||
{
|
||||
return new HaversineRouteEngine(self::SPEED_KMH);
|
||||
}
|
||||
|
||||
public function testMatrixIsSymmetricWithZeroDiagonal(): void
|
||||
{
|
||||
$points = [
|
||||
new RoutePoint('a', 47.0, -1.0),
|
||||
new RoutePoint('b', 47.1, -1.0),
|
||||
new RoutePoint('c', 47.3, -1.0),
|
||||
];
|
||||
|
||||
$matrix = $this->engine()->computeMatrix($points);
|
||||
|
||||
foreach ([0, 1, 2] as $i) {
|
||||
self::assertSame(0, $matrix[$i][$i], 'Diagonale nulle (distance d\'un point a lui-meme).');
|
||||
foreach ([0, 1, 2] as $j) {
|
||||
self::assertSame($matrix[$i][$j], $matrix[$j][$i], 'Matrice symetrique.');
|
||||
}
|
||||
}
|
||||
|
||||
// 0,1° de latitude ≈ 11,1 km : on tolere ±300 m sur le modele Haversine.
|
||||
self::assertEqualsWithDelta(11_100, $matrix[0][1], 300, 'Distance a-b ≈ 11,1 km.');
|
||||
}
|
||||
|
||||
public function testOptimizeOrderFromStartIsNearestNeighbour(): void
|
||||
{
|
||||
$start = new RoutePoint('start', 47.0, -1.0);
|
||||
|
||||
// Fournis en desordre : b(47.3) le plus loin, c(47.2), a(47.1) le plus proche.
|
||||
$points = [
|
||||
new RoutePoint('b', 47.3, -1.0),
|
||||
new RoutePoint('c', 47.2, -1.0),
|
||||
new RoutePoint('a', 47.1, -1.0),
|
||||
];
|
||||
|
||||
$ordered = $this->engine()->optimizeOrder($start, $points);
|
||||
|
||||
self::assertSame(['a', 'c', 'b'], array_map(static fn (RoutePoint $p) => $p->ref, $ordered));
|
||||
}
|
||||
|
||||
public function testOptimizeOrderWithoutStartKeepsFirstPointAsDeparture(): void
|
||||
{
|
||||
// Sans depart explicite : le 1er point fourni est le depart (reste en tete).
|
||||
$points = [
|
||||
new RoutePoint('depart', 47.0, -1.0),
|
||||
new RoutePoint('b', 47.3, -1.0),
|
||||
new RoutePoint('c', 47.2, -1.0),
|
||||
new RoutePoint('a', 47.1, -1.0),
|
||||
];
|
||||
|
||||
$ordered = $this->engine()->optimizeOrder(null, $points);
|
||||
|
||||
self::assertSame(['depart', 'a', 'c', 'b'], array_map(static fn (RoutePoint $p) => $p->ref, $ordered));
|
||||
}
|
||||
|
||||
public function testLegDurationsUseAverageSpeed(): void
|
||||
{
|
||||
$start = new RoutePoint('start', 47.0, -1.0);
|
||||
$points = [
|
||||
new RoutePoint('a', 47.1, -1.0), // ~11,1 km du depart
|
||||
new RoutePoint('b', 47.2, -1.0), // ~11,1 km de a
|
||||
];
|
||||
|
||||
$legs = $this->engine()->estimateLegDurations($start, $points);
|
||||
|
||||
self::assertCount(2, $legs);
|
||||
// 11,1 km a 60 km/h = 0,185 h ≈ 666 s. Tolerance large (modele Haversine).
|
||||
self::assertEqualsWithDelta(666, $legs[0]->durationSeconds, 30);
|
||||
self::assertEqualsWithDelta(11_100, $legs[0]->distanceMeters, 300);
|
||||
}
|
||||
|
||||
public function testLegDurationsWithoutStartFirstLegIsZero(): void
|
||||
{
|
||||
// Sans depart, le 1er point EST le depart -> 1er segment nul.
|
||||
$points = [
|
||||
new RoutePoint('depart', 47.0, -1.0),
|
||||
new RoutePoint('a', 47.1, -1.0),
|
||||
];
|
||||
|
||||
$legs = $this->engine()->estimateLegDurations(null, $points);
|
||||
|
||||
self::assertCount(2, $legs);
|
||||
self::assertSame(0, $legs[0]->distanceMeters, 'Aucun trajet pour atteindre le point de depart.');
|
||||
self::assertSame(0, $legs[0]->durationSeconds);
|
||||
self::assertEqualsWithDelta(11_100, $legs[1]->distanceMeters, 300);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user