使用 Google 地圖平台建構簡單的店家搜尋器 (JavaScript)

1. 事前準備

網站最常見的功能之一就是顯示 Google 地圖,用來標明一或多個商家、場所或其他有實體據點的實體。這些地圖的使用方式因地區而異,例如地點數量和變更頻率。

在這個程式碼研究室中,您查看的是最簡單的應用方式,也就是少數地點極少變更的情形,例如某間分店的店家搜尋器。在這種情況下,您可以採用相對較低的技術方法,而無需使用任何伺服器端程式。但是,您不該相信自己能夠發揮創意,也能運用 GeoJSON 資料格式,儲存和顯示地圖上的每個商店的任意資訊,以及自訂標記和整體地圖樣式。

最後,您還可以使用 Cloud Shell 來開發及代管店家搜尋器。儘管我們嚴格要求使用這項工具,但能讓您從任何執行網路瀏覽器的裝置開發店家搜尋器,並開放大眾使用。

489628918395c3d0.png

事前準備

  • HTML 和 JavaScript 的基本知識

要執行的步驟

  • 顯示地圖,其中包含一組店家位置和以 GeoJSON 格式儲存的資訊。
  • 自訂標記和地圖本身。
  • 按一下標記時,顯示商店的額外資訊。
  • 在網頁上新增 Place Autocomplete 搜尋列。
  • 識別使用者最近的起點位置。

2. 做好準備

在下一節的步驟 3 中,為這個程式碼研究室啟用下列三個 API:

  • Maps JavaScript API
  • Places API
  • Distance Matrix API

開始使用 Google 地圖平台

如果您未曾使用過 Google 地圖平台,請按照開始使用 Google 地圖平台指南或觀看 Google 地圖平台入門指南完成下列步驟:

  1. 建立帳單帳戶。
  2. 建立專案。
  3. 啟用 Google 地圖平台的 API 和 SDK (如上一節所示)。
  4. 產生 API 金鑰。

啟動 Cloud Shell

在這個程式碼研究室中,您可以使用 Cloud Shell,這是一個在 Google Cloud 中執行的指令列環境,可讓您存取在 Google Cloud 中運作的產品與資源,方便您從網路瀏覽器託管及執行專案。

如要透過 Cloud Console 啟用 Cloud Shell,請按一下 [啟用 Cloud Shell] 89665d8d348105cd.png (只需幾分鐘即可佈建並連線至環境)。

5f504766b9b3be17.png

這個選項可能會在瀏覽器開啟後,在瀏覽器下半部開啟新的殼層。

d3bb67d514893d1f.png

連線至 Cloud Shell 之後,您應該已經完成驗證,且專案已設為您在設定時選取的專案 ID。

$ gcloud auth list
Credentialed Accounts:
ACTIVE  ACCOUNT
  *     <myaccount>@<mydomain>.com
$ gcloud config list project
[core]
project = <YOUR_PROJECT_ID>

如果因為某些原因而無法設定專案,請執行下列指令:

$ gcloud config set project <YOUR_PROJECT_ID>

3. 「地圖,大家好!」

開始使用地圖進行開發

在 Cloud Shell 中,您會先建立一個 HTML 頁面,做為程式碼研究室的其餘部分。

  1. 在 Cloud Shell 工具列中,按一下「Launch Editor」(啟動編輯器) 996514928389de40.png 在新分頁中開啟新分頁編輯器。

這項網頁式程式碼編輯器可讓您輕鬆在 Cloud Shell 中編輯檔案。

2017 年 4 月 19 日上午 10.22.48 上午.

  1. 在程式碼編輯器中按一下 [檔案] > [新增資料夾],為應用程式建立新的 store-locator 目錄。

新資料夾.png

  1. 將新的資料夾命名為 store-locator

接下來,請建立一個包含地圖的網頁。

  1. 在名為 index.htmlstore-locator 目錄中建立檔案。

3c257603da5ab524.png

  1. 將下列內容放入 index.html 檔案中:

<html>

<head>
    <title>Store Locator</title>
    <style>
        #map {
            height: 100%;
        }
        
        html,
        body {
            height: 100%;
            margin: 0;
            padding: 0;
        }
    </style>
</head>

<body>
    <!-- The div to hold the map -->
    <div id="map"></div>

    <script src="app.js"></script>
    <script async defer src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&libraries=places&callback=initMap&solution_channel=GMP_codelabs_simplestorelocator_v1_a">
    </script>
