Skip to content

Commit

Permalink
Merge pull request #79 from grimzy/issue-64-scope-order-by-distance
Browse files Browse the repository at this point in the history
Adds scopes orderByDistance() and orderByDistanceSphere()
  • Loading branch information
grimzy authored Mar 4, 2019
2 parents b1db057 + 4523cf4 commit f124c23
Show file tree
Hide file tree
Showing 7 changed files with 367 additions and 112 deletions.
248 changes: 145 additions & 103 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@

[![Build Status](https://img.shields.io/travis/grimzy/laravel-mysql-spatial.svg?style=flat-square)](https://travis-ci.org/grimzy/laravel-mysql-spatial)
[![Code Climate](https://img.shields.io/codeclimate/maintainability/grimzy/laravel-mysql-spatial.svg?style=flat-square)](https://codeclimate.com/github/grimzy/laravel-mysql-spatial/maintainability)
[![Code Climate](https://img.shields.io/codeclimate/c/grimzy/laravel-mysql-spatial.svg?style=flat-square&colorB=4BCA2A)](https://codeclimate.com/github/grimzy/laravel-mysql-spatial/test_coverage)[![Packagist](https://img.shields.io/packagist/v/grimzy/laravel-mysql-spatial.svg?style=flat-square)](https://packagist.org/packages/grimzy/laravel-mysql-spatial)
[![Packagist](https://img.shields.io/packagist/dt/grimzy/laravel-mysql-spatial.svg?style=flat-square)](https://packagist.org/packages/grimzy/laravel-mysql-spatial)
[![Code Climate](https://img.shields.io/codeclimate/c/grimzy/laravel-mysql-spatial.svg?style=flat-square&colorB=4BCA2A)](https://codeclimate.com/github/grimzy/laravel-mysql-spatial/test_coverage) [![Packagist](https://img.shields.io/packagist/v/grimzy/laravel-mysql-spatial.svg?style=flat-square)](https://packagist.org/packages/grimzy/laravel-mysql-spatial)
[![Packagist](https://img.shields.io/packagist/dt/grimzy/laravel-mysql-spatial.svg?style=flat-square)](https://packagist.org/packages/grimzy/laravel-mysql-spatial) [![StyleCI](https://github.styleci.io/repos/83766141/shield?branch=master)](https://github.styleci.io/repos/83766141)
[![license](https://img.shields.io/github/license/mashape/apistatus.svg?style=flat-square)](LICENSE)

Laravel package to easily work with [MySQL Spatial Data Types](https://dev.mysql.com/doc/refman/5.7/en/spatial-datatypes.html) and [MySQL Spatial Functions](https://dev.mysql.com/doc/refman/5.7/en/spatial-function-reference.html).
Laravel package to easily work with [MySQL Spatial Data Types](https://dev.mysql.com/doc/refman/8.0/en/spatial-type-overview.html) and [MySQL Spatial Functions](https://dev.mysql.com/doc/refman/8.0/en/spatial-function-reference.html).

Please check the documentation for your MySQL version. MySQL's Extension for Spatial Data was added in MySQL 5.5 but many Spatial Functions were changed in 5.6 and 5.7.

Expand Down Expand Up @@ -52,11 +52,14 @@ From the command line:
php artisan make:migration create_places_table
```

Then edit the migration you just created by adding at least one spatial data field:
Then edit the migration you just created by adding at least one spatial data field. For Laravel versions prior to 5.5, you can use the Blueprint provided by this package (Grimzy\LaravelMysqlSpatial\Schema\Blueprint):

```php
use Illuminate\Database\Migrations\Migration;
use Grimzy\LaravelMysqlSpatial\Schema\Blueprint;
use Illuminate\Database\Schema\Blueprint;

// For Laravel < 5.5
// use Grimzy\LaravelMysqlSpatial\Schema\Blueprint;

class CreatePlacesTable extends Migration {

Expand Down Expand Up @@ -114,7 +117,8 @@ use Illuminate\Database\Eloquent\Model;
use Grimzy\LaravelMysqlSpatial\Eloquent\SpatialTrait;

/**
* @property \Grimzy\LaravelMysqlSpatial\Types\Point $location
* @property \Grimzy\LaravelMysqlSpatial\Types\Point $location
* @property \Grimzy\LaravelMysqlSpatial\Types\Polygon $area
*/
class Place extends Model
{
Expand Down Expand Up @@ -166,103 +170,19 @@ $lat = $place2->location->getLat(); // 40.7484404
$lng = $place2->location->getLng(); // -73.9878441
```

## Migrations

### Columns

Available [MySQL Spatial Types](https://dev.mysql.com/doc/refman/5.7/en/spatial-datatypes.html) migration blueprints:

-
`$table->geometry('column_name');`

- `$table->point('column_name');`
- `$table->lineString('column_name');`
- `$table->polygon('column_name');`
- `$table->multiPoint('column_name');`
- `$table->multiLineString('column_name');`
- `$table->multiPolygon('column_name');`
- `$table->geometryCollection('column_name');`

### Spatial indexes

You can add or drop spatial indexes in your migrations with the `spatialIndex` and `dropSpatialIndex` blueprints.

- `$table->spatialIndex('column_name');`
- `$table->dropSpatialIndex(['column_name']);` or `$table->dropSpatialIndex('index_name')`

Note about spatial indexes from the [MySQL documentation](https://dev.mysql.com/doc/refman/5.7/en/creating-spatial-indexes.html):

> For [`MyISAM`](https://dev.mysql.com/doc/refman/5.7/en/myisam-storage-engine.html) and (as of MySQL 5.7.5) `InnoDB` tables, MySQL can create spatial indexes using syntax similar to that for creating regular indexes, but using the `SPATIAL` keyword. Columns in spatial indexes must be declared `NOT NULL`.
Also please read this [**important note**](https://laravel.com/docs/5.5/migrations#indexes) regarding Index Lengths in the Laravel 5.6 documentation.

For example, as a follow up to the [Quickstart](#user-content-create-a-migration); from the command line, generate a new migration:

```shell
php artisan make:migration update_places_table
```

Then edit the migration file that you just created:

```php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class UpdatePlacesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
// MySQL < 5.7.5: table has to be MyISAM
// \DB::statement('ALTER TABLE places ENGINE = MyISAM');

Schema::table('places', function (Blueprint $table) {
// Make sure point is not nullable
$table->point('location')->change();

// Add a spatial index on the location field
$table->spatialIndex('location');
});
}

/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('places', function (Blueprint $table) {
$table->dropSpatialIndex(['location']); // either an array of column names or the index name
});

// \DB::statement('ALTER TABLE places ENGINE = InnoDB');

Schema::table('places', function (Blueprint $table) {
$table->point('location')->nullable()->change();
});
}
}
```

## Geometry classes

### Available Geometry classes

| Grimzy\LaravelMysqlSpatial\Types | OpenGIS Class |
| ---------------------------------------- | ---------------------------------------- |
| `Point($lat, $lng)` | [Point](https://dev.mysql.com/doc/refman/5.7/en/gis-class-point.html) |
| `MultiPoint(Point[])` | [MultiPoint](https://dev.mysql.com/doc/refman/5.7/en/gis-class-multipoint.html) |
| `LineString(Point[])` | [LineString](https://dev.mysql.com/doc/refman/5.7/en/gis-class-linestring.html) |
| `MultiLineString(LineString[])` | [MultiLineString](https://dev.mysql.com/doc/refman/5.7/en/gis-class-multilinestring.html) |
| `Polygon(LineString[])` *([exterior and interior boundaries](https://dev.mysql.com/doc/refman/5.7/en/gis-class-polygon.html))* | [Polygon](https://dev.mysql.com/doc/refman/5.7/en/gis-class-polygon.html) |
| `MultiPolygon(Polygon[])` | [MultiPolygon](https://dev.mysql.com/doc/refman/5.7/en/gis-class-multipolygon.html) |
| `GeometryCollection(Geometry[])` | [GeometryCollection](https://dev.mysql.com/doc/refman/5.7/en/gis-class-geometrycollection.html) |
| Grimzy\LaravelMysqlSpatial\Types | OpenGIS Class |
| ------------------------------------------------------------ | ------------------------------------------------------------ |
| `Point($lat, $lng)` | [Point](https://dev.mysql.com/doc/refman/8.0/en/gis-class-point.html) |
| `MultiPoint(Point[])` | [MultiPoint](https://dev.mysql.com/doc/refman/8.0/en/gis-class-multipoint.html) |
| `LineString(Point[])` | [LineString](https://dev.mysql.com/doc/refman/8.0/en/gis-class-linestring.html) |
| `MultiLineString(LineString[])` | [MultiLineString](https://dev.mysql.com/doc/refman/8.0/en/gis-class-multilinestring.html) |
| `Polygon(LineString[])` *([exterior and interior boundaries](https://dev.mysql.com/doc/refman/8.0/en/gis-class-polygon.html))* | [Polygon](https://dev.mysql.com/doc/refman/8.0/en/gis-class-polygon.html) |
| `MultiPolygon(Polygon[])` | [MultiPolygon](https://dev.mysql.com/doc/refman/8.0/en/gis-class-multipolygon.html) |
| `GeometryCollection(Geometry[])` | [GeometryCollection](https://dev.mysql.com/doc/refman/8.0/en/gis-class-geometrycollection.html) |

Check out the [Class diagram](https://user-images.githubusercontent.com/1837678/30788608-a5afd894-a16c-11e7-9a51-0a08b331d4c4.png).

Expand Down Expand Up @@ -290,17 +210,21 @@ for($polygon as $i => $linestring) {

```php
// fromWKT($wkt)
$polygon = Polygon::fromWKT('POLYGON((0 0,4 0,4 4,0 4,0 0),(1 1, 2 1, 2 2, 1 2,1 1))');
$point = Point::fromWKT('POINT(2 1)');
$point->toWKT(); // POINT(2 1)

$polygon = Polygon::fromWKT('POLYGON((0 0,4 0,4 4,0 4,0 0),(1 1, 2 1, 2 2, 1 2,1 1))');
$polygon->toWKT(); // POLYGON((0 0,4 0,4 4,0 4,0 0),(1 1, 2 1, 2 2, 1 2,1 1))
```

##### From/To String

```php
// fromString($wkt)
$polygon = Polygon::fromString('(0 0,4 0,4 4,0 4,0 0),(1 1, 2 1, 2 2, 1 2,1 1)');
$point = new Point(1, 2); // lat, lng
(string)$point // lng, lat: 2 1

$polygon = Polygon::fromString('(0 0,4 0,4 4,0 4,0 0),(1 1, 2 1, 2 2, 1 2,1 1)');
(string)$polygon; // (0 0,4 0,4 4,0 4,0 0),(1 1, 2 1, 2 2, 1 2,1 1)
```

Expand All @@ -309,7 +233,7 @@ $polygon = Polygon::fromString('(0 0,4 0,4 4,0 4,0 0),(1 1, 2 1, 2 2, 1 2,1 1)')
The Geometry classes implement [`JsonSerializable`](http://php.net/manual/en/class.jsonserializable.php) and `Illuminate\Contracts\Support\Jsonable` to help serialize into GeoJSON:

```php
$point = new Point(10, 20);
$point = new Point(40.7484404, -73.9878441);

json_encode($point); // or $point->toJson();

Expand All @@ -329,7 +253,7 @@ json_encode($point); // or $point->toJson();
To deserialize a GeoJSON string into a Geometry class, you can use `Geometry::fromJson($json_string)` :

```php
$locaction = Geometry::fromJson('{"type":"Point","coordinates":[3.4,1.2]}');
$location = Geometry::fromJson('{"type":"Point","coordinates":[3.4,1.2]}');
$location instanceof Point::class; // true
$location->getLat(); // 1.2
$location->getLng()); // 3.4
Expand All @@ -354,9 +278,127 @@ Available scopes:
- `intersects($geometryColumn, $geometry)`
- `overlaps($geometryColumn, $geometry)`
- `doesTouch($geometryColumn, $geometry)`
- `orderBySpatial($geometryColumn, $geometry, $orderFunction, $direction = 'asc')`
- `orderByDistance($geometryColumn, ​$geometry, ​$direction = 'asc')`
- `orderByDistanceSphere($geometryColumn, ​$geometry, ​$direction = 'asc')`

*Note that behavior and availability of MySQL spatial analysis functions differs in each MySQL version (cf. [documentation](https://dev.mysql.com/doc/refman/5.7/en/spatial-function-reference.html)).*

## Migrations

For Laravel versions prior to 5.5, you can use the Blueprint provided with this package: `Grimzy\LaravelMysqlSpatial\Schema\Blueprint`.

```php
use Illuminate\Database\Migrations\Migration;
use Grimzy\LaravelMysqlSpatial\Schema\Blueprint;

class CreatePlacesTable extends Migration {
// ...
}
```

### Columns

Available [MySQL Spatial Types](https://dev.mysql.com/doc/refman/5.7/en/spatial-datatypes.html) migration blueprints:

- `$table->geometry('column_name')`
- `$table->point('column_name')`
- `$table->lineString('column_name')`
- `$table->polygon('column_name')`
- `$table->multiPoint('column_name')`
- `$table->multiLineString('column_name')`
- `$table->multiPolygon('column_name')`
- `$table->geometryCollection('column_name')`

### Spatial indexes

You can add or drop spatial indexes in your migrations with the `spatialIndex` and `dropSpatialIndex` blueprints.

- `$table->spatialIndex('column_name')`
- `$table->dropSpatialIndex(['column_name'])` or `$table->dropSpatialIndex('index_name')`

Note about spatial indexes from the [MySQL documentation](https://dev.mysql.com/doc/refman/5.7/en/creating-spatial-indexes.html):

> For [`MyISAM`](https://dev.mysql.com/doc/refman/5.7/en/myisam-storage-engine.html) and (as of MySQL 5.7.5) `InnoDB` tables, MySQL can create spatial indexes using syntax similar to that for creating regular indexes, but using the `SPATIAL` keyword. Columns in spatial indexes must be declared `NOT NULL`.
Also please read this [**important note**](https://laravel.com/docs/5.5/migrations#indexes) regarding Index Lengths in the Laravel 5.6 documentation.

For example, as a follow up to the [Quickstart](#user-content-create-a-migration); from the command line, generate a new migration:

```shell
php artisan make:migration update_places_table
```

Then edit the migration file that you just created:

```php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class UpdatePlacesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
// MySQL < 5.7.5: table has to be MyISAM
// \DB::statement('ALTER TABLE places ENGINE = MyISAM');

Schema::table('places', function (Blueprint $table) {
// Make sure point is not nullable
$table->point('location')->change();

// Add a spatial index on the location field
$table->spatialIndex('location');
});
}

/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('places', function (Blueprint $table) {
$table->dropSpatialIndex(['location']); // either an array of column names or the index name
});

// \DB::statement('ALTER TABLE places ENGINE = InnoDB');

Schema::table('places', function (Blueprint $table) {
$table->point('location')->nullable()->change();
});
}
}
```

## Tests

```shell
composer test
# or
composer test:unit
composer test:integration
```

Integration tests require a running MySQL database. If you have Docker installed, you can start easily start one:

```shell
make start_db # starts MySQL 8.0
# or
make start_db V=5.7 # starts a MySQL 5.7
```

## Contributing

Recommendations and pull request are most welcome! Pull requests with tests are the best! There are still a lot of MySQL spatial functions to implement or creative ways to use spatial functions.

## Credits

Originally inspired from [njbarrett's Laravel postgis package](https://github.com/njbarrett/laravel-postgis).

7 changes: 7 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
{
"name": "grimzy/laravel-mysql-spatial",
"description": "MySQL spatial data types extension for Laravel.",
"scripts": {
"test": "phpunit -c phpunit.xml.dist",
"test:unit": "phpunit -c phpunit.xml.dist --testsuite unit",
"test:integration": "phpunit -c phpunit.xml.dist --testsuite integration"
},
"type": "library",
"license": "MIT",
"authors": [
Expand All @@ -11,6 +16,8 @@
],
"require": {
"php": ">=5.5",
"ext-pdo": "*",
"ext-json": "*",
"illuminate/database": "^5.2",
"geo-io/wkb-parser": "^1.0",
"jmikola/geojson": "^1.0"
Expand Down
9 changes: 7 additions & 2 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@
processIsolation="false"
stopOnFailure="false">
<testsuites>
<testsuite name="Unit Tests">
<testsuite name="unit">
<directory suffix="Test.php">./tests/Unit</directory>
</testsuite>
<testsuite name="Integration Tests">
<testsuite name="integration">
<directory suffix="Test.php">./tests/Integration</directory>
</testsuite>
</testsuites>
Expand All @@ -36,5 +36,10 @@
<env name="CACHE_DRIVER" value="array"/>
<env name="SESSION_DRIVER" value="array"/>
<env name="QUEUE_DRIVER" value="sync"/>
<env name="DB_DATABASE" value="spatial_test"/>
<env name="DB_HOST" value="127.0.0.1"/>
<env name="DB_PORT" value="3306"/>
<env name="DB_USERNAME" value="root"/>
<env name="DB_PASSWORD" value=""/>
</php>
</phpunit>
Loading

0 comments on commit f124c23

Please sign in to comment.