Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support ST_CENTROID spatial function #95

Closed
leandrocaplan opened this issue Jun 25, 2023 · 8 comments
Closed

Support ST_CENTROID spatial function #95

leandrocaplan opened this issue Jun 25, 2023 · 8 comments

Comments

@leandrocaplan
Copy link

leandrocaplan commented Jun 25, 2023

I'm using Laravel Eloquent Spatial in my project.
Despite I've could used it pretty well when selecting Point objects within a Polygon geometry, I had to create a raw SQL request to use those MySQL Spatial methods.

Here's my code of a method in my app, used to return a Point geometry with the centroid of a polygon:

public function centroide(array $featurePoligono){

  error_log(json_encode($featurePoligono)); //{"type":"Feature" ...}
  error_log(json_encode($featurePoligono['geometry'])); //{"type":"Polygon" ...}
  
  $dbQuery=DB::select('SELECT ST_AsGeoJSON(ST_Centroid(ST_GeomFromGeoJSON(\''. json_encode($featurePoligono) .'\')))');
  
  $centroide=array_values((array)$dbQuery[0])[0];
  
  error_log($centroide); //{"type": "Point", "coordinates": [-58.424712150445174, -34.605689506111084]}
  error_log(gettype($centroide)); //string
  error_log(get_class(json_decode($centroide))); //stdClass
  
  return (array)json_decode($centroide);
}

Is there any way to achieve this using Laravel Eloquent Spatial?

@MatanYadaev MatanYadaev changed the title Compatibility with MySQL Spatial Functions like ST_AsGeoJSON,ST_Centroid and ST_GeomFromGeoJSON Support Spatial Functions: ST_AsGeoJSON, ST_Centroid, and ST_GeomFromGeoJSON Jun 25, 2023
@MatanYadaev MatanYadaev changed the title Support Spatial Functions: ST_AsGeoJSON, ST_Centroid, and ST_GeomFromGeoJSON Support ST_Centroid spatial function Jun 25, 2023
@MatanYadaev MatanYadaev changed the title Support ST_Centroid spatial function Support ST_CENTROID spatial function Jun 25, 2023
@MatanYadaev
Copy link
Owner

@leandrocaplan Hi, let me know if it works for you: https://github.com/MatanYadaev/laravel-eloquent-spatial/blob/master/API.md#withcentroid
Example:

it('calculates geometry centroid', function (): void {
// Arrange
$polygon = Polygon::fromJson('{"type":"Polygon","coordinates":[[[-1,-1],[1,-1],[1,1],[-1,1],[-1,-1]]]}');
TestPlace::factory()->create(['polygon' => $polygon]);
// Act
/** @var TestPlace $testPlace */
$testPlace = TestPlace::query()
->withCentroid('polygon')
->withCasts(['centroid' => Point::class])
->firstOrFail();
// Assert
$expectedCentroid = new Point(0, 0);
expect($testPlace->centroid)->toEqual($expectedCentroid);
});
it('calculates geometry centroid with alias', function (): void {
// Arrange
$polygon = Polygon::fromJson('{"type":"Polygon","coordinates":[[[-1,-1],[1,-1],[1,1],[-1,1],[-1,-1]]]}');
TestPlace::factory()->create(['polygon' => $polygon]);
// Act
/** @var TestPlace $testPlace */
$testPlace = TestPlace::query()
->withCentroid('polygon', 'centroid_alias')
->withCasts(['centroid_alias' => Point::class])
->firstOrFail();
// Assert
$expectedCentroid = new Point(0, 0);
expect($testPlace->centroid_alias)->toEqual($expectedCentroid);
});

@leandrocaplan
Copy link
Author

Hi Matan!

Sorry for the delay of my answer. I've had a few difficult and busy days, and I'm just now able to give you a proper feedback.

First of all, I saw that you updated the package with the new 'withCentroid' method, just after I've posted this issue. I really appreciate that goodwill!

Answering you question, asking if the code shown in 'API.md', as an example of usage of the new 'withCentroid' method, worked for me, I can tell you my answer is:
It worked on a %25, since I got many errors while testing it in my app.

The main ones were:

-When applying 'withCast()' in the chain after 'withCentroid()':