</body>

</html>

這是顯示地圖的 HTML 網頁。其中會包含一些 CSS 來確保地圖在視覺上佔滿整個頁面、一個包含地圖的 <div> 標記,以及兩組 <script> 標記。第一個指令碼標記會載入名為「app.js」的 JavaScript 檔案,其中包含所有 JavaScript 程式碼。第二個指令碼標記會載入 API 金鑰,包括您稍後將用來加入自動完成功能的 Places Library,以及指定載入 Maps JavaScript API 後執行的 JavaScript 函式名稱,也就是 initMap

  1. 將程式碼片段中的文字 YOUR_API_KEY 替換成您在此程式碼研究室中產生的 API 金鑰。
  2. 最後,使用下列程式碼建立另一個名為 app.js 的檔案:

app.js

function initMap() {
   // Create the map.
    const map = new google.maps.Map(document.getElementById('map'), {
        zoom: 7,
        center: { lat: 52.632469, lng: -1.689423 },
    });

}

這就是建立地圖所需的基本程式碼。您會將參照傳遞至 <div> 標記以保存地圖,並指定中心和縮放等級。

如要測試這個應用程式,您可以在 Cloud Shell 中執行簡單的 Python HTTP 伺服器。

  1. 前往 Cloud Shell,然後輸入以下內容:
$ cd store-locator
$ python3 -m http.server 8080

您會看到幾行記錄輸出內容,表示您確實是在 Cloud Shell 中執行簡單的 HTTP 伺服器,且網頁應用程式會監聽 localhost 通訊埠 8080。

  1. 在 Cloud Console 工具列中按一下 [網頁預覽]95e419ae763a1d48.png,然後選取 [透過以下埠預覽:8080],即可在這個應用程式中開啟網路瀏覽器分頁。

47b06e5169eb5add.png

bdab1f021a3b91d5.png

只要按一下這個選單項目,即可在網路瀏覽器中開啟新分頁,其中含有簡單的 Python HTTP 伺服器提供的 HTML 內容。如果一切順利,您應該會看到以倫敦倫敦為中心的地圖。

如要停止簡易型 HTTP 伺服器,請在 Cloud Shell 中按下 Control+C

4. 使用 GeoJSON 填入地圖

現在,請查看商店的資料。GeoJSON 是一種資料格式,代表簡單的地理特徵,例如地圖上的點、線條或多邊形。這些功能也可以包含任意資料。這讓 GeoJSON 成為展示商店的絕佳選擇,基本上就是在地圖上以更多其他資料 (例如商店的名稱、營業時間和電話號碼) 為重點。最重要的是,GeoJSON 支援「Google 地圖」的一級支援,因此您可以將 GeoJSON 文件傳送至 Google 地圖,這樣它就會正確顯示在地圖上。

  1. 建立名為 stores.json 的新檔案並貼入下列程式碼:

stores.json

