diff --git a/migrations/Version20260721135509.php b/migrations/Version20260721135509.php new file mode 100644 index 000000000..d8c9cab0b --- /dev/null +++ b/migrations/Version20260721135509.php @@ -0,0 +1,67 @@ +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 $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 + { + } +} diff --git a/src/Domain/Activity/Route/RouteGeographyAnalyzer.php b/src/Domain/Activity/Route/RouteGeographyAnalyzer.php index 837257f37..3d180e439 100644 --- a/src/Domain/Activity/Route/RouteGeographyAnalyzer.php +++ b/src/Domain/Activity/Route/RouteGeographyAnalyzer.php @@ -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; @@ -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. diff --git a/src/Infrastructure/ValueObject/Geography/EncodedPolyline.php b/src/Infrastructure/ValueObject/Geography/EncodedPolyline.php index 64a4557e6..169f5dfc1 100644 --- a/src/Infrastructure/ValueObject/Geography/EncodedPolyline.php +++ b/src/Infrastructure/ValueObject/Geography/EncodedPolyline.php @@ -64,7 +64,7 @@ public function decode(): array } /** - * @return array> + * @return array */ public function decodeAndPairLngLat(): array { @@ -75,7 +75,7 @@ public function decodeAndPairLngLat(): array } /** - * @return array> + * @return array */ public function decodeAndPairLatLng(): array { diff --git a/src/Infrastructure/ValueObject/Geography/Polyline.php b/src/Infrastructure/ValueObject/Geography/Polyline.php index c419d0e6a..c52c882cb 100644 --- a/src/Infrastructure/ValueObject/Geography/Polyline.php +++ b/src/Infrastructure/ValueObject/Geography/Polyline.php @@ -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; diff --git a/tests/Infrastructure/ValueObject/Geography/PolylineTest.php b/tests/Infrastructure/ValueObject/Geography/PolylineTest.php index b1f1909ee..e2aba9168 100644 --- a/tests/Infrastructure/ValueObject/Geography/PolylineTest.php +++ b/tests/Infrastructure/ValueObject/Geography/PolylineTest.php @@ -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(), ); @@ -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