使用 Place Autocomplete 和 Routes API 建構路線規劃工具

1. 總覽

無論是展開公路旅行、規劃日常通勤路線,還是要在熱鬧的城市中導航,從 A 點前往 B 點不僅僅是知道目的地,可靠的路線產生工具也至關重要。

有了 Google 地圖平台,您就能在應用程式中加入動態地圖、讓使用者透過自動完成功能快速輸入地點,以及在地圖上顯示路線。

本程式碼研究室會引導開發人員使用 Maps JavaScript APIPlace AutocompleteRoutes API 建構網路應用程式。您將透過可自訂的教學課程,瞭解如何整合多個 Google 地圖平台 API。

建構項目

本程式碼研究室將引導您使用 HTML、CSS、JavaScript 和 Node.js 後端,建構網頁應用程式。

路徑規劃工具網頁應用程式架構

路徑規劃工具網頁應用程式

課程內容

  • 如何啟用 Google 地圖平台 API
  • 如何將動態地圖整合至網頁應用程式
  • 如何整合 Place Autocomplete 服務
  • 如何透過 Routes API 要求路線
  • 如何在動態地圖上顯示路線
  • 如何建立地圖 ID
  • 如何在動態地圖中新增進階標記

軟硬體需求

程式碼範例

如需完整解決方案和逐步程式碼,請前往 GitHub。程式碼未包含必要的 Node 套件。執行程式碼前,請先安裝必要的依附元件。如要瞭解必要套件的詳細資料,請參閱 package.json 檔案(說明請見步驟 3)。

2. 設定專案並啟用 API

在啟用步驟中,您需要啟用 Maps JavaScript APIPlace AutocompleteRoutes API

設定 Google 地圖平台

如果您尚未建立 Google Cloud Platform 帳戶,以及啟用計費功能的專案,請參閱「開始使用 Google 地圖平台」指南,建立帳單帳戶和專案。

  1. Cloud 控制台中,按一下專案下拉式選單,然後選取要用於本程式碼研究室的專案。選取專案
  2. Maps API 程式庫頁面中,啟用本程式碼研究室所需的 Google 地圖平台 API。如要瞭解如何操作,請觀看這部影片或參閱這份文件
  3. 在 Cloud Console 的「憑證」頁面中產生 API 金鑰。你可以按照這部影片或這份文件中的步驟操作。所有 Google 地圖平台要求都需要 API 金鑰。

3. 設定 Node.js 專案

在本實驗室中,我們將使用 Node.js 從網頁收集起點和目的地,並透過 Routes API 要求路線。

假設您已安裝 Node.js,請建立用於執行這個專案的目錄:

$ mkdir ac_routes
$ cd ac_routes

在應用程式的目錄中初始化新的 Node.js 套件:

$ npm init

這個指令會提示您輸入多項資訊,例如應用程式名稱和版本。目前只要按下 RETURN 鍵,即可接受大部分設定的預設值。預設進入點為 index.js,您可以將其變更為主要檔案。在本實驗室中,主要檔案為 function/server.js(詳情請參閱步驟 6)。

此外,您也可以安裝偏好的架構和模組。本實驗室使用網路架構(Express) 和主體剖析器(body-parser)。詳情請參閱 package.json 檔案。

4. 建立動態地圖

Node.js 後端已就位,接下來要瞭解用戶端所需的步驟。

  • 為應用程式建立 HTML 網頁
  • 建立 CSS 檔案以設定樣式
  • 將 Google Maps JavaScript API 載入 HTML 網頁
  • 將 API 金鑰貼到指令碼標記中,驗證應用程式
  • 建立 JavaScript 檔案來處理應用程式功能

建立 HTML 網頁

  1. 在專案資料夾中建立新目錄(本例為 ac_routes)
     $ mkdir public
     $ cd public
    
  2. 在公開目錄中建立 index.html
  3. 將下列程式碼複製到 index.html
     <!DOCTYPE html>
     <html>
     <head>
       <title>GMP Autocomplete + Routes</title>
       <meta charset="utf-8">
       <link rel="stylesheet" type="text/css" href="style.css">
     </head>
     <body>
       <div class="container">
         <!-- Start of the container for map -->
         <div class="main">
           <div id="map"></div>
         </div>
         <!-- End of the container for map -->
       </div>
       </body>
     </html>
    

建立 CSS 檔案

  1. 在公開目錄中建立 style.css
  2. 將下列程式碼複製到 style.css:
     html, body {height: 100%;}
     body {
       background: #fff;
       font-family: "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif;
       font-style: normal;
       font-weight: normal;
       font-size:16px;
       line-height: 1.5;
       margin: 0;
       padding: 0;
     }
     .container {display:flex; width:90%; padding:100px 0; margin:0 auto;}
     .main {width:70%; height:800px;}
      #map {height:100%; border-radius:20px;}
    