{
    "type": "FeatureCollection",
    "features": [{
            "geometry": {
                "type": "Point",
                "coordinates": [-0.1428115,
                    51.5125168
                ]
            },
            "type": "Feature",
            "properties": {
                "category": "patisserie",
                "hours": "10am - 6pm",
                "description": "Modern twists on classic pastries. We're part of a larger chain of patisseries and cafes.",
                "name": "Josie's Patisserie Mayfair",
                "phone": "+44 20 1234 5678",
                "storeid": "01"
            }
        },
        {
            "geometry": {
                "type": "Point",
                "coordinates": [-2.579623,
                    51.452251
                ]
            },
            "type": "Feature",
            "properties": {
                "category": "patisserie",
                "hours": "10am - 6pm",
                "description": "Come and try our award-winning cakes and pastries. We're part of a larger chain of patisseries and cafes.",
                "name": "Josie's Patisserie Bristol",
                "phone": "+44 117 121 2121",
                "storeid": "02"
            }
        },
        {
            "geometry": {
                "type": "Point",
                "coordinates": [
                    1.273459,
                    52.638072
                ]
            },
            "type": "Feature",
            "properties": {
                "category": "patisserie",
                "hours": "10am - 6pm",
                "description": "Whatever the occasion, whether it's a birthday or a wedding, Josie's Patisserie has the perfect treat for you. We're part of a larger chain of patisseries and cafes.",
                "name": "Josie's Patisserie Norwich",
                "phone": "+44 1603 123456",
                "storeid": "03"
            }
        },
        {
            "geometry": {
                "type": "Point",
                "coordinates": [-1.9912838,
                    50.8000418
                ]
            },
            "type": "Feature",
            "properties": {
                "category": "patisserie",
                "hours": "10am - 6pm",
                "description": "A gourmet patisserie that will delight your senses. We're part of a larger chain of patisseries and cafes.",
                "name": "Josie's Patisserie Wimborne",
                "phone": "+44 1202 343434",
                "storeid": "04"
            }
        },
        {
            "geometry": {
                "type": "Point",
                "coordinates": [-2.985933,
                    53.408899
                ]
            },
            "type": "Feature",
            "properties": {
                "category": "patisserie",
                "hours": "10am - 6pm",
                "description": "Spoil yourself or someone special with our classic pastries. We're part of a larger chain of patisseries and cafes.",
                "name": "Josie's Patisserie Liverpool",
                "phone": "+44 151 444 4444",
                "storeid": "05"
            }
        },
        {
            "geometry": {
                "type": "Point",
                "coordinates": [-1.689423,
                    52.632469
                ]
            },
            "type": "Feature",
            "properties": {
                "category": "patisserie",
                "hours": "10am - 6pm",
                "description": "Come and feast your eyes and tastebuds on our delicious pastries and cakes. We're part of a larger chain of patisseries and cafes.",
                "name": "Josie's Patisserie Tamworth",
                "phone": "+44 5555 55555",
                "storeid": "06"
            }
        },
        {
            "geometry": {
                "type": "Point",
                "coordinates": [-3.155305,
                    51.479756
                ]
            },
            "type": "Feature",
            "properties": {
                "category": "patisserie",
                "hours": "10am - 6pm",
                "description": "Josie's Patisserie is family-owned, and our delectable pastries, cakes, and great coffee are renowed. We're part of a larger chain of patisseries and cafes.",
                "name": "Josie's Patisserie Cardiff",
                "phone": "+44 29 6666 6666",
                "storeid": "07"
            }
        },
        {
            "geometry": {
                "type": "Point",
                "coordinates": [-0.725019,
                    52.668891
                ]
            },
            "type": "Feature",
            "properties": {
                "category": "cafe",
                "hours": "8am - 9:30pm",
                "description": "Oakham's favorite spot for fresh coffee and delicious cakes. We're part of a larger chain of patisseries and cafes.",
                "name": "Josie's Cafe Oakham",
                "phone": "+44 7777 777777",
                "storeid": "08"
            }
        },
        {
            "geometry": {
                "type": "Point",
                "coordinates": [-2.477653,
                    53.735405
                ]
            },
            "type": "Feature",
            "properties": {
                "category": "cafe",
                "hours": "8am - 9:30pm",
                "description": "Enjoy freshly brewed coffe, and home baked cakes in our homely cafe. We're part of a larger chain of patisseries and cafes.",
                "name": "Josie's Cafe Blackburn",
                "phone": "+44 8888 88888",
                "storeid": "09"
            }
        },
        {
            "geometry": {
                "type": "Point",
                "coordinates": [-0.211363,
                    51.108966
                ]
            },
            "type": "Feature",
            "properties": {
                "category": "cafe",
                "hours": "8am - 9:30pm",
                "description": "A delicious array of pastries with many flavours, and fresh coffee in an snug cafe. We're part of a larger chain of patisseries and cafes.",
                "name": "Josie's Cafe Crawley",
                "phone": "+44 1010 101010",
                "storeid": "10"
            }
        },
        {
            "geometry": {
                "type": "Point",
                "coordinates": [-0.123559,
                    50.832679
                ]
            },
            "type": "Feature",
            "properties": {
                "category": "cafe",
                "hours": "8am - 9:30pm",
                "description": "Grab a freshly brewed coffee, a decadent cake and relax in our idyllic cafe. We're part of a larger chain of patisseries and cafes.",
                "name": "Josie's Cafe Brighton",
                "phone": "+44 1313 131313",
                "storeid": "11"
            }
        },
        {
            "geometry": {
                "type": "Point",
                "coordinates": [-3.319575,
                    52.517827
                ]
            },
            "type": "Feature",
            "properties": {
                "category": "cafe",
                "hours": "8am - 9:30pm",
                "description": "Come in and unwind at this idyllic cafe with fresh coffee and home made cakes. We're part of a larger chain of patisseries and cafes.",
                "name": "Josie's Cafe Newtown",
                "phone": "+44 1414 141414",
                "storeid": "12"
            }
        },
        {
            "geometry": {
                "type": "Point",
                "coordinates": [
                    1.158167,
                    52.071634
                ]
            },
            "type": "Feature",
            "properties": {
                "category": "cafe",
                "hours": "8am - 9:30pm",
                "description": "Fresh coffee and delicious cakes in an snug cafe. We're part of a larger chain of patisseries and cafes.",
                "name": "Josie's Cafe Ipswich",
                "phone": "+44 1717 17171",
                "storeid": "13"
            }
        }
    ]
}

