Skip to content
Merged
67 changes: 67 additions & 0 deletions migrations/Version20260721135509.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php

declare(strict_types=1);

namespace DoctrineMigrations;

use App\Domain\Activity\Stream\StreamType;
use App\Infrastructure\Serialization\Json;
use App\Infrastructure\ValueObject\Geography\EncodedPolyline;
use App\Infrastructure\ValueObject\Geography\Polyline;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

final class Version20260721135509 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}

public function up(Schema $schema): void
{
$results = $this->connection->fetchAllAssociative(
'SELECT Activity.activityId, Activity.polyline, ActivityStream.data AS latLngStream
FROM Activity
INNER JOIN ActivityStream ON ActivityStream.activityId = Activity.activityId
AND ActivityStream.streamType = :streamType
WHERE Activity.polyline IS NOT NULL AND Activity.polyline != ""',
['streamType' => StreamType::LAT_LNG->value]
);

foreach ($results as $result) {
if (count(EncodedPolyline::fromString($result['polyline'])->decodeAndPairLatLng()) >= 10) {
// Polyline was not collapsed, nothing to repair.
continue;
}

try {
$latLngStream = Json::uncompressAndDecode($result['latLngStream']);
} catch (\JsonException) {
continue;
}

/** @var array<int, array{float, float}> $coordinates */
$coordinates = array_values(array_filter(
is_array($latLngStream) ? $latLngStream : [],
is_array(...),
));
if (count($coordinates) < 10) {
// The polyline is legitimately this small.
continue;
}

$this->addSql(
'UPDATE Activity SET polyline = :polyline WHERE activityId = :activityId',
[
'polyline' => (string) Polyline::fromCoordinates($coordinates)->simplify()->encode(),
'activityId' => $result['activityId'],
]
);
}
}

public function down(Schema $schema): void
{
}
}
8 changes: 7 additions & 1 deletion src/Domain/Activity/Route/RouteGeographyAnalyzer.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use App\Infrastructure\Serialization\Json;
use App\Infrastructure\ValueObject\Geography\EncodedPolyline;
use App\Infrastructure\ValueObject\Geography\Polyline;
use Brick\Geo\Engine\GeosOpEngine;
use Brick\Geo\Exception\InvalidGeometryException;
use Brick\Geo\Geometry;
Expand Down Expand Up @@ -57,9 +58,14 @@ public function analyzeForPolyline(EncodedPolyline $polyline): array
{
$passedCountries = [];
try {
$coordinates = $polyline->decodeAndPairLatLng();
$simplifiedPolyline = Polyline::fromCoordinates($coordinates)
->simplify(0.005)
->encode();

$routeLineString = $this->reader->read(Json::encode([
'type' => 'LineString',
'coordinates' => $polyline->decodeAndPairLngLat(),
'coordinates' => $simplifiedPolyline->decodeAndPairLngLat(),
]));
} catch (InvalidGeometryException) {
// Given polyline is somehow not a valid LineString.
Expand Down
4 changes: 2 additions & 2 deletions src/Infrastructure/ValueObject/Geography/EncodedPolyline.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ public function decode(): array
}

/**
* @return array<int, array<float, float>>
* @return array<int, array{float, float}>
*/
public function decodeAndPairLngLat(): array
{
Expand All @@ -75,7 +75,7 @@ public function decodeAndPairLngLat(): array
}

/**
* @return array<int, array<float, float>>
* @return array<int, array{float, float}>
*/
public function decodeAndPairLatLng(): array
{
Expand Down
6 changes: 5 additions & 1 deletion src/Infrastructure/ValueObject/Geography/Polyline.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@ public static function fromCoordinates(array $coordinates): self
return new self($coordinates);
}

public function simplify(float $tolerance = 0.4): self
/**
* The tolerance is expressed in degrees, the same unit as the coordinates
* (0.0001° ≈ 11m on the ground).
*/
public function simplify(float $tolerance = 0.0001): self
{
$points = $this->coordinates;

Expand Down
58 changes: 39 additions & 19 deletions tests/Infrastructure/ValueObject/Geography/PolylineTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,19 @@ public function testSimplifyRemovesPointsOnStraightLine(): void
{
$coordinates = [
[0.0, 0.0],
[1.0, 1.0],
[2.0, 2.0],
[3.0, 3.0],
[0.5, 0.5],
[1.0, 1.02],
[1.5, 1.5],
[2.2, 2.2],
[2.95, 3.0],
[3.5, 3.5],
[4.0, 4.0],
];

self::assertSame(
(string) EncodedPolyline::fromCoordinates([
[0.0, 0.0],
[3.0, 3.0],
[4.0, 4.0],
]),
(string) Polyline::fromCoordinates($coordinates)->simplify(0.1)->encode(),
);
Expand All @@ -43,33 +47,49 @@ public function testSimplifyKeepsCorner(): void
{
$coordinates = [
[0.0, 0.0],
[0.5, 0.0],
[1.0, 0.02],
[1.0, 0.0],
[1.0, 0.5],
[1.02, 0.7],
[1.0, 1.0],
[1.5, 1.0],
[2.0, 1.0],
];

self::assertSame(
(string) EncodedPolyline::fromCoordinates($coordinates),
(string) EncodedPolyline::fromCoordinates([
[0.0, 0.0],
[1.0, 0.0],
[1.0, 1.0],
[2.0, 1.0],
]),
(string) Polyline::fromCoordinates($coordinates)->simplify(0.1)->encode(),
);
}

public function testSimplifyWithLargeToleranceKeepsOnlyEndpoints(): void
public function testSimplifyWithDefaultToleranceKeepsGpsScaleRoute(): void
{
$coordinates = [
[0.0, 0.0],
[1.0, 0.0],
[1.0, 1.0],
[2.0, 1.0],
];
$centerLat = 51.2194;
$centerLng = 4.4025;
$radius = 0.03;
$numberOfPoints = 200;

self::assertSame(
(string) EncodedPolyline::fromCoordinates([
[0.0, 0.0],
[2.0, 1.0],
]),
(string) Polyline::fromCoordinates($coordinates)->simplify(10)->encode(),
);
$coordinates = [];
for ($i = 0; $i < $numberOfPoints; ++$i) {
$angle = 2 * M_PI * $i / $numberOfPoints;
$coordinates[] = [
$centerLat + $radius * sin($angle),
$centerLng + $radius * cos($angle),
];
}

$simplified = Polyline::fromCoordinates($coordinates)->simplify();

$decodedPoints = EncodedPolyline::fromString((string) $simplified->encode())->decodeAndPairLatLng();

self::assertGreaterThan(40, count($decodedPoints));
self::assertLessThan(150, count($decodedPoints));
}

public function testEncodeReturnsEncodedPolyline(): void
Expand Down
Loading