2024-07-13

【網頁程式】土砲打造 -- 依數個地點距離由近至遠規劃行程導航

參考資料 ----


當我們要前往數個地點,想依距離由近至遠排序,規劃開車行程,可以請 Google Cloud 服務,不過這是要收費的。

行程含目的地的停靠點在 10 個點以下,是一種費率,11(含)個點以上則會收較高的費用。

本筆記是自行計算及排序停靠點(注意,還是要收費的)。


您可以服務全開,不過我只使用其中 2 項 -- 取得地理位置(經緯度)導航







建立 GMap 類別
  1.  
  2. <?php
  3. date_default_timezone_set('Asia/Taipei');
  4. // 用來查詢地址的經、緯度
  5. class GMap {
  6. function getPageData($url) {
  7. $ch = curl_init();
  8. curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0);
  9. curl_setopt($ch, CURLOPT_HTTPHEADER, array('Expect: '));
  10. curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
  11. curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
  12. curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
  13. curl_setopt($ch, CURLOPT_URL, $url);
  14. curl_setopt($ch, CURLOPT_NOBODY, false);
  15. curl_setopt($ch, CURLOPT_FILETIME, true);
  16. curl_setopt($ch, CURLOPT_REFERER, $url);
  17. curl_setopt($ch, CURLOPT_BINARYTRANSFER, true);
  18. curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
  19. curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 4);
  20. curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
  21. curl_setopt($ch, CURLOPT_TIMEOUT, 10);
  22. curl_setopt($ch, CURLOPT_USERAGENT, "Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0)");
  23. $result['data'] = curl_exec($ch);
  24. curl_close($ch);
  25. return $result;
  26. }
  27. // 初始化 apikey 金鑰
  28. function __construct() {
  29. $this->apikey = '向 Google map 申請 apikey';
  30. }
  31.  
  32.  
  33. // 從 google map 取得地址經緯度
  34. public function getLngLat($addr='',$sDebug='') {
  35. $apikey = $this->apikey;
  36. $url = "https://maps.googleapis.com/maps/api/geocode/json?address=$addr&key=$apikey";
  37. $geocode = $this->getPageData($url);
  38. if($sDebug=='debug')
  39. var_dump($geocode);
  40.  
  41. if(isset($geocode['data'])) {
  42. if(!$geocode["data"])
  43. // 當 Google map 解析不了時,回應 經/緯度值都是 -1 的虛擬經緯度
  44. $geocode = '{"results":[{"geometry":{"location":{"lat":-1,"lng":-1}}}]}';
  45. else
  46. $geocode = $geocode['data'];
  47. } else
  48. // 當 Google map 解析不了時,回應 經/緯度值都是 -1 的虛擬經緯度
  49. $geocode = '{"results":[{"geometry":{"location":{"lat":-1,"lng":-1}}}]}';
  50.  
  51. $output = json_decode($geocode);
  52. $latitude = $output->results[0]->geometry->location->lat;
  53. $longitude = $output->results[0]->geometry->location->lng;
  54. return array('lat'=>$latitude,'lng'=>$longitude);
  55. }
  56. }
  57. ?>
  58.  


