Laravel 中的地理数据检索
在这个系列中,我将试图涵盖我在那个项目中遇到的与地理数据相关的所有方面。首先是如何检索地理数据。
Overpass API
有些人可能知道 OpenStreetMap 项目 —— Overpass API 是它的一部分,可以用来检索数据。它的行为类似于 GraphQL,因为它只有一个端点,你可以使用 Overpass QL 向其发送查询,通常 OpenStreetMap 及 Overpass API 只有 3 不同的实体:
OSM 数据对象
我们可以通过 PHP 代码创建持有这些实体的简易对象。
Node
class Node
{
/**
* @param array<string, string> $tags
*/
public function __construct(
public readonly int $id,
public readonly float $lat,
public readonly float $lon,
public readonly array $tags = [],
) {
}
}
Way
class Way
{
/**
* @param array{lat: float, lon: float} $center
* @param array<array-key, int> $nodes
* @param array<string, string> $tags
*/
public function __construct(
public readonly int $id,
public readonly array $center,
public readonly array $nodes,
public readonly array $tags = [],
) {
}
}
Relation
class Relation
{
/**
* @param array{lat: float, lon: float} $center
* @param array<array-key, array{type: string, ref: int, role: string}> $members
* @param array<string, string> $tags
*/
public function __construct(
public readonly int $id,
public readonly array $center,
public readonly array $members,
public readonly array $tags = [],
) {
}
}
如果你希望使用 DTO 包来自动进行属性赋值、强制转换,请参考 spatie/laravel-data。它们也是普通对象 - 如你所见,在 way 对象中只持有子元素(比如 node)的 ID。当然,你也可以先解析节点(node)对象,然后直接赋值节点。
Overpass 查询
首先,你可以在 overpass-turbo.eu 上使用一个很酷的沙盒/playground 来测试 Overpass 查询。你可以看到简单的响应或将其可视化在地图上,这对首先构建查询或在没有得到所需内容时进行调试非常有帮助。
我暂时使用以下查询,因为我只对一些 POI 和建筑感兴趣。
[out:json]
[timeout:30]
[bbox:{$box->south()},{$box->west()},{$box->north()},{$box->east()}];
(
node[amenity];
way[building];
>;
);
out center;
对每一行做一下简短的解释:
- [out:json] - 响应应该是 JSON
- [timeout:30] - 查询超时时间为 30 秒
- [bbox:{$box->south()},{$box->west()},{$box->north()},{$box->east()}]; - 为整个查询定义一个边界
- node[amenity]; - 选择带有 amenity 标签的 node
- way[building]; - 选择带有 building 标签的 way
- >; - 选择 building way 中的所有 node
- out center; - 为每个项目添加中心
这不会找到所有的建筑,因为有些建筑是 relation 实体,而它们是更复杂的结构。具有整体(自上而下视角)的主要建筑。而处理 Relation 要复杂得多,所以我暂时忽略了它们。
发送查询并解析结果
由于我们已经有了 DTO 和查询,使用 Laravel HTTP 客户端,这是一个非常简单的过程。
<?php
namespace App\Actions;
use App\Concerns\Makeable;
use App\Data\Overpass\Node;
use App\Data\Overpass\Relation;
use App\Data\Overpass\Way;
use App\Geotools\Data\Box;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Http;
class FetchMapDataInBoundingBox
{
/**
* @return array{ node: Collection<array-key, Node>, way: Collection<array-key, Way>, relation: Collection<array-key, Relation> }
*
* @throws \Illuminate\Http\Client\ConnectionException
*/
public function execute(Box $box): array
{
$response = Http::acceptJson()
->timeout(60)
->connectTimeout(10)
->throw()
->get('https://overpass-api.de/api/interpreter', [
'data' => <<<TXT
[out:json]
[timeout:30]
[bbox:{$box->south()},{$box->west()},{$box->north()},{$box->east()}];
(
node[amenity];
way[building];
>;
);
out center;
TXT
])
->collect('elements')
->groupBy('type');
$nodes = collect($response->get('node'))
->map(fn (array $data) => Node::from($data))
->values();
$ways = collect($response->get('way'))
->map(fn (array $data) => Way::from($data))
->values();
$relations = collect($response->get('relation'))
->map(fn (array $data) => Relation::from($data))
->values();
return [
'node' => $nodes,
'way' => $ways,
'relation' => $relations,
];
}
}
对此操作的一点解释:
- 生成基于 Box 对象的查询 - 你可以使用任何你想要的 box 对象,也可以自己创建一个
- 发送查询到 https://overpass-api.de/api/interpreter 并创建一个元素(element)集合
- 按照类型(type)对元素进行分组
- 将所有元素匹配到 PHP 对象
- 返回一个包含三个集合的数组,其包含返回的元素
总结
通过这种方法你可以查询您想要/需要的任何地理数据。通过调整边界框和查询本身,可以更改返回的数据——根据 box 的大小,你可能需要增加超时时间。如果你严重依赖这些数据,并且经常需要它们,你可能应该拥有自己的 Overpass API 实例。
后面的文章,我将展示如何使用 PostGIS 将这些信息存储在本地数据库中。