|
|
<?php
|
|
|
|
|
|
namespace App\Http\Controllers\Api;
|
|
|
|
|
|
use App\Http\Controllers\Controller;
|
|
|
use Illuminate\Http\JsonResponse;
|
|
|
use Illuminate\Http\Request;
|
|
|
use Illuminate\Support\Facades\Http;
|
|
|
|
|
|
class MapController extends Controller
|
|
|
{
|
|
|
/** 苏州大致范围(minLng,minLat,maxLng,maxLat) */
|
|
|
private const SUZHOU_MAP_BOUND = '119.90,30.75,121.35,32.05';
|
|
|
|
|
|
/** 天地图「服务端」Key(与浏览器 Key 不同);未配置则搜索/逆地理走腾讯 WebService */
|
|
|
private function tiandituServerTk(): ?string
|
|
|
{
|
|
|
$key = trim((string) env('TIANDITU_SERVER_TK', ''));
|
|
|
if ($key !== '') {
|
|
|
return $key;
|
|
|
}
|
|
|
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
private function tencentServerKey(): string
|
|
|
{
|
|
|
$key = trim((string) env('TENCENT_MAP_SERVER_KEY', ''));
|
|
|
abort_unless($key !== '', 500, '未配置腾讯地图服务端 Key(TENCENT_MAP_SERVER_KEY)');
|
|
|
|
|
|
return $key;
|
|
|
}
|
|
|
|
|
|
public function search(Request $request): JsonResponse
|
|
|
{
|
|
|
$data = $request->validate([
|
|
|
'keyword' => ['required', 'string', 'max:120'],
|
|
|
'region' => ['nullable', 'string', 'max:80'],
|
|
|
]);
|
|
|
|
|
|
$tk = $this->tiandituServerTk();
|
|
|
if ($tk !== null) {
|
|
|
$rows = $this->searchTianditu($data['keyword'], $tk);
|
|
|
if ($rows !== null) {
|
|
|
return response()->json($rows);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return response()->json($this->searchTencent($data['keyword'], $data['region'] ?? '苏州'));
|
|
|
}
|
|
|
|
|
|
public function reverseGeocode(Request $request): JsonResponse
|
|
|
{
|
|
|
$data = $request->validate([
|
|
|
'lat' => ['required', 'numeric'],
|
|
|
'lng' => ['required', 'numeric'],
|
|
|
]);
|
|
|
|
|
|
$tk = $this->tiandituServerTk();
|
|
|
if ($tk !== null) {
|
|
|
$row = $this->reverseTianditu((float) $data['lat'], (float) $data['lng'], $tk);
|
|
|
if ($row !== null) {
|
|
|
return response()->json($row);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return response()->json($this->reverseTencent((float) $data['lat'], (float) $data['lng']));
|
|
|
}
|
|
|
|
|
|
/** @return array<int, array<string, mixed>>|null null 表示应回退腾讯 */
|
|
|
private function searchTianditu(string $keyword, string $tk): ?array
|
|
|
{
|
|
|
$postStr = json_encode([
|
|
|
'keyWord' => $keyword,
|
|
|
'level' => 12,
|
|
|
'mapBound' => self::SUZHOU_MAP_BOUND,
|
|
|
'queryType' => 1,
|
|
|
'start' => 0,
|
|
|
'count' => 15,
|
|
|
], JSON_UNESCAPED_UNICODE);
|
|
|
|
|
|
$resp = Http::get('https://api.tianditu.gov.cn/v2/search', [
|
|
|
'postStr' => $postStr,
|
|
|
'type' => 'query',
|
|
|
'tk' => $tk,
|
|
|
])->json();
|
|
|
|
|
|
if (! is_array($resp)) {
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
if ($this->tiandituApiFailed($resp)) {
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
$pois = $resp['pois'] ?? $resp['data'] ?? [];
|
|
|
if (! is_array($pois)) {
|
|
|
return [];
|
|
|
}
|
|
|
|
|
|
return collect($pois)->map(function ($item) {
|
|
|
$item = is_array($item) ? $item : [];
|
|
|
$lonlat = (string) ($item['lonlat'] ?? $item['lonLat'] ?? '');
|
|
|
$lng = null;
|
|
|
$lat = null;
|
|
|
if ($lonlat !== '' && str_contains($lonlat, ',')) {
|
|
|
[$lngRaw, $latRaw] = array_map('trim', explode(',', $lonlat, 2));
|
|
|
$lng = is_numeric($lngRaw) ? (float) $lngRaw : null;
|
|
|
$lat = is_numeric($latRaw) ? (float) $latRaw : null;
|
|
|
}
|
|
|
if ($lat === null) {
|
|
|
$lat = data_get($item, 'lat') ?? data_get($item, 'location.lat');
|
|
|
}
|
|
|
if ($lng === null) {
|
|
|
$lng = data_get($item, 'lng') ?? data_get($item, 'lon') ?? data_get($item, 'location.lng');
|
|
|
}
|
|
|
|
|
|
return [
|
|
|
'title' => $item['name'] ?? $item['title'] ?? '',
|
|
|
'address' => $item['address'] ?? '',
|
|
|
'province' => $item['province'] ?? '',
|
|
|
'city' => $item['city'] ?? $item['cityName'] ?? '',
|
|
|
'district' => $item['district'] ?? $item['county'] ?? '',
|
|
|
'lat' => $lat,
|
|
|
'lng' => $lng,
|
|
|
];
|
|
|
})->filter(fn ($row) => $row['lat'] !== null && $row['lng'] !== null)->values()->all();
|
|
|
}
|
|
|
|
|
|
/** @return array<int, array<string, mixed>> */
|
|
|
private function searchTencent(string $keyword, string $region): array
|
|
|
{
|
|
|
$resp = Http::get('https://apis.map.qq.com/ws/place/v1/suggestion', [
|
|
|
'keyword' => $keyword,
|
|
|
'region' => $region,
|
|
|
'region_fix' => 1,
|
|
|
'key' => $this->tencentServerKey(),
|
|
|
])->json();
|
|
|
|
|
|
$status = (int) ($resp['status'] ?? -1);
|
|
|
if ($status === 121) {
|
|
|
abort(422, '腾讯地图 WebService Key 每日调用量已达上限。请配置 TIANDITU_SERVER_TK 或更换腾讯 Key。');
|
|
|
}
|
|
|
if ($status !== 0) {
|
|
|
abort(422, $resp['message'] ?? '地图搜索失败');
|
|
|
}
|
|
|
|
|
|
return collect($resp['data'] ?? [])->map(function ($item) {
|
|
|
return [
|
|
|
'title' => $item['title'] ?? '',
|
|
|
'address' => $item['address'] ?? '',
|
|
|
'province' => $item['province'] ?? '',
|
|
|
'city' => $item['city'] ?? '',
|
|
|
'district' => $item['district'] ?? '',
|
|
|
'lat' => data_get($item, 'location.lat'),
|
|
|
'lng' => data_get($item, 'location.lng'),
|
|
|
];
|
|
|
})->filter(fn ($row) => $row['lat'] !== null && $row['lng'] !== null)->values()->all();
|
|
|
}
|
|
|
|
|
|
/** @return array<string, mixed>|null */
|
|
|
private function reverseTianditu(float $lat, float $lng, string $tk): ?array
|
|
|
{
|
|
|
$postStr = json_encode([
|
|
|
'lon' => $lng,
|
|
|
'lat' => $lat,
|
|
|
'ver' => 1,
|
|
|
], JSON_UNESCAPED_UNICODE);
|
|
|
|
|
|
$resp = Http::get('https://api.tianditu.gov.cn/geocoder', [
|
|
|
'postStr' => $postStr,
|
|
|
'type' => 'geocode',
|
|
|
'tk' => $tk,
|
|
|
])->json();
|
|
|
|
|
|
if (! is_array($resp) || $this->tiandituApiFailed($resp)) {
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
$result = $resp['result'] ?? [];
|
|
|
$addr = is_array($result) ? ($result['formatted_address'] ?? $result['address'] ?? '') : '';
|
|
|
$comp = is_array($result) ? ($result['addressComponent'] ?? []) : [];
|
|
|
|
|
|
return [
|
|
|
'address' => $addr,
|
|
|
'province' => data_get($comp, 'province'),
|
|
|
'city' => data_get($comp, 'city'),
|
|
|
'district' => data_get($comp, 'district') ?? data_get($comp, 'county'),
|
|
|
];
|
|
|
}
|
|
|
|
|
|
/** @return array<string, mixed> */
|
|
|
private function reverseTencent(float $lat, float $lng): array
|
|
|
{
|
|
|
$resp = Http::get('https://apis.map.qq.com/ws/geocoder/v1/', [
|
|
|
'location' => $lat . ',' . $lng,
|
|
|
'get_poi' => 0,
|
|
|
'key' => $this->tencentServerKey(),
|
|
|
])->json();
|
|
|
|
|
|
$status = (int) ($resp['status'] ?? -1);
|
|
|
if ($status === 121) {
|
|
|
abort(422, '腾讯地图 WebService Key 每日调用量已达上限。请配置 TIANDITU_SERVER_TK 或更换腾讯 Key。');
|
|
|
}
|
|
|
if ($status !== 0) {
|
|
|
abort(422, $resp['message'] ?? '逆地理编码失败');
|
|
|
}
|
|
|
|
|
|
$result = $resp['result'] ?? [];
|
|
|
|
|
|
return [
|
|
|
'address' => $result['address'] ?? '',
|
|
|
'province' => data_get($result, 'address_component.province'),
|
|
|
'city' => data_get($result, 'address_component.city'),
|
|
|
'district' => data_get($result, 'address_component.district'),
|
|
|
];
|
|
|
}
|
|
|
|
|
|
/** @param array<string, mixed> $resp */
|
|
|
private function tiandituApiFailed(array $resp): bool
|
|
|
{
|
|
|
$code = $resp['code'] ?? $resp['status'] ?? null;
|
|
|
if ($code === 301012 || (is_string($resp['msg'] ?? null) && str_contains((string) $resp['msg'], '权限'))) {
|
|
|
return true;
|
|
|
}
|
|
|
if ($code !== null && (string) $code !== '0' && (int) $code !== 0) {
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
return false;
|
|
|
}
|
|
|
}
|