getroute.php,用來依次計算起點至每個目的地的距離,取最近的地址做為下個停靠點,並再以這地址做為下次計算的起點
  1.  
  2. <?php
  3. // 切換至 https
  4. if (empty($_SERVER['HTTPS']) || $_SERVER['HTTPS'] === "off")
  5. {
  6. $location = 'https://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
  7. header('HTTP/1.1 301 Moved Permanently');
  8. header('Location: ' . $location);
  9. exit;
  10. }
  11.  
  12. include('gmap.php');
  13.  
  14. class MyDest {
  15. public $addr;
  16. public $len = 0;
  17. public $lng = -1;
  18. public $lat = -1;
  19. function __construct($aa) {
  20. $this->addr = $aa;
  21. }
  22. }
  23.  
  24.  
  25. if(isset($_GET['lat'])) {
  26. // 接收到起點 經/緯 度
  27. // $sLat = 22.612058525444247; // 測試用
  28. // $sLng = 120.3142669919977; // 測試用
  29. $sLat = $_GET['lat'];
  30. $sLng = $_GET['lng'];
  31.  
  32. $aDest = array(); // 存放上傳的目的地
  33. $aSort = array(); // 排序後的目的地
  34.  
  35. $gmap = new GMap();
  36. if(isset($_GET['dest1'])) {
  37. $obj = new MyDest($_GET['dest1']);
  38. $fLngLat = $gmap->getLngLat($obj->addr); // 上車地點經緯度
  39. $obj->lat = $fLngLat["lat"]; // 緯度
  40. $obj->lng = $fLngLat["lng"]; // 經度
  41. array_push($aDest, $obj);
  42. }
  43. if(isset($_GET['dest2'])) {
  44. $obj = new MyDest($_GET['dest2']);
  45. $fLngLat = $gmap->getLngLat($obj->addr); // 上車地點經緯度
  46. $obj->lat = $fLngLat["lat"]; // 緯度
  47. $obj->lng = $fLngLat["lng"]; // 經度
  48. array_push($aDest, $obj);
  49. }
  50. if(isset($_GET['dest3'])) {
  51. $obj = new MyDest($_GET['dest3']);
  52. $fLngLat = $gmap->getLngLat($obj->addr); // 上車地點經緯度
  53. $obj->lat = $fLngLat["lat"]; // 緯度
  54. $obj->lng = $fLngLat["lng"]; // 經度
  55. array_push($aDest, $obj);
  56. }
  57. if(isset($_GET['dest4'])) {
  58. $obj = new MyDest($_GET['dest4']);
  59. $fLngLat = $gmap->getLngLat($obj->addr); // 上車地點經緯度
  60. $obj->lat = $fLngLat["lat"]; // 緯度
  61. $obj->lng = $fLngLat["lng"]; // 經度
  62. array_push($aDest, $obj);
  63. }
  64. if(isset($_GET['dest5'])) {
  65. $obj = new MyDest($_GET['dest5']);
  66. $fLngLat = $gmap->getLngLat($obj->addr); // 上車地點經緯度
  67. $obj->lat = $fLngLat["lat"]; // 緯度
  68. $obj->lng = $fLngLat["lng"]; // 經度
  69. array_push($aDest, $obj);
  70. }
  71.  
  72. while(count($aDest)>0) {
  73. foreach($aDest as &$val) { // 注意:$val 前面有 &
  74. // 計算【各目的地】距離【起點】的長度
  75. $url = "https://maps.googleapis.com/maps/api/directions/json?origin=$sLat,$sLng&destination=".urlencode($val->addr)."&key=$apiKey";
  76. // 目的地 destination 參數也可以是經緯度
  77. $url = "https://maps.googleapis.com/maps/api/directions/json?origin=$sLat,$sLng&destination=".urlencode($val->lat).",".urlencode($val->lng)."&key=$apiKey";
  78. // 若希望路線避開上高速公路, 則可加 avoid=highways 參數
  79. $url = "https://maps.googleapis.com/maps/api/directions/json?origin=$sLat,$sLng&destination=".urlencode($val->addr)."&avoid=highways&key=$apiKey";
  80. // 此外, 還可避開 ----
  81. // tolls:避開收費路段
  82. // highways:避開高速公路
  83. // ferries:避開渡輪
  84. // indoor:避開室內路線(主要用於步行路線)
  85. // 初始化 cURL
  86. $ch = curl_init();
  87. curl_setopt($ch, CURLOPT_URL, $url);
  88. curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
  89. $response = curl_exec($ch);
  90. curl_close($ch);
  91. // 解析 json 響應
  92. $data = json_decode($response, true);
  93. if ($data['status']=='OK') {
  94. $val->len = $data['routes'][0]['legs'][0]['distance']['value'];
  95. } else {
  96. $val->len = -1;
  97. }
  98. unset($val);
  99. }
  100.  
  101. // 使用 usort 函數排序陣列,根據 $a 屬性值從小到大排序
  102. usort($aDest, function($a, $b) {
  103. return $a->len <=> $b->len;
  104. });
  105. // 排序後的第一個物件就是 距離最短的地址
  106. $minObj = $aDest[0];
  107. // print_r($aDest);
  108. array_push($aSort, ['addr' => ($minObj->addr)]);
  109. // 這個地址做為起點
  110. $sLat = $minObj->lat;
  111. $sLng = $minObj->lng;
  112. array_splice($aDest, 0, 1);
  113. }
  114. echo json_encode($aSort); // 回傳依距離排序後的停靠點及目的地
  115. }
  116.  
  117. ?>
  118.  


