tarfin-labs/laravel-spatial

Laravel 地理空间数据类型和函数处理的包。

v2.0.0 2024-04-09 07:03 UTC

README

Latest Version on Packagist Total Downloads GitHub Actions

这是一个用于处理地理空间数据类型和函数的 Laravel 包。

它仅支持 MySQL 地理空间数据类型和函数,其他 RDBMS 将在路线图上。

Laravel 兼容性

支持的数据类型

可用的作用域

  • withinDistanceTo($column, $coordinates, $distance)
  • selectDistanceTo($column, $coordinates)
  • orderByDistanceTo($column, $coordinates, 'asc')

安装

您可以通过 composer 安装此包。

composer require tarfin-labs/laravel-spatial

用法

使用迁移文件生成新的模型

php artisan make:model Address --migration

1- 迁移

基于 Laravel 版本的代码差异

项目中的某些代码片段在 Laravel 11 版本之前和之后有所不同。以下是指定这些差异的步骤

对于 Laravel 8、9 和 10 版本

要添加空间数据字段,您需要从 TarfinLabs\LaravelSpatial\Migrations\SpatialMigration 扩展迁移。

这是一个简单的抽象类,在构造函数中添加了 point 空间数据类型到 Doctrine 映射类型。

use TarfinLabs\LaravelSpatial\Migrations\SpatialMigration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends SpatialMigration {

    public function up(): void
    {
        Schema::create('addresses', function (Blueprint $table) {
            $table->point('location');
        })
    }
}

上述迁移创建了一个具有 location 空间列的 addresses 表。

没有 SRID 属性的空间列不受 SRID 限制,接受任何 SRID 的值。然而,优化器无法在这些列上使用 SPATIAL 索引,直到将列定义修改为包含 SRID 属性,这可能需要首先修改列内容,以确保所有值具有相同的 SRID。

因此,您应该在迁移中提供 SRID 属性以使用空间索引,并且索引列必须为 NOT NULL

Schema::create('addresses', function (Blueprint $table) {
    $table->point(column: 'location', srid: 4326);

    $table->spatialIndex('location');
})

对于 Laravel 11 及以上版本

从 Laravel 11 开始,迁移创建如下

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

return new class extends Migration {
    
    public function up(): void
    {
        Schema::create('addresses', function (Blueprint $table) {
            $table->geography('location', 'point');
        })
    }

}

从 Laravel 11 开始,删除了 pointlineStringpolygongeometryCollectionmultiPointmultiLineStringmultiPolygon 方法。因此,我们更新为使用 geography 方法。该 geography 方法将默认 SRID 值设置为 4326。

向现有表添加带索引的新位置列的问题

当在 Laravel 中添加带索引的新位置列时,如果存在现有数据,可能会很麻烦。一个常见的错误是尝试使用 ->default(new Point(0, 0, 4326)) 为新列设置默认值。然而,POINT 列不能有默认值,这可能导致在尝试向列添加索引时出现问题,因为索引列不能为可空。

要解决这个问题,建议执行以下两步迁移

对于 Laravel 8、9 和 10 版本

public function up()
{
    // Add the new location column as nullable
    Schema::table('table', function (Blueprint $table) {
        $table->point('location')->nullable();
    });

    // In the second go, set 0,0 values, make the column not null and finally add the spatial index
    Schema::table('table', function (Blueprint $table) {
        DB::statement("UPDATE `table` SET `location` = POINT(0,0);");

        DB::statement("ALTER TABLE `table` CHANGE `location` `location` POINT NOT NULL;");

        $table->spatialIndex('location');
    });
}

对于 Laravel 11 及以上版本

public function up()
{
    // Add the new location column as nullable
    Schema::table('table', function (Blueprint $table) {
        $table->geography('location', 'point')->nullable();
    });

    // In the second go, set 0,0 values, make the column not null and finally add the spatial index
    Schema::table('table', function (Blueprint $table) {
        DB::statement("UPDATE `addresses` SET `location` = ST_GeomFromText('POINT(0 0)', 4326);");

        DB::statement("ALTER TABLE `table` CHANGE `location` `location` POINT NOT NULL;");

        $table->spatialIndex('location');
    });
}

2- 模型

在模型中填充 $fillable$casts 数组

use Illuminate\Database\Eloquent\Model;
use TarfinLabs\LaravelSpatial\Casts\LocationCast;
use TarfinLabs\LaravelSpatial\Traits\HasSpatial;

class Address extends Model {

    use HasSpatial;

    protected $fillable = [
        'id',
        'name',
        'address',
        'location',
    ];
    
    protected array $casts = [
        'location' => LocationCast::class
    ];

}

3- 空间数据类型

Point 表示位置的坐标,并包含 latitudelongitudesrid 属性。

此时,理解 SRID 非常重要。每个空间实例都有一个空间参考标识符 (SRID)。SRID 与基于特定椭球体(用于平面地球测绘或球面地球测绘)的空间参考系统相对应。空间列可以包含具有不同 SRID 的对象。

