正式版不能直接请求地图域名时,由后端转发逆地理。保存区划时带上省的 id,避免同名直辖县挂错省份。 Co-authored-by: Cursor <cursoragent@cursor.com>
252 行
9.2 KiB
PHP
252 行
9.2 KiB
PHP
<?php
|
|
|
|
namespace app\common\library;
|
|
|
|
use addons\shopro\model\Area;
|
|
|
|
/**
|
|
* 天地图逆地理编码。
|
|
* 微信选点返回 GCJ-02,天地图使用 CGCS2000,请求前先转换。
|
|
*/
|
|
class Tianditu
|
|
{
|
|
public static function reverse($longitude, $latitude)
|
|
{
|
|
$key = self::key();
|
|
if ($key === '') {
|
|
throw new \Exception('未配置天地图密钥');
|
|
}
|
|
|
|
list($lon, $lat) = self::gcj02ToWgs84((float)$longitude, (float)$latitude);
|
|
$result = self::request($lon, $lat, $key);
|
|
$component = isset($result['addressComponent']) && is_array($result['addressComponent'])
|
|
? $result['addressComponent']
|
|
: [];
|
|
$area = self::matchArea($component);
|
|
$address = self::detailAddress($result, $component);
|
|
$province = $area['province'];
|
|
$city = $area['city'] === $province ? '' : $area['city'];
|
|
|
|
return [
|
|
'area_text' => $area['province'] . '-' . $area['city'] . '-' . $area['district'],
|
|
'address' => $address,
|
|
'formatted_address' => $province . $city . $area['district'] . $address,
|
|
];
|
|
}
|
|
|
|
protected static function key()
|
|
{
|
|
$key = function_exists('config') ? config('tianditu.key') : '';
|
|
if (!$key) {
|
|
$file = dirname(__DIR__, 2) . '/extra/tianditu.php';
|
|
$config = is_file($file) ? include $file : [];
|
|
$key = is_array($config) && !empty($config['key']) ? $config['key'] : '';
|
|
}
|
|
return trim((string)$key);
|
|
}
|
|
|
|
protected static function request($longitude, $latitude, $key)
|
|
{
|
|
$postStr = json_encode([
|
|
'lon' => round($longitude, 6),
|
|
'lat' => round($latitude, 6),
|
|
'ver' => 1,
|
|
], JSON_UNESCAPED_UNICODE);
|
|
$url = 'https://api.tianditu.gov.cn/geocoder?postStr=' . rawurlencode($postStr) . '&type=geocode&tk=' . rawurlencode($key);
|
|
|
|
$ch = curl_init($url);
|
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
|
|
curl_setopt($ch, CURLOPT_TIMEOUT, 8);
|
|
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
|
|
curl_setopt($ch, CURLOPT_USERAGENT, 'chufang-server/1.0');
|
|
$body = curl_exec($ch);
|
|
$errno = curl_errno($ch);
|
|
curl_close($ch);
|
|
|
|
if ($errno || $body === false || $body === '') {
|
|
throw new \Exception('地址解析失败,请稍后重试');
|
|
}
|
|
|
|
$json = json_decode($body, true);
|
|
if (!is_array($json) || !isset($json['status']) || (string)$json['status'] !== '0' || empty($json['result']) || !is_array($json['result'])) {
|
|
$message = is_array($json) && !empty($json['msg']) ? $json['msg'] : '地址解析失败';
|
|
throw new \Exception($message);
|
|
}
|
|
|
|
return $json['result'];
|
|
}
|
|
|
|
protected static function matchArea(array $component)
|
|
{
|
|
$district = self::findDistrict($component);
|
|
if (!$district) {
|
|
throw new \Exception('未能匹配区县,请手动选择所在地区');
|
|
}
|
|
|
|
$city = Area::where('id', $district['pid'])->find();
|
|
$province = $city ? Area::where('id', $city['pid'])->find() : null;
|
|
if (!$city || !$province || (int)$city['level'] !== 2 || (int)$province['level'] !== 1) {
|
|
throw new \Exception('未能匹配省市区,请手动选择所在地区');
|
|
}
|
|
|
|
return [
|
|
'province' => $province['name'],
|
|
'city' => $city['name'],
|
|
'district' => $district['name'],
|
|
];
|
|
}
|
|
|
|
protected static function findDistrict(array $component)
|
|
{
|
|
$countyId = self::adminCode(isset($component['county_code']) ? $component['county_code'] : '');
|
|
if ($countyId) {
|
|
$district = Area::where('id', $countyId)->where('level', 3)->find();
|
|
if ($district) {
|
|
return $district;
|
|
}
|
|
}
|
|
|
|
$county = trim(isset($component['county']) ? $component['county'] : '');
|
|
if ($county === '') {
|
|
return null;
|
|
}
|
|
|
|
$cityId = self::adminCode(isset($component['city_code']) ? $component['city_code'] : '');
|
|
$query = Area::where('name', $county)->where('level', 3);
|
|
if ($cityId) {
|
|
$query->where('pid', $cityId);
|
|
}
|
|
return $query->find();
|
|
}
|
|
|
|
protected static function detailAddress(array $result, array $component)
|
|
{
|
|
$segments = [];
|
|
$adminNames = [];
|
|
foreach (['province', 'city', 'county'] as $field) {
|
|
$name = trim(isset($component[$field]) ? (string)$component[$field] : '');
|
|
if ($name !== '') {
|
|
$adminNames[] = $name;
|
|
}
|
|
}
|
|
foreach (['town', 'road', 'address', 'poi'] as $field) {
|
|
$segment = trim(isset($component[$field]) ? (string)$component[$field] : '');
|
|
$segment = self::stripAdmin($segment, $adminNames);
|
|
if ($segment === '' || self::isRelative($segment)) {
|
|
continue;
|
|
}
|
|
$replaced = false;
|
|
foreach ($segments as $index => $exist) {
|
|
if (mb_strpos($segment, $exist) !== false) {
|
|
$segments[$index] = $segment;
|
|
$replaced = true;
|
|
break;
|
|
}
|
|
if (mb_strpos($exist, $segment) !== false) {
|
|
$replaced = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!$replaced) {
|
|
$segments[] = $segment;
|
|
}
|
|
}
|
|
if ($segments) {
|
|
return implode('', $segments);
|
|
}
|
|
|
|
$detail = isset($result['formatted_address']) ? $result['formatted_address'] : '';
|
|
foreach (['province', 'city', 'county'] as $field) {
|
|
$part = trim(isset($component[$field]) ? (string)$component[$field] : '');
|
|
if ($part !== '') {
|
|
$detail = preg_replace('/' . preg_quote($part, '/') . '/u', '', $detail, 1);
|
|
}
|
|
}
|
|
return trim($detail);
|
|
}
|
|
|
|
protected static function stripAdmin($segment, array $adminNames)
|
|
{
|
|
foreach ($adminNames as $name) {
|
|
if ($name !== '' && mb_strpos($segment, $name) !== false) {
|
|
$segment = str_replace($name, '', $segment);
|
|
}
|
|
$short = preg_replace('/(省|市|自治区|特别行政区)$/u', '', $name);
|
|
if ($short === '' || $short === $name || mb_strlen($short) < 2) {
|
|
continue;
|
|
}
|
|
if ($segment === $short) {
|
|
return '';
|
|
}
|
|
$suffixLength = mb_strlen($short);
|
|
if (mb_strlen($segment) > $suffixLength && mb_substr($segment, -$suffixLength) === $short) {
|
|
$segment = mb_substr($segment, 0, mb_strlen($segment) - $suffixLength);
|
|
}
|
|
}
|
|
return trim($segment);
|
|
}
|
|
|
|
protected static function isRelative($text)
|
|
{
|
|
return (bool)preg_match('/[东西南北].{0,4}约?\d+米/u', $text);
|
|
}
|
|
|
|
protected static function adminCode($code)
|
|
{
|
|
$digits = preg_replace('/\D/', '', (string)$code);
|
|
if (strlen($digits) < 6) {
|
|
return 0;
|
|
}
|
|
return (int)substr($digits, -6);
|
|
}
|
|
|
|
protected static function gcj02ToWgs84($longitude, $latitude)
|
|
{
|
|
if (self::outOfChina($longitude, $latitude)) {
|
|
return [$longitude, $latitude];
|
|
}
|
|
|
|
$offset = self::offset($longitude, $latitude);
|
|
return [$longitude * 2 - $offset[0], $latitude * 2 - $offset[1]];
|
|
}
|
|
|
|
protected static function offset($longitude, $latitude)
|
|
{
|
|
$a = 6378245.0;
|
|
$ee = 0.00669342162296594323;
|
|
$dlng = self::transformLng($longitude - 105.0, $latitude - 35.0);
|
|
$dlat = self::transformLat($longitude - 105.0, $latitude - 35.0);
|
|
$radLat = $latitude / 180.0 * M_PI;
|
|
$magic = sin($radLat);
|
|
$magic = 1 - $ee * $magic * $magic;
|
|
$sqrtMagic = sqrt($magic);
|
|
$dlat = ($dlat * 180.0) / (($a * (1 - $ee)) / ($magic * $sqrtMagic) * M_PI);
|
|
$dlng = ($dlng * 180.0) / ($a / $sqrtMagic * cos($radLat) * M_PI);
|
|
return [$longitude + $dlng, $latitude + $dlat];
|
|
}
|
|
|
|
protected static function outOfChina($longitude, $latitude)
|
|
{
|
|
return $longitude < 72.004 || $longitude > 137.8347 || $latitude < 0.8293 || $latitude > 55.8271;
|
|
}
|
|
|
|
protected static function transformLat($lng, $lat)
|
|
{
|
|
$ret = -100.0 + 2.0 * $lng + 3.0 * $lat + 0.2 * $lat * $lat + 0.1 * $lng * $lat + 0.2 * sqrt(abs($lng));
|
|
$ret += (20.0 * sin(6.0 * $lng * M_PI) + 20.0 * sin(2.0 * $lng * M_PI)) * 2.0 / 3.0;
|
|
$ret += (20.0 * sin($lat * M_PI) + 40.0 * sin($lat / 3.0 * M_PI)) * 2.0 / 3.0;
|
|
$ret += (160.0 * sin($lat / 12.0 * M_PI) + 320 * sin($lat * M_PI / 30.0)) * 2.0 / 3.0;
|
|
return $ret;
|
|
}
|
|
|
|
protected static function transformLng($lng, $lat)
|
|
{
|
|
$ret = 300.0 + $lng + 2.0 * $lat + 0.1 * $lng * $lng + 0.1 * $lng * $lat + 0.1 * sqrt(abs($lng));
|
|
$ret += (20.0 * sin(6.0 * $lng * M_PI) + 20.0 * sin(2.0 * $lng * M_PI)) * 2.0 / 3.0;
|
|
$ret += (20.0 * sin($lng * M_PI) + 40.0 * sin($lng / 3.0 * M_PI)) * 2.0 / 3.0;
|
|
$ret += (150.0 * sin($lng / 12.0 * M_PI) + 300.0 * sin($lng / 30.0 * M_PI)) * 2.0 / 3.0;
|
|
return $ret;
|
|
}
|
|
}
|