雖然資料相當龐大,但是一旦在使用,您就會發現每家商店的結構是一樣的。每間商店皆以 GeoJSON Point 及其座標和額外金鑰 (properties) 底下的其他資料表示。有趣的是,GeoJSON 允許在 properties 鍵下加入任意命名的鍵。在這個程式碼研究室中,這些鍵為 categoryhoursdescriptionnamephone

  1. 現在請編輯 app.js,使其可將 stores.js 中的 GeoJSON 載入您的地圖。

app.js

function initMap() {
  // Create the map.
  const map = new google.maps.Map(document.getElementById('map'), {
    zoom: 7,
    center: {lat: 52.632469, lng: -1.689423},
  });

  // Load the stores GeoJSON onto the map.
  map.data.loadGeoJson('stores.json', {idPropertyName: 'storeid'});

  const apiKey = 'YOUR_API_KEY';
  const infoWindow = new google.maps.InfoWindow();

  // Show the information for a store when its marker is clicked.
  map.data.addListener('click', (event) => {
    const category = event.feature.getProperty('category');
    const name = event.feature.getProperty('name');
    const description = event.feature.getProperty('description');
    const hours = event.feature.getProperty('hours');
    const phone = event.feature.getProperty('phone');
    const position = event.feature.getGeometry().get();
    const content = `
      <h2>${name}</h2><p>${description}</p>
      <p><b>Open:</b> ${hours}<br/><b>Phone:</b> ${phone}</p>
    `;

    infoWindow.setContent(content);
    infoWindow.setPosition(position);
    infoWindow.setOptions({pixelOffset: new google.maps.Size(0, -30)});
    infoWindow.open(map);
  });
}

在程式碼範例中,呼叫 loadGeoJson 並傳送 JSON 檔案名稱,即可將 GeoJSON 載入地圖。此外,您定義了每次按下標記時要執行的函式。然後,函式可以存取標記按一下之商店的其他資料,並使用顯示的資訊視窗中的資訊。如要測試這個應用程式,您可以使用與之前相同的指令執行簡單的 Python HTTP 伺服器。

  1. 返回 Cloud Shell 並輸入以下內容:
$ python3 -m http.server 8080
  1. 依序點選 [Web Preview] (網頁預覽)95e419ae763a1d48.png > [Preview onport 8080] (在通訊埠 8080 上預覽),您應該會看到含有完整標記的地圖,您可以點選這些標記,查看各商店的詳細資料 (如以下範例所示)。進度!

c4507f7d3ea18439.png

5. 自訂地圖

就快大功告成了。您的地圖包含所有商店標記,並在使用者點選時顯示額外資訊。不過,「Google 地圖」中還有許多其他地圖。太棒了!提供自訂地圖樣式、標記、標誌和街景服務圖片,讓內容更豐富。

以下是新版的 app.js,並新增自訂樣式:

app.js

const mapStyle = [{
  'featureType': 'administrative',
  'elementType': 'all',
  'stylers': [{
    'visibility': 'on',
  },
  {
    'lightness': 33,
  },
  ],
},
{
  'featureType': 'landscape',
  'elementType': 'all',
  'stylers': [{
    'color': '#f2e5d4',
  }],
},
{
  'featureType': 'poi.park',
  'elementType': 'geometry',
  'stylers': [{
    'color': '#c5dac6',
  }],
},
{
  'featureType': 'poi.park',
  'elementType': 'labels',
  'stylers': [{
    'visibility': 'on',
  },
  {
    'lightness': 20,
  },
  ],
},
{
  'featureType': 'road',
  'elementType': 'all',
  'stylers': [{
    'lightness': 20,
  }],
},
{
  'featureType': 'road.highway',
  'elementType': 'geometry',
  'stylers': [{
    'color': '#c5c6c6',
  }],
},
{
  'featureType': 'road.arterial',
  'elementType': 'geometry',
  'stylers': [{
    'color': '#e4d7c6',
  }],
},
{
  'featureType': 'road.local',
  'elementType': 'geometry',
  'stylers': [{
    'color': '#fbfaf7',
  }],
},
{
  'featureType': 'water',
  'elementType': 'all',
  'stylers': [{
    'visibility': 'on',
  },
  {
    'color': '#acbcc9',
  },
  ],
},
];