載入 Maps JavaScript API

在本實驗室中,我們將使用動態程式庫匯入功能載入 Maps JavaScript API。詳情請參閱這裡

在 index.html 中,複製結尾 body 標記前的下列程式碼。將「YOUR_API_KEY」替換成您自己的 API 金鑰。

<script>(g=>{var h,a,k,p="The Google Maps JavaScript API",c="google",l="importLibrary",q="__ib__",m=document,b=window;b=b[c]||(b[c]={});var d=b.maps||(b.maps={}),r=new Set,e=new URLSearchParams,u=()=>h||(h=new Promise(async(f,n)=>{await (a=m.createElement("script"));e.set("libraries",[...r]+"");for(k in g)e.set(k.replace(/[A-Z]/g,t=>"_"+t[0].toLowerCase()),g[k]);e.set("callback",c+".maps."+q);a.src=`https://maps.${c}apis.com/maps/api/js?`+e;d[q]=f;a.onerror=()=>h=n(Error(p+" could not load."));a.nonce=m.querySelector("script[nonce]")?.nonce||"";m.head.append(a)}));d[l]?console.warn(p+" only loads once. Ignoring:",g):d[l]=(f,...n)=>r.add(f)&&u().then(()=>d[l](f,...n))})({key: "YOUR_API_KEY", v: "weekly"});</script>

建立 JavaScript 檔案

  1. 在公開目錄中建立 app.js
  2. 將下列程式碼複製到 app.js
     (function(){
       let map;
    
       async function initMap() {
           const { Map } = await google.maps.importLibrary('maps');
           map = new Map(document.getElementById('map'), {
               center: { lat: -34.397, lng: 150.644 },
               zoom: 8,
               mapId: 'DEMO_MAP_ID'
           });
       }
    
       initMap();
     }());
    

DEMO_MAP_ID 是可用於需要地圖 ID 的程式碼範例。這個 ID 不適用於正式版應用程式,也無法用於需要雲端樣式設定的功能。在本實驗室的後續階段,我們需要地圖 ID 才能使用進階標記。進一步瞭解如何為應用程式建立地圖 ID

在 index.html 中,將 app.js 連結至 body 結尾標記前,以及載入 Maps JavaScript API 的指令碼標記後。

<script type="text/JavaScript" src="app.js"></script>

完整程式碼範例

您可以在 GitHub:step1_createDynamicMap 取得目前為止的完整程式碼。

5. 輸入起點和目的地地址

  • 在 index.html 中新增兩個文字欄位,分別用於輸入起點和目的地
  • 匯入 Autocomplete 程式庫
  • 將 Autocomplete 服務繫結至來源和目的地文字欄位

新增文字欄位

在 index.html 中,將下列程式碼新增為 div 的第一個子項,類別為 container

<div class="aside">
  <div class="inputgroup">
    <label for="origin">Start</label>
    <input type="text" id="origin" name="origin" class="input-location" placeholder="Enter an address">
  </div>
  <div class="inputgroup">
    <label for="origin">End</label>
    <input type="text" id="destination" name="destination" class="input-location" placeholder="Enter an address">
  </div>
</div>

匯入並啟用自動完成功能

google.maps.places.Autocomplete 類別是小工具,可根據使用者輸入的文字提供地點預測結果。這個指令會附加至文字類型的輸入元素,並監聽該欄位中的文字輸入內容。預測結果清單會以下拉式清單的形式顯示,並在您輸入文字時更新。

在 app.js 中,於地圖初始化後加入下列程式碼:

let placeIds = [];
async function initPlace() {
  const { Autocomplete } = await google.maps.importLibrary('places');
  let autocomplete = [];
  let locationFields = Array.from(document.getElementsByClassName('input-location'));
  //Enable autocomplete for input fields
  locationFields.forEach((elem,i) => {
      autocomplete[i] = new Autocomplete(elem);
      google.maps.event.addListener(autocomplete[i],"place_changed", () => {
          let place = autocomplete[i].getPlace();
          if(Object.keys(place).length > 0){
              if (place.place_id){
                  placeIds[i] = place.place_id; //We use Place Id in this example
              } else {
                  placeIds.splice(i,1); //If no place is selected or no place is found, remove the previous value from the placeIds.
                  window.alert(`No details available for input: ${place.name}`);
                  return;
              }
          }
      });
  });
}
initPlace();

