mapUtil.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371
  1. <?php
  2. /**
  3. * User: ssh
  4. * Date: 2019/11/20
  5. * Time: 23:21
  6. */
  7. namespace common\components;
  8. use linslin\yii2\curl;
  9. use yii\helpers\Json;
  10. class mapUtil
  11. {
  12. public static $thirdMapKey = 'da0634bbbe4b9434854158c7d68e3cb8';
  13. public static function suggestion($region, $keyword)
  14. {
  15. if (getenv('YII_ENV') == 'production') {
  16. $url = "https://restapi.amap.com/v3/assistant/inputtips?city={$region}&keywords={$keyword}&key=" . self::$thirdMapKey;
  17. } else {
  18. $url = "http://restapi.amap.com/v3/assistant/inputtips?city={$region}&keywords={$keyword}&key=" . self::$thirdMapKey;
  19. }
  20. $curl = new curl\Curl();
  21. $result = $curl->get($url);
  22. return Json::decode($result);
  23. }
  24. /**
  25. * 计算两点骑行距离(米)。
  26. * 使用高德普通骑行路径规划 /v5/direction/bicycling(配额更宽);
  27. * 不再用 electrobike 电动车接口(高级服务,日调用量通常很低)。
  28. * 与电动车路线相比,城市场景一般差几十到一两百米,少数因限行差异可能更大,运费场景可接受。
  29. */
  30. public static function calculateDistance($fromLnt, $fromLat, $toLnt, $toLat)
  31. {
  32. if (getenv('YII_ENV') == 'production') {
  33. $url = "https://restapi.amap.com/v5/direction/bicycling?key=" . self::$thirdMapKey . '&origin=' . $fromLnt . ',' . $fromLat . '&destination=' . $toLnt . ',' . $toLat;
  34. } else {
  35. $url = "http://restapi.amap.com/v5/direction/bicycling?key=" . self::$thirdMapKey . '&origin=' . $fromLnt . ',' . $fromLat . '&destination=' . $toLnt . ',' . $toLat;
  36. }
  37. $curl = new curl\Curl();
  38. $result = $curl->get($url);
  39. $result = Json::decode($result);
  40. $distance = 0;
  41. if (!empty($result['status']) && $result['status'] == 1) {
  42. $path = $result['route']['paths'] ?? [];
  43. $current = $path[0] ?? [];
  44. $distance = $current['distance'] ?? 0;
  45. } else {
  46. $infoCode = $result['infocode'] ?? '';
  47. $info = $result['info'] ?? '';
  48. noticeUtil::push("计算两地距离报错:{$infoCode} {$info}", '15280215347');
  49. util::fail('距离计算失败');
  50. }
  51. return $distance;
  52. }
  53. /**
  54. * 计算客户到店骑行距离(内部仍走 calculateDistance=普通骑行)。
  55. * 方法名保留 EleBike 兼容已有调用方,实际已非电动车接口。
  56. */
  57. public static function calcShopDistanceByEleBike($beginPlace, $toPlace, $report = 1)
  58. {
  59. $beginLnt = $beginPlace->long ?? '';
  60. $beginLat = $beginPlace->lat ?? '';
  61. $toLnt = $toPlace->long ?? '';
  62. $toLat = $toPlace->lat ?? '';
  63. // 校验经纬度参数格式
  64. if (empty($beginLnt) || empty($beginLat)) {
  65. if ($report == 1) {
  66. util::fail('计算距离失败,请完善地址');
  67. }
  68. return ['distance' => 0, 'distanceKm' => 0];
  69. }
  70. if (!is_numeric($beginLnt) || !is_numeric($beginLat)) {
  71. if ($report == 1) {
  72. util::fail('计算距离失败,请完善地址');
  73. }
  74. return ['distance' => 0, 'distanceKm' => 0];
  75. }
  76. if (empty($toLnt) || empty($toLat)) {
  77. if ($report == 1) {
  78. util::fail('计算距离失败,商家地址没有完善');
  79. }
  80. return ['distance' => 0, 'distanceKm' => 0];
  81. }
  82. if (!is_numeric($toLnt) || !is_numeric($toLat)) {
  83. if ($report == 1) {
  84. util::fail('计算距离失败,商家地址没有完善');
  85. }
  86. return ['distance' => 0, 'distanceKm' => 0];
  87. }
  88. $distance = self::calculateDistance($beginLnt, $beginLat, $toLnt, $toLat);
  89. $distanceKm = bcdiv($distance, 1000, 2);
  90. return ['distance' => $distance, 'distanceKm' => $distanceKm];
  91. }
  92. /**
  93. * 批量计算多个起点到一个终点的距离 (高德 V3 距离测量接口)
  94. * @param array $origins 起点坐标数组,例如:[['lng' => '116.481028', 'lat' => '39.989643'], ...]
  95. * @param string $toLnt 终点经度
  96. * @param string $toLat 终点纬度
  97. * @param int $type 路径计算的方式和方法 (0:直线距离, 1:驾车, 3:步行)
  98. * @return array
  99. */
  100. public static function calculateBatchDistance($origins, $toLnt, $toLat, $type = 1)
  101. {
  102. if (empty($origins)) {
  103. return [];
  104. }
  105. // 将起点数组拼接成高德要求的格式: lng1,lat1|lng2,lat2
  106. $originStrs = [];
  107. foreach ($origins as $origin) {
  108. $originStrs[] = $origin['lng'] . ',' . $origin['lat'];
  109. }
  110. $originsParam = implode('|', $originStrs);
  111. $destinationParam = $toLnt . ',' . $toLat;
  112. $domain = getenv('YII_ENV') == 'production' ? 'https://restapi.amap.com' : 'http://restapi.amap.com';
  113. $url = $domain . "/v3/distance?key=" . self::$thirdMapKey . "&origins={$originsParam}&destination={$destinationParam}&type={$type}";
  114. $curl = new curl\Curl();
  115. $result = $curl->get($url);
  116. $result = \yii\helpers\Json::decode($result);
  117. $distances = [];
  118. if (!empty($result['status']) && $result['status'] == 1 && !empty($result['results'])) {
  119. foreach ($result['results'] as $index => $res) {
  120. // 返回的结果顺序与传入的 origins 顺序一致
  121. $distances[$index] = $res['distance'] ?? 0;
  122. }
  123. } else {
  124. noticeUtil::push("高德批量测距接口返回失败:" . json_encode($result) . " url:" . $url);
  125. }
  126. return $distances;
  127. }
  128. /**
  129. * 逆地址解析(高德),根据经纬度解析省市区与地址
  130. * @param string|float $lat 纬度
  131. * @param string|float $long 经度
  132. * @param string $locationType gcj02|wgs84,wgs84 时传 coordsys=gps 给高德
  133. * @return array
  134. */
  135. public static function resolveLocation($lat, $long, $locationType = 'gcj02')
  136. {
  137. $lat = (float)$lat;
  138. $long = (float)$long;
  139. if ($lat == 0 || $long == 0) {
  140. return [];
  141. }
  142. $domain = getenv('YII_ENV') == 'production' ? 'https://restapi.amap.com' : 'http://restapi.amap.com';
  143. // 高德 location 参数:经度,纬度
  144. $location = $long . ',' . $lat;
  145. $url = $domain . '/v3/geocode/regeo?location=' . urlencode($location)
  146. . '&key=' . self::$thirdMapKey
  147. . '&extensions=base';
  148. if ($locationType === 'wgs84' || $locationType === 'gps') {
  149. $url .= '&coordsys=gps';
  150. }
  151. $curl = new curl\Curl();
  152. $result = $curl->get($url);
  153. $result = Json::decode($result);
  154. return self::formatAmapRegeoResult($result);
  155. }
  156. /**
  157. * 格式化高德逆地理编码结果
  158. */
  159. private static function formatAmapRegeoResult($result)
  160. {
  161. if (empty($result['status']) || (string)$result['status'] !== '1' || empty($result['regeocode'])) {
  162. return [];
  163. }
  164. $regeo = $result['regeocode'];
  165. $component = $regeo['addressComponent'] ?? [];
  166. $province = self::normalizeAmapRegionValue($component['province'] ?? '');
  167. $city = self::normalizeAmapRegionValue($component['city'] ?? '');
  168. $district = self::normalizeAmapRegionValue($component['district'] ?? '');
  169. if (empty($city)) {
  170. $city = $province;
  171. }
  172. $showAddress = $regeo['formatted_address'] ?? '';
  173. $township = self::normalizeAmapRegionValue($component['township'] ?? '');
  174. $streetNumber = $component['streetNumber'] ?? [];
  175. $street = is_array($streetNumber) ? ($streetNumber['street'] ?? '') : '';
  176. $number = is_array($streetNumber) ? ($streetNumber['number'] ?? '') : '';
  177. $address = trim($township . $street . $number);
  178. if (empty($address)) {
  179. $address = $showAddress;
  180. if ($province && mb_strpos($address, $province) === 0) {
  181. $address = mb_substr($address, mb_strlen($province));
  182. }
  183. if ($city && $city !== $province && mb_strpos($address, $city) === 0) {
  184. $address = mb_substr($address, mb_strlen($city));
  185. }
  186. if ($district && mb_strpos($address, $district) === 0) {
  187. $address = mb_substr($address, mb_strlen($district));
  188. }
  189. $address = trim($address);
  190. }
  191. if (empty($address) || empty($province) || empty($city)) {
  192. return [];
  193. }
  194. return [
  195. 'province' => $province,
  196. 'city' => $city,
  197. 'district' => $district,
  198. 'address' => $address,
  199. 'showAddress' => $showAddress ?: ($province . $city . $district . $address),
  200. ];
  201. }
  202. /**
  203. * 根据经纬度获取当前位置及附近 POI 列表(高德逆地理 extensions=all)
  204. */
  205. public static function resolveNearby($lat, $long, $locationType = 'gcj02')
  206. {
  207. $lat = (float)$lat;
  208. $long = (float)$long;
  209. if ($lat == 0 || $long == 0) {
  210. return [];
  211. }
  212. $domain = getenv('YII_ENV') == 'production' ? 'https://restapi.amap.com' : 'http://restapi.amap.com';
  213. $location = $long . ',' . $lat;
  214. $url = $domain . '/v3/geocode/regeo?location=' . urlencode($location)
  215. . '&key=' . self::$thirdMapKey
  216. . '&extensions=all&radius=1000&roadlevel=0';
  217. if ($locationType === 'wgs84' || $locationType === 'gps') {
  218. $url .= '&coordsys=gps';
  219. }
  220. $curl = new curl\Curl();
  221. $result = Json::decode($curl->get($url));
  222. if (empty($result['status']) || (string)$result['status'] !== '1' || empty($result['regeocode'])) {
  223. return [];
  224. }
  225. $regeo = $result['regeocode'];
  226. $component = $regeo['addressComponent'] ?? [];
  227. $province = self::normalizeAmapRegionValue($component['province'] ?? '');
  228. $city = self::normalizeAmapRegionValue($component['city'] ?? '');
  229. $district = self::normalizeAmapRegionValue($component['district'] ?? '');
  230. if (empty($city)) {
  231. $city = $province;
  232. }
  233. $centerAddress = $regeo['formatted_address'] ?? '';
  234. $centerName = self::normalizeAmapRegionValue($component['township'] ?? '');
  235. if (empty($centerName)) {
  236. $centerName = '当前位置';
  237. }
  238. $list = [[
  239. 'name' => $centerName,
  240. 'address' => $centerAddress,
  241. 'province' => $province,
  242. 'city' => $city,
  243. 'district' => $district,
  244. 'location' => $long . ',' . $lat,
  245. 'showAddress' => $centerAddress,
  246. ]];
  247. $pois = $regeo['pois'] ?? [];
  248. $exists = [$centerName];
  249. foreach ($pois as $poi) {
  250. $poiName = $poi['name'] ?? '';
  251. if ($poiName === '' || in_array($poiName, $exists, true)) {
  252. continue;
  253. }
  254. $exists[] = $poiName;
  255. $poiAddress = $poi['address'] ?? '';
  256. if ($poiAddress === '') {
  257. $poiAddress = $centerAddress ? ($centerAddress . ' ' . $poiName) : $poiName;
  258. }
  259. $list[] = [
  260. 'name' => $poiName,
  261. 'address' => $poiAddress,
  262. 'province' => $province,
  263. 'city' => $city,
  264. 'district' => $district,
  265. 'location' => $poi['location'] ?? ($long . ',' . $lat),
  266. 'showAddress' => $poiAddress,
  267. ];
  268. if (count($list) >= 21) {
  269. break;
  270. }
  271. }
  272. return $list;
  273. }
  274. private static function normalizeAmapRegionValue($value)
  275. {
  276. if (is_array($value)) {
  277. return !empty($value) ? (string)$value[0] : '';
  278. }
  279. return (string)$value;
  280. }
  281. /**
  282. * 坐标系转换:从 WGS84 转换为 GCJ-02
  283. * @param $lng
  284. * @param $lat
  285. * @return array
  286. */
  287. public static function wgs84ToGcj02($lng, $lat)
  288. {
  289. $lng = (float)$lng;
  290. $lat = (float)$lat;
  291. if (self::outOfChina($lng, $lat)) {
  292. return ['lng' => $lng, 'lat' => $lat];
  293. }
  294. $a = 6378245.0;
  295. $ee = 0.00669342162296594323;
  296. $dLat = self::transformLat($lng - 105.0, $lat - 35.0);
  297. $dLng = self::transformLng($lng - 105.0, $lat - 35.0);
  298. $radLat = $lat / 180.0 * M_PI;
  299. $magic = sin($radLat);
  300. $magic = 1 - $ee * $magic * $magic;
  301. $sqrtMagic = sqrt($magic);
  302. $dLat = ($dLat * 180.0) / (($a * (1 - $ee)) / ($magic * $sqrtMagic) * M_PI);
  303. $dLng = ($dLng * 180.0) / ($a / $sqrtMagic * cos($radLat) * M_PI);
  304. return ['lng' => $lng + $dLng, 'lat' => $lat + $dLat];
  305. }
  306. private static function transformLat($lng, $lat)
  307. {
  308. $ret = -100.0 + 2.0 * $lng + 3.0 * $lat + 0.2 * $lat * $lat + 0.1 * $lng * $lat + 0.2 * sqrt(abs($lng));
  309. $ret += (20.0 * sin(6.0 * $lng * M_PI) + 20.0 * sin(2.0 * $lng * M_PI)) * 2.0 / 3.0;
  310. $ret += (20.0 * sin($lat * M_PI) + 40.0 * sin($lat / 3.0 * M_PI)) * 2.0 / 3.0;
  311. $ret += (160.0 * sin($lat / 12.0 * M_PI) + 320.0 * sin($lat * M_PI / 30.0)) * 2.0 / 3.0;
  312. return $ret;
  313. }
  314. private static function transformLng($lng, $lat)
  315. {
  316. $ret = 300.0 + $lng + 2.0 * $lat + 0.1 * $lng * $lng + 0.1 * $lng * $lat + 0.1 * sqrt(abs($lng));
  317. $ret += (20.0 * sin(6.0 * $lng * M_PI) + 20.0 * sin(2.0 * $lng * M_PI)) * 2.0 / 3.0;
  318. $ret += (20.0 * sin($lng * M_PI) + 40.0 * sin($lng / 3.0 * M_PI)) * 2.0 / 3.0;
  319. $ret += (150.0 * sin($lng / 12.0 * M_PI) + 300.0 * sin($lng / 30.0 * M_PI)) * 2.0 / 3.0;
  320. return $ret;
  321. }
  322. private static function outOfChina($lng, $lat)
  323. {
  324. if ($lng < 72.004 || $lng > 137.8347) {
  325. return true;
  326. }
  327. if ($lat < 0.8293 || $lat > 55.8271) {
  328. return true;
  329. }
  330. return false;
  331. }
  332. }