function initMap() {
  // Create the map.
  const map = new google.maps.Map(document.getElementById('map'), {
    zoom: 7,
    center: {lat: 52.632469, lng: -1.689423},
    styles: mapStyle,
  });

  // Load the stores GeoJSON onto the map.
  map.data.loadGeoJson('stores.json', {idPropertyName: 'storeid'});

  // Define the custom marker icons, using the store's "category".
  map.data.setStyle((feature) => {
    return {
      icon: {
        url: `img/icon_${feature.getProperty('category')}.png`,
        scaledSize: new google.maps.Size(64, 64),
      },
    };
  });

  const apiKey = 'YOUR_API_KEY';
  const infoWindow = new google.maps.InfoWindow();

  // Show the information for a store when its marker is clicked.
  map.data.addListener('click', (event) => {
    const category = event.feature.getProperty('category');
    const name = event.feature.getProperty('name');
    const description = event.feature.getProperty('description');
    const hours = event.feature.getProperty('hours');
    const phone = event.feature.getProperty('phone');
    const position = event.feature.getGeometry().get();
    const content = `
      <img style="float:left; width:200px; margin-top:30px" src="img/logo_${category}.png">
      <div style="margin-left:220px; margin-bottom:20px;">
        <h2>${name}</h2><p>${description}</p>
        <p><b>Open:</b> ${hours}<br/><b>Phone:</b> ${phone}</p>
        <p><img src="https://maps.googleapis.com/maps/api/streetview?size=350x120&location=${position.lat()},${position.lng()}&key=${apiKey}&solution_channel=GMP_codelabs_simplestorelocator_v1_a"></p>
      </div>
      `;

    infoWindow.setContent(content);
    infoWindow.setPosition(position);
    infoWindow.setOptions({pixelOffset: new google.maps.Size(0, -30)});
    infoWindow.open(map);
  });

}

以下是您新增的內容:

  • mapStyle 變數包含設定地圖樣式的所有資訊。(如有需要,您甚至可以建立自己的樣式)。
  • 使用 map.data.setStyle 方法,您套用自訂標記,與 GeoJSON 中每個 category 的標記不同。
  • 您已修改 content 變數,加入標誌 (再次使用 GeoJSON 中的 category) 和商店位置的街景服務圖片。

在部署之前,您必須完成幾個步驟:

  1. 設定 apiKey 變數的正確值。請將 app.js 中的 'YOUR_API_KEY' 字串替換成更早建立的 API 金鑰 (也就是您貼到 index.html 中的相同字串,並保留引號不變)。
  2. 在 Cloud Shell 中執行下列指令,以下載標記和標誌圖形。確認您在 store-locator 目錄中。使用 Control+C 可停止簡易型 HTTP 伺服器 (如果伺服器正在執行)。
$ mkdir -p img; cd img
$ wget https://github.com/googlecodelabs/google-maps-simple-store-locator/raw/master/src/img/icon_cafe.png
$ wget https://github.com/googlecodelabs/google-maps-simple-store-locator/raw/master/src/img/icon_patisserie.png
$ wget https://github.com/googlecodelabs/google-maps-simple-store-locator/raw/master/src/img/logo_cafe.png
$ wget https://github.com/googlecodelabs/google-maps-simple-store-locator/raw/master/src/img/logo_patisserie.png
  1. 執行下列指令來預覽已完成的店家搜尋器:
$ python3 -m http.server 8080

重新載入預覽時,您會看到下列地圖,其中包含自訂樣式、自訂標記圖片、改善資訊視窗格式,以及每個地點的街景服務圖片:

3d8d13da126021dd.png

6. 取得使用者輸入內容