"Call to undefined method MatanYadaev\EloquentSpatial\SpatialBuilder::withCast()" (I couldn't even find the definition of that method anywhere)

-When removing that method in the chain, but just executing the query with 'first()' or 'get()':

"Call to a member function hydrate() on null"

We should take into consideration that, since Eloquent is a ORM, every Eloquent model and accordingly, every Eloquent Query Builder instance, it's related to a table in our database. However, these kind of functions of MySQL Spatial, such as 'ST_Centroid' or 'ST_GeomFromGeoJSON', doesn't always retrieve data from a table to perform the needed query, since they just return a geometry object based on the values of another geometry, which may be coded directly in the query text.

For example:

"select ST_CENTROID(ST_GeomFromText('POLYGON((-58.420486450195 -34.619216934912, ... , -58.420486450195 -34.619216934912))', 0)) AS centroid"

In this way, we may need an instance of 'Illuminate\Database\Query\Builder' (Laravel Query Builder) instead of an instance of 'Illuminate\Database\Eloquent\Builder' (Eloquent Query Builder), which 'MatanYadaev\EloquentSpatial\SpatialBuilder' inherits from, to perform those kind of queries properly, since we don't want to generate those queries from a given Eloquent model.

So, in the example of API.md, we have the line:

Place::create(['polygon' => $polygon]); //After '$polygon = Polygon::fromJson('{"type":"Polygon",...'

Being 'Place' an Eloquent ORM model, which creates a new record into our database just for calculating the centroid of a given polygon. This is something we don't want and no need to.

So, I'm sharing you here, the section of my code that at least could use, somewhere in it, the new 'withCentroid()' method to achieve its purpose, with the lines which throwed error commented and describing the error:

    public function centroide(array $featurePoligono){

        //Code of the previous version:

        error_log(json_encode($featurePoligono)); //{"type":"Feature" ...}
        error_log(json_encode($featurePoligono['geometry'])); //{"type":"Polygon" ...}
        
        //We generate and run a raw SQL query:
        $dbQuery=DB::select('SELECT ST_AsGeoJSON(ST_Centroid(ST_GeomFromGeoJSON(\''. json_encode($featurePoligono) .'\')))');
        
        $centroide=array_values((array)$dbQuery[0])[0]; //En $centroide nos queda un GeoJSON como string

        error_log($centroide); //{"type": "Point", "coordinates": [-58.424712150445174, -34.605689506111084]}
        error_log(gettype($centroide)); //string
        error_log(get_class(json_decode($centroide))); //stdClass
       
        


        //Code using new 'withCentroid' method:

        $polygon = Polygon::fromJson(json_encode($featurePoligono['geometry']));

        //Place::create(['polygon' => $polygon]); //Unwanted sentence

        //We create a new SpatialBuilder instance which is not related to any model, just passing to it constructor, an instance of
        //a Laravel (not Eloquent) query builder, by calling 'DB::query' without arguments:
        $query=new SpatialBuilder(DB::query()); 
        //$query=new SpatialBuilder(new Builder(DB::connection())); //Same result as line above

        //Applying 'withCentroid' to new instance of SpatialBuilder:
        $placeWithCentroid = $query
            ->withCentroid($polygon);
            //->withCast('centroid', Point::class); //Call to undefined method MatanYadaev\EloquentSpatial\SpatialBuilder::withCast()
            //->first(); //Call to a member function hydrate() on null

        error_log($placeWithCentroid->toSql()); //// select ST_CENTROID(ST_GeomFromText('POLYGON((...
        //error_log($placeWithCentroid->get()); //Call to a member function hydrate() on null

        //Executing the query as raw SQL, but with text generated from SpatialBuilder instance
        $dbQuery2=DB::select($placeWithCentroid->toSql())[0];
 
        error_log($dbQuery2->centroid);
        
        $centroide2=Point::fromWkb($dbQuery2->centroid)->toJson();
       
        error_log($centroide2); //{"type": "Point", "coordinates": [-58.424712150445174, -34.605689506111084]}

        

        
        //These two lines have the same result (the second one returning the GeoJSON generated using 'withCentroid', and the first one using just raw SQL):
        return (array)json_decode($centroide);
        //return (array)json_decode($centroide2);
    }

May you tell me if there's a more proper way to achieve this same result by using this package?

Thanks a lot!
Leandro

@MatanYadaev
Copy link
Owner

@leandrocaplan can you please fork this repo and reproduce this issue with a failing test? Or even a fresh new Laravel project?
A failing test will be the best way for me to fix it.

@leandrocaplan
Copy link
Author

@MatanYadaev I'll create a new branch of the project in my own repo with the failure, and last version of laravel-eloquent-spatial defined into 'composer.json', that would be OK?

@MatanYadaev
Copy link
Owner

@leandrocaplan Hi, there are two issues:

  1. You used withCast instead of withCasts - it's an issue in my docs, I fixed it.
  2. withCasts works only when you have a model, and in this case, you don't have one. You use Laravel's Query Builder, and Not Eloquent (the ORM). This package isn't meant for the Query Builder, unfortunately.

There is a hacky workaround, if you want:

$emptyModel = new class extends Model {};
$query = (new SpatialBuilder(DB::query()))->setModel($emptyModel);
$centroid = $query
    ->withCentroid($polygon)
    ->withCasts(['centroid' => Point::class])
    ->from(null)
    ->first()['centroid'];

@leandrocaplan
Copy link
Author

@MatanYadaev That worked pretty fine!

Just notice this: if you check the API doc, at 'withCentroid' example, it shows:

$placeWithCentroid = Place::query()
    ->withCentroid('polygon')
    ->withCast('centroid', Point:class) // This is important, otherwise the centroid will be returned as a binary string.
    ->first();

Instead of:

$placeWithCentroid = Place::query()
    ->withCentroid('polygon')
    ->withCasts(['centroid' => Point:class]) 
    ->first();

Besides we should have 'withCasts' instead of 'withCast', we must pass it an array, instead of two arguments like the doc is currently showing.
There's also any documentation of the 'withCasts' method at the moment in the docs.

Thanks a lot!
Leandro

@MatanYadaev
Copy link
Owner

@leandrocaplan I forgot to push a commit :) Now it's live. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants