From ccfce33821fb502a3caba863a6510e88846210c8 Mon Sep 17 00:00:00 2001 From: Riley Schoppa Date: Mon, 27 Feb 2023 20:18:33 -0500 Subject: [PATCH 1/4] Add convex hull query --- API.md | 24 +++++++++++++++++++ src/SpatialBuilder.php | 18 ++------------ src/SpatialQuery.php | 40 +++++++++++++++++++++++++++++++ src/SpatialQueryHelpers.php | 25 +++++++++++++++++++ tests/SpatialBuilderTest.php | 31 ------------------------ tests/SpatialQueryHelpersTest.php | 35 +++++++++++++++++++++++++++ tests/SpatialQueryTest.php | 24 +++++++++++++++++++ 7 files changed, 150 insertions(+), 47 deletions(-) create mode 100644 src/SpatialQuery.php create mode 100644 src/SpatialQueryHelpers.php create mode 100644 tests/SpatialQueryHelpersTest.php create mode 100644 tests/SpatialQueryTest.php diff --git a/API.md b/API.md index 1864eb4..9929394 100644 --- a/API.md +++ b/API.md @@ -441,3 +441,27 @@ Place::query() ->exists(); // true ``` + +## Available Spatial Queries + +### convexHull + +Creates a ConvexHull using the specified geometry using the [ST_ConvexHull](https://dev.mysql.com/doc/refman/8.0/en/spatial-operator-functions.html#function_st-convexhull) function. + +| parameter name | type | +|----------------|---------------------| +| `$geometry` | `Geometry \ string` | + +
Example + +```php +$points = MultiPoint::fromJson('{"type":"MultiPoint","coordinates":[[-1,-1],[1,-1],[1,1],[-1,1],[-1,-1],[0,0]]}'); + +$wkbHull = SpatialQuery::make() + ->convexHull($points) + ->first() + ->convex_hull; + +Polygon::fromWkb($wkbHull) // {"type":"Polygon","coordinates":[[[-1,-1],[1,-1],[1,1],[-1,1],[-1,-1]]]} +``` +
diff --git a/src/SpatialBuilder.php b/src/SpatialBuilder.php index fdbd2a6..c17ae2b 100644 --- a/src/SpatialBuilder.php +++ b/src/SpatialBuilder.php @@ -6,7 +6,6 @@ use Illuminate\Contracts\Database\Query\Expression as ExpressionContract; use Illuminate\Database\Eloquent\Builder; -use Illuminate\Support\Facades\DB; use MatanYadaev\EloquentSpatial\Objects\Geometry; /** @@ -18,6 +17,8 @@ */ class SpatialBuilder extends Builder { + use SpatialQueryHelpers; + public function withDistance( ExpressionContract|Geometry|string $column, ExpressionContract|Geometry|string $geometryOrColumn, @@ -315,19 +316,4 @@ public function whereSrid( return $this; } - - protected function toExpressionString(ExpressionContract|Geometry|string $geometryOrColumnOrExpression): string - { - $grammar = $this->getGrammar(); - - if ($geometryOrColumnOrExpression instanceof ExpressionContract) { - $expression = $geometryOrColumnOrExpression; - } elseif ($geometryOrColumnOrExpression instanceof Geometry) { - $expression = $geometryOrColumnOrExpression->toSqlExpression($this->getConnection()); - } else { - $expression = DB::raw($grammar->wrap($geometryOrColumnOrExpression)); - } - - return (string) $expression->getValue($grammar); - } } diff --git a/src/SpatialQuery.php b/src/SpatialQuery.php new file mode 100644 index 0000000..fbc722a --- /dev/null +++ b/src/SpatialQuery.php @@ -0,0 +1,40 @@ +selectRaw( + sprintf( + 'ST_CONVEXHULL(%s) as convex_hull', + $this->toExpressionString($geometry), + ) + ); + + return $this; + } +} diff --git a/src/SpatialQueryHelpers.php b/src/SpatialQueryHelpers.php new file mode 100644 index 0000000..9baa572 --- /dev/null +++ b/src/SpatialQueryHelpers.php @@ -0,0 +1,25 @@ +getGrammar(); + + if ($geometryOrColumnOrExpression instanceof ExpressionContract) { + $expression = $geometryOrColumnOrExpression; + } elseif ($geometryOrColumnOrExpression instanceof Geometry) { + $expression = $geometryOrColumnOrExpression->toSqlExpression($this->getConnection()); + } else { + $expression = DB::raw($grammar->wrap($geometryOrColumnOrExpression)); + } + + return (string) $expression->getValue($grammar); + } +} diff --git a/tests/SpatialBuilderTest.php b/tests/SpatialBuilderTest.php index af7dfc5..f981e8e 100644 --- a/tests/SpatialBuilderTest.php +++ b/tests/SpatialBuilderTest.php @@ -408,34 +408,3 @@ expect($testPlaceWithDistance)->not()->toBeNull(); }); - -it('toExpressionString can handle a Expression input', function (): void { - $spatialBuilder = TestPlace::query(); - $toExpressionStringMethod = (new ReflectionClass($spatialBuilder))->getMethod('toExpressionString'); - - $result = $toExpressionStringMethod->invoke($spatialBuilder, DB::raw('POINT(longitude, latitude)')); - - expect($result)->toBe('POINT(longitude, latitude)'); -}); - -it('toExpressionString can handle a Geometry input', function (): void { - $spatialBuilder = TestPlace::query(); - $toExpressionStringMethod = (new ReflectionClass($spatialBuilder))->getMethod('toExpressionString'); - $polygon = Polygon::fromJson('{"type":"Polygon","coordinates":[[[-1,-1],[1,-1],[1,1],[-1,1],[-1,-1]]]}'); - - $result = $toExpressionStringMethod->invoke($spatialBuilder, $polygon); - - $grammar = $spatialBuilder->getGrammar(); - $connection = $spatialBuilder->getConnection(); - $sqlSerializedPolygon = $polygon->toSqlExpression($connection)->getValue($grammar); - expect($result)->toBe($sqlSerializedPolygon); -}); - -it('toExpressionString can handle a string input', function (): void { - $spatialBuilder = TestPlace::query(); - $toExpressionStringMethod = (new ReflectionClass($spatialBuilder))->getMethod('toExpressionString'); - - $result = $toExpressionStringMethod->invoke($spatialBuilder, 'test_places.point'); - - expect($result)->toBe('`test_places`.`point`'); -}); diff --git a/tests/SpatialQueryHelpersTest.php b/tests/SpatialQueryHelpersTest.php new file mode 100644 index 0000000..686dfc5 --- /dev/null +++ b/tests/SpatialQueryHelpersTest.php @@ -0,0 +1,35 @@ +getMethod('toExpressionString'); + + $result = $toExpressionStringMethod->invoke($spatialBuilder, DB::raw('POINT(longitude, latitude)')); + + expect($result)->toBe('POINT(longitude, latitude)'); +}); + +it('toExpressionString can handle a Geometry input', function (): void { + $spatialBuilder = TestPlace::query(); + $toExpressionStringMethod = (new ReflectionClass($spatialBuilder))->getMethod('toExpressionString'); + $polygon = Polygon::fromJson('{"type":"Polygon","coordinates":[[[-1,-1],[1,-1],[1,1],[-1,1],[-1,-1]]]}'); + + $result = $toExpressionStringMethod->invoke($spatialBuilder, $polygon); + + $grammar = $spatialBuilder->getGrammar(); + $connection = $spatialBuilder->getConnection(); + $sqlSerializedPolygon = $polygon->toSqlExpression($connection)->getValue($grammar); + expect($result)->toBe($sqlSerializedPolygon); +}); + +it('toExpressionString can handle a string input', function (): void { + $spatialBuilder = TestPlace::query(); + $toExpressionStringMethod = (new ReflectionClass($spatialBuilder))->getMethod('toExpressionString'); + + $result = $toExpressionStringMethod->invoke($spatialBuilder, 'test_places.point'); + + expect($result)->toBe('`test_places`.`point`'); +}); diff --git a/tests/SpatialQueryTest.php b/tests/SpatialQueryTest.php new file mode 100644 index 0000000..82fa641 --- /dev/null +++ b/tests/SpatialQueryTest.php @@ -0,0 +1,24 @@ +convexHull($points) + ->first(); + + // @phpstan-ignore-next-line + $returnedPolygon = Polygon::fromWkb($hull->convex_hull)->toArray(); + + $expectedPolygon = Polygon::fromJson('{"type":"Polygon","coordinates":[[[-1,-1],[1,-1],[1,1],[-1,1],[-1,-1]]]}')->toArray(); + + // Compare coordinates like this to prevent ordering differences between database types/versions + // @phpstan-ignore-next-line + expect($returnedPolygon['coordinates'][0])->toEqualCanonicalizing($expectedPolygon['coordinates'][0]); +}); From e78984a2fea0b8901e8b0b71af43d0efb43ac29d Mon Sep 17 00:00:00 2001 From: Riley Schoppa Date: Tue, 28 Feb 2023 17:28:55 -0500 Subject: [PATCH 2/4] Refactor geometry query --- src/Exceptions/GeometryQueryException.php | 14 +++++ src/GeometryUtilities.php | 53 +++++++++++++++++++ src/SpatialQuery.php | 40 -------------- ...ueryTest.php => GeometryUtilitiesTest.php} | 14 +++-- 4 files changed, 73 insertions(+), 48 deletions(-) create mode 100644 src/Exceptions/GeometryQueryException.php create mode 100644 src/GeometryUtilities.php delete mode 100644 src/SpatialQuery.php rename tests/{SpatialQueryTest.php => GeometryUtilitiesTest.php} (69%) diff --git a/src/Exceptions/GeometryQueryException.php b/src/Exceptions/GeometryQueryException.php new file mode 100644 index 0000000..950154d --- /dev/null +++ b/src/Exceptions/GeometryQueryException.php @@ -0,0 +1,14 @@ +connection)->getQueryGrammar(); + } + + private function getConnection(): \Illuminate\Database\Connection + { + return DB::connection($this->connection); + } + + public function convexHull( + ExpressionContract|Geometry|string $geometry, + ): Geometry + { + $queryResult = DB::connection($this->connection) + ->query() + ->selectRaw( + sprintf( + 'ST_CONVEXHULL(%s) as result', + $this->toExpressionString($geometry), + ) + )->first(); + + throw_unless($queryResult, GeometryQueryException::noData()); + + // @phpstan-ignore-next-line + return Geometry::fromWkb($queryResult->result); + } +} diff --git a/src/SpatialQuery.php b/src/SpatialQuery.php deleted file mode 100644 index fbc722a..0000000 --- a/src/SpatialQuery.php +++ /dev/null @@ -1,40 +0,0 @@ -selectRaw( - sprintf( - 'ST_CONVEXHULL(%s) as convex_hull', - $this->toExpressionString($geometry), - ) - ); - - return $this; - } -} diff --git a/tests/SpatialQueryTest.php b/tests/GeometryUtilitiesTest.php similarity index 69% rename from tests/SpatialQueryTest.php rename to tests/GeometryUtilitiesTest.php index 82fa641..a74901c 100644 --- a/tests/SpatialQueryTest.php +++ b/tests/GeometryUtilitiesTest.php @@ -1,24 +1,22 @@ convexHull($points) - ->first(); - - // @phpstan-ignore-next-line - $returnedPolygon = Polygon::fromWkb($hull->convex_hull)->toArray(); + $hull = GeometryUtilities::make() + ->convexHull($points); $expectedPolygon = Polygon::fromJson('{"type":"Polygon","coordinates":[[[-1,-1],[1,-1],[1,1],[-1,1],[-1,-1]]]}')->toArray(); + expect($hull)->toBeInstanceOf(Polygon::class); + // Compare coordinates like this to prevent ordering differences between database types/versions // @phpstan-ignore-next-line - expect($returnedPolygon['coordinates'][0])->toEqualCanonicalizing($expectedPolygon['coordinates'][0]); + expect($hull->toArray()['coordinates'][0])->toEqualCanonicalizing($expectedPolygon['coordinates'][0]); }); From 91e574644849e458c8e77e67c109e175fc1ae820 Mon Sep 17 00:00:00 2001 From: Riley Schoppa Date: Tue, 28 Feb 2023 18:15:42 -0500 Subject: [PATCH 3/4] Add Distance Sphere Calculation --- API.md | 24 ++++++++++++++++++++- src/GeometryUtilities.php | 37 +++++++++++++++++++++++++++++++++ tests/GeometryUtilitiesTest.php | 12 +++++++++++ 3 files changed, 72 insertions(+), 1 deletion(-) diff --git a/API.md b/API.md index 9929394..64ad150 100644 --- a/API.md +++ b/API.md @@ -442,7 +442,29 @@ Place::query() ``` -## Available Spatial Queries +## Available Geometry Helpers + + +### distanceSphere + +Compute the spherical distance between two points using the [ST_Distance_Sphere](https://dev.mysql.com/doc/refman/5.7/en/spatial-convenience-functions.html#function_st-distance-sphere) function. + +| parameter name | type | +|----------------|------------------| +| `$point1` | `Point \ string` | +| `$point2` | `Point \ string` | + +
Example + +```php + $point1 = new Point(41.9631174, -87.6770458); + $point2 = new Point(40.7628267, -73.9898293); + + $distance = GeometryUtilities::make() + ->distanceSphere($point1, $point2); // 1148798.720296128 (meters) +``` +
+ ### convexHull diff --git a/src/GeometryUtilities.php b/src/GeometryUtilities.php index 1b832ea..73ac121 100644 --- a/src/GeometryUtilities.php +++ b/src/GeometryUtilities.php @@ -7,6 +7,7 @@ use Illuminate\Support\Facades\DB; use MatanYadaev\EloquentSpatial\Exceptions\GeometryQueryException; use MatanYadaev\EloquentSpatial\Objects\Geometry; +use MatanYadaev\EloquentSpatial\Objects\Point; class GeometryUtilities { @@ -50,4 +51,40 @@ public function convexHull( // @phpstan-ignore-next-line return Geometry::fromWkb($queryResult->result); } + + /** + * Compute the distance between two points on a sphere in meters. + * + * @param ExpressionContract|Point|string $point1 + * @param ExpressionContract|Point|string $point2 + * @param float|null $sphereSize Size of the sphere + * @return float Distance between points + */ + public function distanceSphere(ExpressionContract|Point|string $point1, ExpressionContract|Point|string $point2, float $sphereSize = null): float + { + $queryResult = DB::connection($this->connection) + ->query() + ->when($sphereSize, fn ($query) => $query + ->selectRaw( + sprintf( + 'ST_DISTANCE_SPHERE(%s, %s, %s) as result', + $this->toExpressionString($point1), + $this->toExpressionString($point2), + $sphereSize + ) + ), fn ($query) => $query + ->selectRaw( + sprintf( + 'ST_DISTANCE_SPHERE(%s, %s) as result', + $this->toExpressionString($point1), + $this->toExpressionString($point2), + ) + ) + ) + ->first(); + + throw_unless($queryResult, GeometryQueryException::noData()); + + return $queryResult->result; + } } diff --git a/tests/GeometryUtilitiesTest.php b/tests/GeometryUtilitiesTest.php index a74901c..fe71056 100644 --- a/tests/GeometryUtilitiesTest.php +++ b/tests/GeometryUtilitiesTest.php @@ -2,6 +2,7 @@ use Illuminate\Foundation\Testing\DatabaseMigrations; use MatanYadaev\EloquentSpatial\GeometryUtilities; +use MatanYadaev\EloquentSpatial\Objects\Point; use MatanYadaev\EloquentSpatial\Objects\Polygon; uses(DatabaseMigrations::class); @@ -20,3 +21,14 @@ // @phpstan-ignore-next-line expect($hull->toArray()['coordinates'][0])->toEqualCanonicalizing($expectedPolygon['coordinates'][0]); }); + +it('calculates the distance between points on a sphere with sphere size', function (): void { + + $point1 = new Point(41.9631174, -87.6770458); + $point2 = new Point(40.7628267, -73.9898293); + + $distance = GeometryUtilities::make() + ->distanceSphere($point1, $point2); + + expect($distance)->toBe(1148798.720296128); +}); From 09c4704b76f71f24a3b891bde58e0b4798dd4ee0 Mon Sep 17 00:00:00 2001 From: Riley Schoppa Date: Tue, 28 Feb 2023 18:17:19 -0500 Subject: [PATCH 4/4] Add test for distance --- tests/GeometryUtilitiesTest.php | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/tests/GeometryUtilitiesTest.php b/tests/GeometryUtilitiesTest.php index fe71056..e23aa84 100644 --- a/tests/GeometryUtilitiesTest.php +++ b/tests/GeometryUtilitiesTest.php @@ -22,7 +22,7 @@ expect($hull->toArray()['coordinates'][0])->toEqualCanonicalizing($expectedPolygon['coordinates'][0]); }); -it('calculates the distance between points on a sphere with sphere size', function (): void { +it('calculates the distance between points on a sphere', function (): void { $point1 = new Point(41.9631174, -87.6770458); $point2 = new Point(40.7628267, -73.9898293); @@ -32,3 +32,14 @@ expect($distance)->toBe(1148798.720296128); }); + +it('calculates the distance between points on a sphere with sphere size', function (): void { + + $point1 = new Point(41.9631174, -87.6770458); + $point2 = new Point(40.7628267, -73.9898293); + + $distance = GeometryUtilities::make() + ->distanceSphere($point1, $point2, 1); + + expect($distance)->toBe(0.18031725706132895); +});