店家搜尋器的使用者通常都想知道離自己最近的商店,或是預計展開旅程的地址。新增 Place Autocomplete 搜尋列,方便使用者輕鬆輸入起始地址。「地點自動完成」功能與其他 Google 搜尋列中的功能類似,就像「自動完成」功能一樣,不過預測都是 Google 地圖平台中所有的「地點」功能。

  1. 返回編輯 index.html,為「自動完成」搜尋列及相關側邊面板新增樣式。貼上舊程式碼後,別忘了替換您的 API 金鑰。

<html>

<head>
  <title>Store Locator</title>
  <style>
    #map {
      height: 100%;
    }
    
    html,
    body {
      height: 100%;
      margin: 0;
      padding: 0;
    }

    /* Styling for Autocomplete search bar */
    #pac-card {
      background-color: #fff;
      border-radius: 2px 0 0 2px;
      box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
      box-sizing: border-box;
      font-family: Roboto;
      margin: 10px 10px 0 0;
      -moz-box-sizing: border-box;
      outline: none;
    }
    
    #pac-container {
      padding-top: 12px;
      padding-bottom: 12px;
      margin-right: 12px;
    }
    
    #pac-input {
      background-color: #fff;
      font-family: Roboto;
      font-size: 15px;
      font-weight: 300;
      margin-left: 12px;
      padding: 0 11px 0 13px;
      text-overflow: ellipsis;
      width: 400px;
    }
    
    #pac-input:focus {
      border-color: #4d90fe;
    }
    
    #title {
      color: #fff;
      background-color: #acbcc9;
      font-size: 18px;
      font-weight: 400;
      padding: 6px 12px;
    }
    
    .hidden {
      display: none;
    }

    /* Styling for an info pane that slides out from the left. 
     * Hidden by default. */
    #panel {
      height: 100%;
      width: null;
      background-color: white;
      position: fixed;
      z-index: 1;
      overflow-x: hidden;
      transition: all .2s ease-out;
    }
    
    .open {
      width: 250px;
    }
    
    .place {
      font-family: 'open sans', arial, sans-serif;
      font-size: 1.2em;
      font-weight: 500;
      margin-block-end: 0px;
      padding-left: 18px;
      padding-right: 18px;
    }
    
    .distanceText {
      color: silver;
      font-family: 'open sans', arial, sans-serif;
      font-size: 1em;
      font-weight: 400;
      margin-block-start: 0.25em;
      padding-left: 18px;
      padding-right: 18px;
    }
  </style>
</head>

<body>
  <!-- The div to hold the map -->
  <div id="map"></div>

  <script src="app.js"></script>
  <script async defer src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&libraries=places&callback=initMap&solution_channel=GMP_codelabs_simplestorelocator_v1_a">
  </script>
</body>

</html>

「自動完成」搜尋列和投影片面板一開始會隱藏,直到需要為止。

  1. 現在,只要在 app.jsinitMap 函式結尾加入自動完成小工具,就跟在大括號前方一樣。

app.js

  // Build and add the search bar
  const card = document.createElement('div');
  const titleBar = document.createElement('div');
  const title = document.createElement('div');
  const container = document.createElement('div');
  const input = document.createElement('input');
  const options = {
    types: ['address'],
    componentRestrictions: {country: 'gb'},
  };

  card.setAttribute('id', 'pac-card');
  title.setAttribute('id', 'title');
  title.textContent = 'Find the nearest store';
  titleBar.appendChild(title);
  container.setAttribute('id', 'pac-container');
  input.setAttribute('id', 'pac-input');
  input.setAttribute('type', 'text');
  input.setAttribute('placeholder', 'Enter an address');
  container.appendChild(input);
  card.appendChild(titleBar);
  card.appendChild(container);
  map.controls[google.maps.ControlPosition.TOP_RIGHT].push(card);

  // Make the search bar into a Places Autocomplete search bar and select
  // which detail fields should be returned about the place that
  // the user selects from the suggestions.
  const autocomplete = new google.maps.places.Autocomplete(input, options);

  autocomplete.setFields(
      ['address_components', 'geometry', 'name']);

這個程式碼只會將「自動完成」建議限制為傳回地址 (因為「地點自動完成」可以比對建築物名稱和管理地點),並限制只傳回英國的地址。新增這些選用規格會減少使用者需要輸入的字元數,以縮小預測範圍以顯示他們需要的地址。然後將您建立的「自動完成」div 移到地圖的右上角,並指定回應中每個地點的相關欄位。

  1. 執行下列指令,重新啟動伺服器並重新整理預覽:
$ python3 -m http.server 8080