使用者從自動完成預測清單選取地點後,即可使用 getPlace() 方法擷取地點結果詳細資料。地點結果包含大量地點資訊,在本實驗室中,我們將使用 place_id 識別所選地點。地點 ID 可以用來辨識 Google 地點介面集資料庫和 Google 地圖中的特定地點。進一步瞭解地點 ID

新增相關樣式

在 style.css 中新增下列程式碼:

.aside {width:30%; padding:20px;}
.inputgroup {margin-bottom:30px;}
.aside label {display:block; padding:0 10px; margin-bottom:10px; font-size:18px; color:#666565;}
.aside input[type=text] {width:90%;padding:10px; font-size:16px; border:1px solid #e6e8e6; border-radius:10px;}

完整程式碼範例

您可以在 GitHub:step2_inputAddress 取得目前為止的完整程式碼。

6. 要求路線

  • 在 index.html 中新增「取得路線」按鈕,以啟動路線要求
  • 這個按鈕會觸發將來源和目的地資料傳送至 Node.js 服務
  • Node.js 服務會將要求傳送至 Routes API
  • API 回應會傳回用戶端以供顯示

設定出發地和目的地,並準備好動態地圖後,就可以取得路線。新一代的 Routes API 是效能最佳化的路線和距離矩陣服務,可解決上述問題。在本實驗室中,我們將使用 Node.js 從網頁收集起點和目的地,並透過 Routes API 要求路線。

在 index.html 中,於 div 的結尾標記前新增「Get a route」按鈕,並加上 aside 類別:

<div class="inputgroup">
  <button id="btn-getroute">Get a route</button>
</div>

在 style.css 中新增以下程式碼:

.aside button {padding:20px 30px; font-size:16px; border:none; border-radius:50px; background-color:#1a73e8; color:#fff;}

在 app.js 中,加入下列程式碼,將來源和目的地資料傳送至 Node.js 服務:

function requestRoute(){
  let btn = document.getElementById('btn-getroute');
  btn.addEventListener('click', () => {
    //In this example, we will extract the Place IDs from the Autocomplete response
    //and use the Place ID for origin and destination
    if(placeIds.length == 2){
        let reqBody = {
            "origin": {
                "placeId": placeIds[0]
            },
            "destination": {
                "placeId": placeIds[1]
            }
        }

        fetch("/request-route", {
            method: 'POST',
            body: JSON.stringify(reqBody),
            headers: {
                "Content-Type": "application/json"
            }
        }).then((response) => {
            return response.json();
        }).then((data) => {
            //Draw the route on the map
            //Details will be covered in next step
            renderRoutes(data);
        }).catch((error) => {
            console.log(error);
        });
    } else {
        window.alert('Location must be set');
        return;
    }
  });
}

requestRoute();

renderRoutes() 是用來在地圖上繪製路線的函式。我們會在下一個步驟中說明細節。

建立伺服器

在專案目錄(本例為 ac_routes) 中,建立名為 function 的新資料夾。在這個資料夾中,建立名為 server.js 的檔案。這個檔案是專案的進入點,會在設定 Node.js 專案時進行設定,並處理三項重要函式:

  1. 從網頁用戶端收集資料
  2. 傳送要求至 Routes API
  3. 將 API 回應傳回用戶端

將下列程式碼複製到 server.js。將「YOUR_API_KEY」替換成您自己的 API 金鑰。為提升 API 金鑰安全性,我們強烈建議您為後端使用不同的金鑰。請參閱安全性指南

const express = require('express');
const app = express();
const bodyParser = require('body-parser');

const port  = 8080;
const urlencodedParser = bodyParser.urlencoded({extended:true}); 

function main() {
  app.use('/', express.static('public'));
  app.use(urlencodedParser);
  app.use(express.json());

  app.post('/request-route', (req,res) => {    
    fetch("https://routes.googleapis.com/directions/v2:computeRoutes", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "X-Goog-Api-Key": "YOUR_API_KEY",
        "X-Goog-FieldMask": "*"
      },
      body: JSON.stringify(req.body)
    }).then((response) => {
      return response.json();
    }).then((data) => {
      if('error' in data){
        console.log(data.error);
      } else if(!data.hasOwnProperty("routes")){
        console.log("No route round");
      } else {
        res.end(JSON.stringify(data));
      }
    }).catch((error) => {
      console.log(error)
    });
  });

  app.listen(port, () => {
      console.log('App listening on port ${port}: ' + port);
      console.log('Press Ctrl+C to quit.');
  });
}

main();

如要進一步瞭解 Routes API,請參閱這篇文章

執行程式碼

在指令列中執行下列程式碼:

$ node function/server.js

開啟瀏覽器並前往 http://127.0.0.1:8080/index.html。您應該會看到應用程式頁面。到這個階段為止,API 回應會傳回給網路用戶端。請參閱下一個步驟,瞭解如何在 Google 地圖上顯示路線。

完整程式碼範例

您可以在 GitHub:step3_requestRoute 取得目前為止的完整程式碼

7. 在地圖上顯示路線

在上一個步驟中,我們在成功收到 Node.js 服務的回應時,會參照 renderRoutes()。現在,我們來加入實際程式碼,在地圖上顯示路線。

在 app.js 中新增下列程式碼:

let paths = [];
async function renderRoutes(data) {
  const { encoding } = await google.maps.importLibrary("geometry");
  let routes = data.routes;
  let decodedPaths = [];

  ///Display routes and markers
  routes.forEach((route,i) => {
      if(route.hasOwnProperty('polyline')){
        //Decode the encoded polyline
        decodedPaths.push(encoding.decodePath(route.polyline.encodedPolyline));

        //Draw polyline on the map
        for(let i = decodedPaths.length - 1; i >= 0; i--){
            let polyline = new google.maps.Polyline({
                map: map,
                path: decodedPaths[i],
                strokeColor: "#4285f4",
                strokeOpacity: 1,
                strokeWeight: 5
            });
            paths.push(polyline);
        }
        
        //Add markers for origin/destination
        addMarker(route.legs[0].startLocation.latLng,"A");
        addMarker(route.legs[0].endLocation.latLng,"B");
        //Set the viewport
        setViewport(route.viewport);
      } else {
        console.log("Route cannot be found");
      }
  });
}

Routes API 會以 encodedPolyline(預設) 或 geoJsonLinestring 格式傳回折線。在本實驗室中,我們使用 encodedPolyline 格式,並透過 Maps JavaScript 幾何圖形程式庫解碼

我們會使用 addMarker() 為起點和目的地新增進階標記。在 app.js 中新增下列程式碼:

let markers = [];
async function addMarker(pos,label){
  const { AdvancedMarkerElement } = await google.maps.importLibrary("marker");
  const { PinElement } = await google.maps.importLibrary("marker");
  const { LatLng } = await google.maps.importLibrary("core");
  let pinGlyph = new PinElement({
      glyphColor: "#fff",
      glyph: label
  });
  let marker = new AdvancedMarkerElement({
      position: new LatLng({lat:pos.latitude,lng:pos.longitude}),
      gmpDraggable: false,
      content: pinGlyph.element,
      map: map
  });
  markers.push(marker);
}

我們在這裡建立兩個進階標記,分別是起點的 A 和目的地的 B。進一步瞭解進階標記

接著,我們會使用 Routes API 提供的便利 viewport 資訊,將地圖檢視區塊置中於擷取的路線。在 app.js 中新增下列程式碼:

async function setViewport(viewPort) {
  const { LatLng } = await google.maps.importLibrary("core");
  const { LatLngBounds } = await google.maps.importLibrary("core");
  let sw = new LatLng({lat:viewPort.low.latitude,lng:viewPort.low.longitude});
  let ne = new LatLng({lat:viewPort.high.latitude,lng:viewPort.high.longitude});
  map.fitBounds(new LatLngBounds(sw,ne));
}

完整程式碼範例:您可以在 GitHub:step4_displayRoute 取得目前為止的完整程式碼。

8. 從地圖中移除元素

現在,我們要採取更進一步的行動。請先清除地圖,再繪製新標記和路線,以免畫面過於雜亂。

在 app.js 中,我們再新增一個函式:

function clearUIElem(obj,type) {
  if(obj.length > 0){
      if(type == 'advMarker'){
          obj.forEach(function(item){
              item.map = null;
          });
      } else {
          obj.forEach(function(item){
              item.setMap(null);
          });
      }
  }
}

renderRoutes() 開頭加入以下程式碼:

clearUIElem(paths,'polyline');

addMarker() 開頭加入以下程式碼:

clearUIElem(markers,'advMarker');

完整程式碼範例

您可以在 GitHub:step5_removeElements 取得目前為止的完整程式碼。

9. 恭喜

您已成功建構物體。

您學到的內容

  • 啟用 Google 地圖平台 API
  • 將 Google Maps JavaScript API 載入 HTML 網頁
  • 匯入 Maps JavaScript API 的 Places Library
  • 將 Place Autocomplete 服務繫結至文字欄位
  • 透過 Routes API 要求路線
  • 在動態地圖上顯示路線
  • 建立地圖 ID
  • 建立進階標記

瞭解詳情

你還想看到哪些程式碼研究室?

在地圖上以視覺化方式呈現資料 進一步瞭解如何自訂地圖樣式 在地圖中建構 3D 互動

如果想找的程式碼研究室未列於上方,請在這裡提出新的問題