navi.php,將排好的順序丟給 GoogleMap 導航。
  1.  
  2. <?php
  3. // 切換至 https
  4. if(empty($_SERVER['HTTPS']) || $_SERVER['HTTPS'] === "off")
  5. {
  6. $location = 'https://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
  7. header('HTTP/1.1 301 Moved Permanently');
  8. header('Location: ' . $location);
  9. exit;
  10. }
  11. ?>
  12. <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
  13. <html style="height:100%;">
  14. <head>
  15. <meta charset="utf-8">
  16. <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.min.js"></script>
  17. </head>
  18. <body>
  19. <br />
  20. <button onclick="startNavigation()" style="font-size:40pt;">巡航</button><br />
  21. <span style="font-size:40pt;">提醒:在 Android Chrome 開啟本網頁</span><br />
  22. <br />
  23. <br />
  24. <script>
  25. function startNavigation() {
  26. var aRoute = [
  27. "高雄市苓雅區四維三路2號",
  28. "高雄市前鎮區中華五路789號",
  29. "高雄市鼓山區濱海二路1號",
  30. "高雄市左營區博愛二路777號",
  31. "高雄市左營區高鐵路107號"
  32. ];
  33.  
  34. if (navigator.geolocation) {
  35. navigator.geolocation.getCurrentPosition(function(position) {
  36. var latitude = position.coords.latitude; // 緯度
  37. var longitude = position.coords.longitude; // 經度
  38. var str = "https://您自己的網址/getroute.php?lat="+latitude+"&lng="+longitude+"&dest1="+aRoute[0]+"&dest2="+aRoute[1]+"&dest3="+aRoute[2]+"&dest4="+aRoute[3]+"&dest5="+aRoute[4];
  39. // alert('str = '+str);
  40. // return;
  41.  
  42. var destinations = new Array();
  43. $.getJSON(str, function(res) {
  44. // alert("res = "+res);
  45. // 這段是從 client 端向 server 請求查詢
  46. // server 回傳 json 型態的陣列
  47. // 陣列內的元素是 addr
  48. res.forEach(function(element) {
  49. destinations.push(element.addr);
  50. });
  51. var waypoints = destinations.map(function(destination) {
  52. // Google map 規定停靠點以【|】字元分隔
  53. // 將 5 個地點以【|】字元分隔
  54. return encodeURIComponent(destination);
  55. }).join("|");
  56. destination = waypoints.split('|').pop(); // 取得最後【|】右邊的字串做為目的地
  57. waypoints = waypoints.slice(0, waypoints.lastIndexOf('|')); // 取得最後【|】的左邊字串做為停靠點
  58.  
  59. // 構建導航 URL
  60. var mapsURL = "https://www.google.com/maps/dir/?api=1&origin=" + latitude + "," + longitude + "&destination=" + destination + "&waypoints=" + waypoints + "&travelmode=driving";
  61. // alert(mapsURL);
  62. // return;
  63.  
  64. // 跳轉到 Google Maps
  65. window.location.href = mapsURL;
  66. });
  67. // return;
  68.  
  69. }, function(error) {
  70. alert("無法取得地理位置: " + error.message);
  71. }
  72. );
  73. } else {
  74. alert("瀏覽器不支援地理定位功能。");
  75. }
  76. }
  77. </script>
  78. <br />
  79. <br />
  80. </body>
  81. </html>
  82.  
  83.  


排序後,程式呼叫 Android 手機上的 Google Map app 進行導航。

第一次執行這個網頁程式時,會詢問是否同意瀏覽器存取你的地理位置,選擇同意且記住您的選擇,之後執行就不會再跳出詢問視窗了。

測試 Google Chrome、Firefox、Microsoft Edge 這幾個主流瀏覽器
Chrome 效果最佳,直接啟動 Google Map,畢竟都是自家的產品;
Edge 雖可呼叫 Google Map,但詢問視窗一直開著,不會自動關閉
Firefox 最差,啟動了 Google Map,但要導航的地址全不見了!!



另一個有趣的發現:
如果只單純導航前往一個地點,Google Map 並不管目的地是否與您的行車方向順向,只要抵達目的地附近,Google Map 就認為任務完成了。ex:出發點是高雄市中山大學,目的地是高雄女中(雄女),雄女的對面是國軍英雄館,Google 導航只要帶您到國軍英雄館,導航就結束了。
但當設了多個停靠點時,Google Map 就會使命必達,規劃的路線即使繞一圈也會帶您順向抵達雄女。

然後...老人家又好奇了...這個網頁程式在 iPhone 上打開會是什麼結果?
答案是:在 Safari 直接 Deny!! 不給開

2025


相關筆記 ----






沒有留言:

張貼留言