有关 SRID 的详细信息,请参阅以下链接: https://en.wikipedia.org/wiki/Spatial_reference_system

  • latitudelongitude 参数的默认值是 0.0
  • srid参数的默认值是0
use TarfinLabs\LaravelSpatial\Types\Point;

$location = new Point(lat: 28.123456, lng: 39.123456, srid: 4326);

$location->getLat(); // 28.123456
$location->getLng(); // 39.123456
$location->getSrid(); // 4326

您可以通过laravel-spatial配置文件覆盖默认的SRID。为此,您应该使用vendor:publish artisan命令发布配置文件

php artisan vendor:publish --provider="TarfinLabs\LaravelSpatial\LaravelSpatialServiceProvider"

之后,您可以在config/laravel-spatial.php中更改default_srid的值

return [
    'default_srid' => 4326,
];

配置WKT选项

默认情况下,此包使用经度 纬度的顺序来表示空间函数中使用的WKT格式的坐标值。对于某些版本的MySQL,如果不将axis-order选项明确设置为long-lat,则会将坐标对解释为纬-经。

然而,MariaDB默认将WKT值读取为long-lat,其空间函数如ST_GeomFromTextST_DISTANCE不接受与MySQL对应函数类似的options参数。这意味着在使用MariaDB时使用此包会导致语法错误或访问违规:1582 在调用本地函数'ST_GeomFromText'时参数数量不正确异常。

为了解决这个问题,我们在配置文件中添加了with_wkt_options参数,可以用来覆盖默认选项。此属性可以设置为false以完全删除选项参数,这可以修复使用MariaDB时的错误。

return [
    'with_wkt_options' => true,
];

批量操作

为了使用Laravel中的upsert()方法在一个查询中插入或更新多个具有空间数据的行,该包需要一个解决方案来避免错误Object of class TarfinLabs\LaravelSpatial\Types\Point could not be converted to string

解决方案是使用toGeomFromText()方法将Point对象转换为WKT字符串,然后使用DB::raw()创建原始查询字符串。

以下是如何在代码中使用此解决方案的示例

use TarfinLabs\LaravelSpatial\Types\Point;

$points = [
    ['external_id' => 5, 'location' => DB::raw((new Point(lat: 40.73, lng: -73.93))->toGeomFromText())],
    ['external_id' => 7, 'location' => DB::raw((new Point(lat: -37.81, lng: 144.96))->toGeomFromText())],
];

Property::upsert($points, ['external_id'], ['location']);

4- 范围

withinDistanceTo()

您可以使用withinDistanceTo()范围根据给定的距离筛选位置

要筛选给定坐标10公里范围内的地址

use TarfinLabs\LaravelSpatial\Types\Point;
use App\Models\Address;

Address::query()
       ->withinDistanceTo('location', new Point(lat: 25.45634, lng: 35.54331), 10000)
       ->get();

selectDistanceTo()

您可以使用selectDistanceTo()范围通过两点之间的距离。距离将以米为单位

use TarfinLabs\LaravelSpatial\Types\Point;
use App\Models\Address;

Address::query()
       ->selectDistanceTo('location', new Point(lat: 25.45634, lng: 35.54331))
       ->get();

orderByDistanceTo()

您可以根据给定的坐标对模型进行排序

use TarfinLabs\LaravelSpatial\Types\Point;
use App\Models\Address;

// ASC
Address::query()
       ->orderByDistanceTo('location', new Point(lat: 25.45634, lng: 35.54331))
       ->get();

// DESC
Address::query()
       ->orderByDistanceTo('location', new Point(lat: 25.45634, lng: 35.54331), 'desc')
       ->get();

获取位置的纬度和经度

use App\Models\Address;

$address = Address::find(1);
$address->location; // TarfinLabs\LaravelSpatial\Types\Point

$address->location->getLat();
$address->location->getLng();

创建一个新的具有位置的地址

use App\Models\Address;

Address::create([
    'name'      => 'Bag End',
    'address'   => '1 Bagshot Row, Hobbiton, Shire',
    'location'  => new Point(lat: 25.45634, lng: 35.54331),
]);

在资源中的使用

要获取资源中位置转换字段的数组表示形式,您可以返回parent::toArray($request)

如果您需要从资源返回自定义数组,则可以使用Point对象的toArray()方法。

class LocationResource extends JsonResource
{
    public function toArray($request)
    {
        return [
            'location' => $this->location->toArray(),
        ];
    }
}

无论如何,您都会得到以下输出作为位置转换字段

{
    "lat": 25.45634,
    "lng": 35.54331,
    "srid": 4326
}

测试

composer test

变更日志

请参阅变更日志以获取有关最近更改的更多信息。

贡献

请参阅贡献指南以获取详细信息。

安全性

如果您发现任何与安全相关的问题,请通过development@tarfin.com发送电子邮件,而不是使用问题跟踪器。

鸣谢

许可证

MIT许可证(MIT)。有关更多信息,请参阅许可证文件