現在,您的地圖右上角應該會顯示「自動完成」小工具,並顯示與您輸入相符的英國地址。

5163f34a03910187.png

現在,您必須處理使用者從「自動完成」小工具中選取一個預測,並以該位置做為計算商店距離的基準。

  1. 將下列程式碼貼到您剛貼上的 app.jsinitMap 結尾。

app.js

 // Set the origin point when the user selects an address
  const originMarker = new google.maps.Marker({map: map});
  originMarker.setVisible(false);
  let originLocation = map.getCenter();

  autocomplete.addListener('place_changed', async () => {
    originMarker.setVisible(false);
    originLocation = map.getCenter();
    const place = autocomplete.getPlace();

    if (!place.geometry) {
      // User entered the name of a Place that was not suggested and
      // pressed the Enter key, or the Place Details request failed.
      window.alert('No address available for input: \'' + place.name + '\'');
      return;
    }

    // Recenter the map to the selected address
    originLocation = place.geometry.location;
    map.setCenter(originLocation);
    map.setZoom(9);
    console.log(place);

    originMarker.setPosition(originLocation);
    originMarker.setVisible(true);

    // Use the selected address as the origin to calculate distances
    // to each of the store locations
    const rankedStores = await calculateDistances(map.data, originLocation);
    showStoresList(map.data, rankedStores);

    return;
  });

程式碼會新增一個事件監聽器,如此一來,當使用者按一下其中一個建議時,該地圖就會根據所選的地址進行推算,並將起點設為計算距離的起點。您會在下一個步驟中導入距離計算。

7. 列出最近的商店

Directions API 的運作方式與在 Google 地圖應用程式中要求路線十分類似,只要輸入一個起點和一個目的地即可獲取兩者之間的路線。Distance Matrix API 可進一步採用這項概念,根據交通時間和距離,找出適用於多個可能出發地和多個可能目的地之間的最佳配對方式。在這種情況下,為了協助使用者找到與所選地址最近的商店,您必須提供一個來源和一系列商店位置做為目的地。

  1. app.js 中新增名為 calculateDistances 的函式。

app.js

async function calculateDistances(data, origin) {
  const stores = [];
  const destinations = [];

  // Build parallel arrays for the store IDs and destinations
  data.forEach((store) => {
    const storeNum = store.getProperty('storeid');
    const storeLoc = store.getGeometry().get();

    stores.push(storeNum);
    destinations.push(storeLoc);
  });

  // Retrieve the distances of each store from the origin
  // The returned list will be in the same order as the destinations list
  const service = new google.maps.DistanceMatrixService();
  const getDistanceMatrix =
    (service, parameters) => new Promise((resolve, reject) => {
      service.getDistanceMatrix(parameters, (response, status) => {
        if (status != google.maps.DistanceMatrixStatus.OK) {
          reject(response);
        } else {
          const distances = [];
          const results = response.rows[0].elements;
          for (let j = 0; j < results.length; j++) {
            const element = results[j];
            const distanceText = element.distance.text;
            const distanceVal = element.distance.value;
            const distanceObject = {
              storeid: stores[j],
              distanceText: distanceText,
              distanceVal: distanceVal,
            };
            distances.push(distanceObject);
          }

          resolve(distances);
        }
      });
    });

  const distancesList = await getDistanceMatrix(service, {
    origins: [origin],
    destinations: destinations,
    travelMode: 'DRIVING',
    unitSystem: google.maps.UnitSystem.METRIC,
  });

  distancesList.sort((first, second) => {
    return first.distanceVal - second.distanceVal;
  });

  return distancesList;
}

此函式會使用傳送至單一物件的 起點呼叫 Distance Matrix API,並將商店位置視為一個陣列陣列。接著,它會建構一個物件,用來儲存商店的 ID、以使用者可理解的字串表示的距離、以公尺為單位的距離 (以公尺為單位),而且會排序陣列。

使用者會期望看到一份清單,其中有從最近到最遠的訂購商店。使用從 calculateDistances 函式傳回的清單,為每個商店填入側邊面板清單,以便告知商店的顯示順序。

  1. app.js 中新增名為 showStoresList 的函式。

app.js

