diff --git a/server/src/Support/Utils.php b/server/src/Support/Utils.php index 5d5d5ea86..8671742e1 100644 --- a/server/src/Support/Utils.php +++ b/server/src/Support/Utils.php @@ -271,12 +271,20 @@ public static function getPointFromMixed($coordinates): ?Point try { $coordinates = Point::fromJson($coordinatesJson); } catch (\Throwable $e) { + // Both fallbacks carry a GeoJSON coordinate value, so they are + // read as GeoJSON first: pointFromGeoJson() maps [lng, lat], + // whereas recursing hands the pair to the positional array + // reader below, which takes index 0 as the latitude. Nested + // values (a Polygon or LineString ring rather than a single + // pair) yield null there and still fall through to it. if ($coordinatesInGeoJson) { - return static::getPointFromMixed($coordinatesInGeoJson); + return static::pointFromGeoJsonCoordinates($coordinatesInGeoJson) + ?? static::getPointFromMixed($coordinatesInGeoJson); } if ($coordinatesInGeoJsonFeature) { - return static::getPointFromMixed($coordinatesInGeoJsonFeature); + return static::pointFromGeoJsonCoordinates($coordinatesInGeoJsonFeature) + ?? static::getPointFromMixed($coordinatesInGeoJsonFeature); } } } @@ -701,6 +709,19 @@ protected static function pointFromGeoJson($coordinates): ?Point return new Point((float) $latitude, (float) $longitude); } + /** + * Resolve a bare GeoJSON `coordinates` value — an `[longitude, latitude]` + * pair — into a Point, without the surrounding envelope. + * + * Returns null for anything that is not a usable pair, including the nested + * rings a Polygon or LineString carries, so callers can fall through to + * their own handling. + */ + protected static function pointFromGeoJsonCoordinates($coordinates): ?Point + { + return static::pointFromGeoJson(['type' => 'Point', 'coordinates' => $coordinates]); + } + /** * Converts a point to a WKT (Well-Known Text) representation for SQL insert. * diff --git a/server/tests/PointResolutionTest.php b/server/tests/PointResolutionTest.php index 63564d20c..83346c834 100644 --- a/server/tests/PointResolutionTest.php +++ b/server/tests/PointResolutionTest.php @@ -53,13 +53,44 @@ function fleetopsBboxPoint(): array ], ]); - // NOTE: this fallback recurses with the bare coordinate pair, which the - // array reader interprets positionally as [lat, lng] — the reverse of - // GeoJSON's [lng, lat]. The pair therefore comes back transposed. This - // asserts the behaviour as it stands rather than the intent; changing it - // affects every location write path and belongs in its own change. - expect($point->getLat())->toBe(103.851) - ->and($point->getLng())->toBe(1.2816); + // The nested pair is GeoJSON, so it is read as [lng, lat] + expect($point->getLat())->toBe(1.2816) + ->and($point->getLng())->toBe(103.851); +}); + +test('point resolution falls back to top level coordinates before nested geometry', function () { + // The top-level `coordinates` arm is preferred, and is read in the same + // GeoJSON order. Point::fromJson rejects the envelope because of the extra + // member, which is what pushes resolution into the fallback at all. + $point = Utils::getPointFromMixed([ + 'type' => 'Point', + 'coordinates' => [103.851, 1.2816], + 'geometry' => [ + 'type' => 'Point', + 'coordinates' => [1.0, 2.0], + ], + ]); + + expect($point->getLat())->toBe(1.2816) + ->and($point->getLng())->toBe(103.851); +}); + +test('point resolution still hands nested rings to the positional reader', function () { + // A Polygon ring is not a coordinate pair, so the GeoJSON read declines and + // resolution falls through to the existing recursion unchanged. + $point = Utils::getPointFromMixed([ + 'type' => 'Polygon', + 'coordinates' => [ + [ + [103.0, 1.0], + [104.0, 1.0], + [104.0, 2.0], + [103.0, 1.0], + ], + ], + ]); + + expect($point)->toBeInstanceOf(Fleetbase\LaravelMysqlSpatial\Types\Point::class); }); test('coordinate helper does not collapse bbox GeoJSON point to zero', function () {