function showStoresList(data, stores) {
  if (stores.length == 0) {
    console.log('empty stores');
    return;
  }

  let panel = document.createElement('div');
  // If the panel already exists, use it. Else, create it and add to the page.
  if (document.getElementById('panel')) {
    panel = document.getElementById('panel');
    // If panel is already open, close it
    if (panel.classList.contains('open')) {
      panel.classList.remove('open');
    }
  } else {
    panel.setAttribute('id', 'panel');
    const body = document.body;
    body.insertBefore(panel, body.childNodes[0]);
  }


  // Clear the previous details
  while (panel.lastChild) {
    panel.removeChild(panel.lastChild);
  }

  stores.forEach((store) => {
    // Add store details with text formatting
    const name = document.createElement('p');
    name.classList.add('place');
    const currentStore = data.getFeatureById(store.storeid);
    name.textContent = currentStore.getProperty('name');
    panel.appendChild(name);
    const distanceText = document.createElement('p');
    distanceText.classList.add('distanceText');
    distanceText.textContent = store.distanceText;
    panel.appendChild(distanceText);
  });

  // Open the panel
  panel.classList.add('open');

  return;
}
  1. 執行下列指令,重新啟動伺服器並重新整理預覽。
$ python3 -m http.server 8080
  1. 最後,在「自動完成」搜尋列中輸入英國的地址,然後按一下其中一個建議即可。

地圖應以該地址為中心,側欄應會顯示商店位置,並與選取的地址相距。如下圖所示:

489628918395c3d0.png

8. 選擇性:代管您的網頁

到目前為止,只有當您積極執行 Python HTTP 伺服器時,您才能查看地圖。如要在使用中的 Cloud Shell 工作階段以外的地方查看地圖,或是想將自己的地圖網址分享給他人,請參考 Cloud Storage 來代管您的網頁。Cloud Storage 是一個線上檔案儲存網路服務,可用於在 Google 的基礎架構上儲存及存取資料。這項服務結合了 Google Cloud 的效能和擴充性,並提供先進的安全性與共用功能。此外,這項服務也提供免費方案,是代管簡易店家搜尋器的絕佳方法。

使用 Cloud Storage 時,檔案會儲存在值區中,與電腦上的目錄類似。如要代管網頁,您必須先建立值區。您必須為值區選擇專屬名稱,例如使用值區名稱做為值區名稱。

  1. 決定名稱後,請在 Cloud Shell 中執行下列指令:
$ gsutil mb gs://yourname-store-locator

gsutil 是用於與 Cloud Storage 互動的工具。mb 指令能以創意方式代表「建立值區」。如要進一步瞭解所有可用的指令 (包括您使用的指令),請參閱 gsutil 工具

根據預設,在 Cloud Storage 託管的值區和檔案為私人檔案。然而,對於您的店家搜尋器,您想要公開所有的檔案,以便透過網際網路讓所有使用者存取。您可以在上傳檔案後公開公開檔案,但這將會相當繁瑣。您只須為您建立的值區設定預設存取層級,而所有上傳至該檔案的檔案都會沿用該存取層級。

  1. 執行下列指令,並將 yourname-store-locator 替換為您為值區選擇的名稱:
$ gsutil defacl ch -u AllUsers:R gs://yourname-store-locator
  1. 現在您可以透過下列指令上傳目前目錄中的所有檔案 (目前只包括 index.htmlapp.js 檔案):
$ gsutil -h "Cache-Control:no-cache" cp * gs://yourname-store-locator

現在,您應該已經有一個網頁,當中提供線上地圖。要查看的網址會是 http://storage.googleapis.com/yourname-store-locator/index.html,和您之前選擇的值區名稱取代了 yourname-store-locator 部分。

清理

如要清除在這項專案中建立的所有資源,最簡單的方法是關閉您在本教學課程開始時建立的 Google Cloud 專案

  • 在 Cloud Console 中開啟「Settings」(設定) 頁面
  • 按一下 [Select a project] (選取專案)
  • 選取您在本教學課程開始時建立的專案,然後按一下 [Open] (開啟)。
  • 輸入專案 ID,然後按一下 [Shutdown]

9. 恭喜

恭喜!您已完成這個程式碼研究室。

您學到的內容

瞭解詳情

您還想查看其他程式碼研究室嗎?

在地圖上顯示資料視覺化 進一步瞭解如何自訂地圖樣式 在地圖上建立 3D 互動

上方未列出您所需的程式碼研究室嗎?請在這裡提出新的問題

如想進一步研究程式碼,請前往 https://github.com/googlecodelabs/google-maps-simple-store-locator 查看原始碼存放區。