使用 Google 地圖平台 (JavaScript) 建立附近的商家搜尋服務

1. 事前準備

瞭解如何使用 Google 地圖平台地圖和地點 API 建構在地商家搜尋功能,為使用者進行地理位置定位,並顯示附近的有趣地點。這個應用程式整合了地理位置、Place Details、Place Photos 等功能。

必要條件

  • 具備 HTML、CSS 和 JavaScript 基礎知識
  • 已連結帳單帳戶的專案 (如果沒有,請按照下一個步驟的指示操作)。
  • 在下方的啟用步驟中,您需要啟用 Maps JavaScript APIPlaces API
  • 上述專案的 API 金鑰。

開始使用 Google 地圖平台

如果您從未使用過 Google 地圖平台,請按照「開始使用 Google 地圖平台」指南或「開始使用 Google 地圖平台」播放清單中的操作說明,完成下列步驟:

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

執行步驟

  • 建構顯示 Google 地圖的網頁
  • 將地圖中心設為使用者所在位置
  • 尋找附近地點,並以可點選的標記顯示結果
  • 擷取並顯示每個地點的詳細資訊

ae1caf211daa484d.png

軟硬體需求

  • 網路瀏覽器,例如 Google Chrome (建議)、Firefox、Safari 或 Internet Explorer
  • 慣用的文字或程式碼編輯器

取得範例程式碼

  1. 開啟指令列介面 (MacOS 上的「終端機」或 Windows 上的「命令提示字元」),然後使用下列指令下載範例程式碼:
git clone https://github.com/googlecodelabs/google-maps-nearby-search-js/

如果無法解決問題,請點選下方按鈕下載這個程式碼研究室的所有程式碼,然後解壓縮檔案:

下載程式碼

  1. 變更為您剛複製或下載的目錄。
cd google-maps-nearby-search-js

stepN 資料夾包含本程式碼研究室每個步驟的最終狀態。僅供參考。在名為 work 的目錄中完成所有編碼工作。

2. 建立以預設中心為中心的地圖

在網頁上建立 Google 地圖時,步驟共有三個:

  1. 建立 HTML 網頁
  2. 新增地圖
  3. 貼上 API 金鑰

1. 建立 HTML 網頁

以下是這個步驟建立的地圖。地圖中心位於澳洲雪梨的雪梨歌劇院。如果使用者拒絕授予位置存取權,地圖會預設顯示這個位置,但仍會提供有趣的搜尋結果。

569b9781658fec74.png

  1. 將目錄變更為 work/ 資料夾。在本程式碼研究室的其餘部分,請在 work/ 資料夾中的版本進行編輯。
cd work
  1. work/ 目錄中,使用文字編輯器建立名為 index.html 的空白檔案。
  2. 將下列程式碼複製到 index.html

index.html

<!DOCTYPE html>
<html>

<head>
  <title>Sushi Finder</title>
  <meta name="viewport" content="initial-scale=1.0, user-scalable=no">
  <meta charset="utf-8">
  <style>
    /* Always set the map height explicitly to define the size of the div
     * element that contains the map. */
    #map {
      height: 100%;
      background-color: grey;
    }

    /* Optional: Makes the sample page fill the window. */
    html,
    body {
      height: 100%;
      margin: 0;
      padding: 0;
    }

    /* TODO: Step 4A1: Make a generic sidebar. */
  </style>
</head>

<body>
  <!-- TODO: Step 4A2: Add a generic sidebar -->

  <!-- Map appears here -->
  <div id="map"></div>

  <!-- TODO: Step 1B, Add a map -->
</body>

</html>
  1. 在網路瀏覽器中開啟 index.html 檔案。
open index.html

2. 新增地圖

本節說明如何在網頁中載入 Maps JavaScript API,以及如何自行編寫 JavaScript,以使用 API 在網頁中加入地圖。

  1. map div 後方和結尾 </body> 標記之前,加入這個指令碼程式碼 (顯示 <!-- TODO: Step 1B, Add a map --> 的位置)。

step1/index.html

<!-- TODO: Step 1B, Add a map -->
<script>
    /* Note: This example requires that you consent to location sharing when
     * prompted by your browser. If you see the error "Geolocation permission
     * denied.", it means you probably did not give permission for the browser * to locate you. */

    /* TODO: Step 2, Geolocate your user
     * Replace the code from here to the END TODO comment with new code from
     * codelab instructions. */
    let pos;
    let map;
    function initMap() {
        // Set the default location and initialize all variables
        pos = {lat: -33.857, lng: 151.213};
        map = new google.maps.Map(document.getElementById('map'), {
            center: pos,
            zoom: 15
        });
    }
    /* END TODO: Step 2, Geolocate your user */
</script>

<!-- TODO: Step 1C, Get an API key -->
<!-- TODO: Step 3A, Load the Places Library -->
<script async defer src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&callback=initMap">
</script>

3. 貼上 API 金鑰

  1. <!-- TODO: Step 1C, Get an API key --> 後方的行中,複製並將指令碼來源網址中的金鑰參數值,替換為您在必要條件中建立的 API 金鑰。

step1/index.html

<!-- TODO: Step 1C, Get an API key -->
<!-- TODO: Step 3A, Load the Places Library -->
<script async defer src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&callback=initMap">
</script>
  1. 儲存您正在處理的 HTML 檔案。

測試

重新載入瀏覽器中您正在編輯的檔案。現在應該會看到地圖,取代先前的灰色矩形。如果看到錯誤訊息,請確認您已將最終 <script> 標記中的「YOUR_API_KEY」換成自己的 API 金鑰。如尚未取得 API 金鑰,請參閱上文瞭解如何取得。

完整程式碼範例

您可以在 Github 取得這個專案目前為止的完整程式碼。

3. 找出使用者的地理位置

接下來,您要使用瀏覽器的 HTML5 地理位置功能以及 Maps JavaScript API,在 Google 地圖上顯示使用者或裝置的地理位置。

以下是地圖範例,顯示您在加州山景城瀏覽時的地理位置:

1dbb3fec117cd895.png

什麼是地理位置?

地理位置是指透過各種資料收集機制,針對使用者或運算裝置識別出的地理位置。一般而言,大部分的地理定位服務利用網路路線規劃位址或利用內部 GPS 裝置,來判斷位置。這個應用程式會使用網路瀏覽器的 W3C 地理位置標準 navigator.geolocation 屬性,判斷使用者的位置。

親自試試

將註解 TODO: Step 2, Geolocate your userEND TODO: Step 2, Geolocate your user 之間的程式碼替換成下列程式碼:

step2/index.html

/* TODO: Step 2, Geolocate your user
    * Replace the code from here to the END TODO comment with this code
    * from codelab instructions. */
let pos;
let map;
let bounds;
let infoWindow;
let currentInfoWindow;
let service;
let infoPane;
function initMap() {
    // Initialize variables
    bounds = new google.maps.LatLngBounds();
    infoWindow = new google.maps.InfoWindow;
    currentInfoWindow = infoWindow;
    /* TODO: Step 4A3: Add a generic sidebar */

    // Try HTML5 geolocation
    if (navigator.geolocation) {
    navigator.geolocation.getCurrentPosition(position => {
        pos = {
        lat: position.coords.latitude,
        lng: position.coords.longitude
        };
        map = new google.maps.Map(document.getElementById('map'), {
        center: pos,
        zoom: 15
        });
        bounds.extend(pos);

        infoWindow.setPosition(pos);
        infoWindow.setContent('Location found.');
        infoWindow.open(map);
        map.setCenter(pos);

        /* TODO: Step 3B2, Call the Places Nearby Search */
    }, () => {
        // Browser supports geolocation, but user has denied permission
        handleLocationError(true, infoWindow);
    });
    } else {
    // Browser doesn't support geolocation
    handleLocationError(false, infoWindow);
    }
}

// Handle a geolocation error
function handleLocationError(browserHasGeolocation, infoWindow) {
    // Set default location to Sydney, Australia
    pos = {lat: -33.856, lng: 151.215};
    map = new google.maps.Map(document.getElementById('map'), {
    center: pos,
    zoom: 15
    });

    // Display an InfoWindow at the map center
    infoWindow.setPosition(pos);
    infoWindow.setContent(browserHasGeolocation ?
    'Geolocation permissions denied. Using default location.' :
    'Error: Your browser doesn\'t support geolocation.');
    infoWindow.open(map);
    currentInfoWindow = infoWindow;

    /* TODO: Step 3B3, Call the Places Nearby Search */
}
/* END TODO: Step 2, Geolocate your user */
/* TODO: Step 3B1, Call the Places Nearby Search */

測試

  1. 儲存檔案。
  2. 重新載入頁面。

瀏覽器現在應該會要求您授權應用程式分享位置資訊。

  1. 按一下「Block」一次,看看是否能順利處理錯誤,並維持以雪梨為中心。
  2. 再次重新載入,然後按一下「允許」,查看地理位置資訊是否正常運作,並將地圖移至目前所在位置。

完整程式碼範例

您可以在 Github 取得這個專案目前為止的完整程式碼。

4. 搜尋附近地點

「搜尋附近」可讓您依照關鍵字或類型,搜尋指定區域內的地點。「搜尋附近」必須一律包含位置,可透過以下任一方法指定:

  • 定義矩形搜尋區域的 LatLngBounds 物件
  • 使用 location 屬性 (將圓心指定為 LatLng 物件) 與半徑 (以公尺為單位) 的組合定義圓形區域。

呼叫 PlacesService nearbySearch() 方法,啟動「搜尋附近」要求,傳回 PlaceResult 物件的陣列。

A. 載入 Places Library

首先,如要存取 Places Library 服務,請更新指令碼來源網址,加入 libraries 參數,並新增 places 做為值。

step3/index.html

<!-- TODO: Step 3A, Load the Places Library -->
<script async defer
    src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&libraries=places&callback=initMap">

B. 呼叫 Places Nearby Search 要求並處理回應

接著,請建立 PlaceSearch 要求。最低必要欄位如下:

最低必要欄位如下:

  • bounds:必須是定義矩形搜尋區域的 google.maps.LatLngBounds 物件, locationradius;前者需要採用 google.maps.LatLng 物件,後者需要採用簡單整數,代表圓形的半徑 (以公尺為單位)。允許的最大半徑為 50,000 公尺。請注意,當 rankBy 設為 DISTANCE 時,您必須指定位置,但無法指定半徑或範圍。
  • 要與所有可用欄位比對的 keyword,包括但不限於名稱、類型、地址、顧客評論和其他第三方內容, type,可限制結果只顯示符合指定類型的地點。您只能指定一種類型 (如果提供多種類型,系統會忽略第一種類型之後輸入的所有類型)。請參閱支援類型清單

在本程式碼研究室中,您會使用使用者的目前位置做為搜尋位置,並依距離排序結果。

  1. 在註解 TODO: Step 3B1 中新增下列內容,編寫兩個函式來呼叫搜尋並處理回應。

系統會使用 sushi 關鍵字做為搜尋字詞,但您可以變更。定義 createMarkers 函式的程式碼會在下一節中提供。

step3/index.html

/* TODO: Step 3B1, Call the Places Nearby Search */
// Perform a Places Nearby Search Request
function getNearbyPlaces(position) {
    let request = {
    location: position,
    rankBy: google.maps.places.RankBy.DISTANCE,
    keyword: 'sushi'
    };

    service = new google.maps.places.PlacesService(map);
    service.nearbySearch(request, nearbyCallback);
}

// Handle the results (up to 20) of the Nearby Search
function nearbyCallback(results, status) {
    if (status == google.maps.places.PlacesServiceStatus.OK) {
    createMarkers(results);
    }
}

/* TODO: Step 3C, Generate markers for search results */
  1. 在註解 TODO: Step 3B2 處,將這行程式碼新增至 initMap 函式的結尾。
/* TODO: Step 3B2, Call the Places Nearby Search */
// Call Places Nearby Search on user's location
getNearbyPlaces(pos);
  1. 在註解 TODO: Step 3B3 處,將這行程式碼新增至 handleLocationError 函式的結尾。
/* TODO: Step 3B3, Call the Places Nearby Search */
// Call Places Nearby Search on the default location
getNearbyPlaces(pos);

C. 為搜尋結果產生標記

您可以使用標記代表地圖上的位置。標記預設會使用標準圖片,如要瞭解如何自訂標記圖片,請參閱「標記」。

google.maps.Marker 建構函式會使用一個 Marker options 物件常值來指定標記的起始屬性。

請特別留意以下欄位,因為當您建立標記時,通常必須設定這些欄位:

  • position (必要) 會指定 LatLng 來識別標記的起始位置。
  • map (選用) 會指定要加入標記的地圖。如果您在建立標記時不指定地圖,這個標記就不會附加到 (或顯示在) 地圖上。如果之後要新增標記,您可以呼叫標記的 setMap() 方法。
  • 在註解 TODO: Step 3C 後方新增下列程式碼,為回應中傳回的每個地點設定位置、地圖和標題。您也可以使用 bounds 變數的 extend 方法,確保地圖上顯示中心點和所有標記。

step3/index.html

/* TODO: Step 3C, Generate markers for search results */
// Set markers at the location of each place result
function createMarkers(places) {
    places.forEach(place => {
    let marker = new google.maps.Marker({
        position: place.geometry.location,
        map: map,
        title: place.name
    });

    /* TODO: Step 4B: Add click listeners to the markers */

    // Adjust the map bounds to include the location of this marker
    bounds.extend(place.geometry.location);
    });
    /* Once all the markers have been placed, adjust the bounds of the map to
    * show all the markers within the visible area. */
    map.fitBounds(bounds);
}

/* TODO: Step 4C: Show place details in an info window */

測試

  1. 儲存並重新載入頁面,然後按一下「允許」,授予地理位置資訊權限。

地圖中心位置周圍最多會顯示 20 個紅色標記。

  1. 再次重新載入網頁,這次請封鎖地理位置資訊存取權。

您是否仍會在預設地圖中心 (在範例中,預設中心位於澳洲雪梨) 取得結果?

完整程式碼範例

您可以在 Github 取得這個專案目前為止的完整程式碼。

5. 視需要顯示地點詳細資料

取得地點的 Place ID (以 Nearby Search 結果中的其中一個欄位形式提供) 後,您就可以要求該地點的其他詳細資料,例如完整地址、電話號碼、使用者評分和評論。在本程式碼研究室中,您將製作側邊欄來顯示豐富的 Place Details,並讓標記成為互動式元素,方便使用者選取地點來查看詳細資料。

A. 製作一般側欄

您需要顯示地點詳細資料的位置,因此以下提供側邊欄的簡單程式碼,使用者點選標記時,即可滑出並顯示地點詳細資料。

  1. 在註解 TODO: Step 4A1 後方,將下列程式碼新增至 style 標記:

step4/index.html

/* TODO: Step 4A1: Make a generic sidebar */
/* 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;
}

/* Styling for place details */
.hero {
    width: 100%;
    height: auto;
    max-height: 166px;
    display: block;
}

.place,
p {
    font-family: 'open sans', arial, sans-serif;
    padding-left: 18px;
    padding-right: 18px;
}

.details {
    color: darkslategrey;
}

a {
    text-decoration: none;
    color: cadetblue;
}
  1. map div 前方的 body 區段中,新增詳細資料面板的 div。
<!-- TODO: Step 4A2: Add a generic sidebar -->
<!-- The slide-out panel for showing place details -->
<div id="panel"></div>
  1. TODO: Step 4A3 註解後方的 initMap() 函式中,初始化 infoPane 變數,如下所示:
/* TODO: Step 4A3: Add a generic sidebar */
infoPane = document.getElementById('panel');

B. 將點按事件監聽器新增至標記

  1. createMarkers 函式中,建立每個標記時,請為標記新增點按事件監聽器。

點擊事件監聽器會擷取與該標記相關聯的地點詳細資料,並呼叫函式來顯示詳細資料。

  1. 將下列程式碼貼到程式碼註解 TODO: Step 4BcreateMarkers 函式中。

下一節會實作 showDetails 方法。

step4/index.html

/* TODO: Step 4B: Add click listeners to the markers */
// Add click listener to each marker
google.maps.event.addListener(marker, 'click', () => {
    let request = {
    placeId: place.place_id,
    fields: ['name', 'formatted_address', 'geometry', 'rating',
        'website', 'photos']
    };

    /* Only fetch the details of a place when the user clicks on a marker.
    * If we fetch the details for all place results as soon as we get
    * the search response, we will hit API rate limits. */
    service.getDetails(request, (placeResult, status) => {
    showDetails(placeResult, marker, status)
    });
});

addListener 要求中,placeId 屬性會指定詳細資料要求中的單一地點,而 fields 屬性則是欄位名稱的陣列,用於指定要傳回的地點資訊。如需可要求欄位的完整清單,請參閱 PlaceResult 介面

C. 在資訊視窗中顯示地點詳細資料

資訊視窗會在對話方塊中顯示內容 (通常為文字或圖片),並顯示在地圖上特定位置的上方。資訊視窗是以一個內容區域以及一個錐形柄所組成。錐形柄的尖端會連接地圖上的指定位置。一般來說,資訊視窗會附加至標記,但您也可以附加至特定經緯度。

  1. 在註解 TODO: Step 4C 中加入下列程式碼,建立 InfoWindow 來顯示商家名稱和評分,並將該視窗附加至標記。

您會在下一節中定義 showPanel,以便在側欄中顯示詳細資料。

step4/index.html

/* TODO: Step 4C: Show place details in an info window */
// Builds an InfoWindow to display details above the marker
function showDetails(placeResult, marker, status) {
    if (status == google.maps.places.PlacesServiceStatus.OK) {
    let placeInfowindow = new google.maps.InfoWindow();
    placeInfowindow.setContent('<div><strong>' + placeResult.name +
        '</strong><br>' + 'Rating: ' + placeResult.rating + '</div>');
    placeInfowindow.open(marker.map, marker);
    currentInfoWindow.close();
    currentInfoWindow = placeInfowindow;
    showPanel(placeResult);
    } else {
    console.log('showDetails failed: ' + status);
    }
}

/* TODO: Step 4D: Load place details in a sidebar */

D. 在側欄載入地點詳細資料

使用 PlaceResult 物件中傳回的相同詳細資料,填入另一個 div。在本範例中,請使用 infoPane,這是 ID 為「panel」的 div 的任意變數名稱。每當使用者點選新標記時,這段程式碼會關閉側邊欄 (如果已開啟)、清除舊的詳細資料、新增詳細資料,然後開啟側邊欄。

  1. 在註解 TODO: Step 4D 後方新增以下程式碼。

step4/index.html

/* TODO: Step 4D: Load place details in a sidebar */
// Displays place details in a sidebar
function showPanel(placeResult) {
    // If infoPane is already open, close it
    if (infoPane.classList.contains("open")) {
    infoPane.classList.remove("open");
    }

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

    /* TODO: Step 4E: Display a Place Photo with the Place Details */

    // Add place details with text formatting
    let name = document.createElement('h1');
    name.classList.add('place');
    name.textContent = placeResult.name;
    infoPane.appendChild(name);
    if (placeResult.rating != null) {
    let rating = document.createElement('p');
    rating.classList.add('details');
    rating.textContent = `Rating: ${placeResult.rating} \u272e`;
    infoPane.appendChild(rating);
    }
    let address = document.createElement('p');
    address.classList.add('details');
    address.textContent = placeResult.formatted_address;
    infoPane.appendChild(address);
    if (placeResult.website) {
    let websitePara = document.createElement('p');
    let websiteLink = document.createElement('a');
    let websiteUrl = document.createTextNode(placeResult.website);
    websiteLink.appendChild(websiteUrl);
    websiteLink.title = placeResult.website;
    websiteLink.href = placeResult.website;
    websitePara.appendChild(websiteLink);
    infoPane.appendChild(websitePara);
    }

    // Open the infoPane
    infoPane.classList.add("open");
}

E. 顯示地點相片和地點詳細資料

getDetails 結果會傳回與 placeId 相關聯的陣列,最多可包含 10 張相片。在這裡,您會在側欄中的地點名稱上方顯示第一張相片。

  1. 如要讓相片顯示在側欄頂端,請在建立 name 元素前放置這段程式碼。

step4/index.html

/* TODO: Step 4E: Display a Place Photo with the Place Details */
// Add the primary photo, if there is one
if (placeResult.photos != null) {
    let firstPhoto = placeResult.photos[0];
    let photo = document.createElement('img');
    photo.classList.add('hero');
    photo.src = firstPhoto.getUrl();
    infoPane.appendChild(photo);
}

測試

  1. 儲存並重新載入瀏覽器中的頁面,然後允許地理位置資訊權限。
  2. 按一下標記,即可查看標記彈出的資訊視窗,當中會顯示一些詳細資料,而側欄則會從左側滑出,顯示更多詳細資料。
  3. 重新載入並拒絕地理位置權限後,測試搜尋功能是否仍可運作。編輯搜尋關鍵字,查詢不同內容,並探索該搜尋傳回的結果。

ae1caf211daa484d.png

完整程式碼範例

您可以在 Github 取得這個專案目前為止的完整程式碼。

6. 恭喜

恭喜!您使用了 Maps JavaScript API 的許多功能,包括 Places 程式庫。

涵蓋內容

瞭解詳情

如要進一步運用地圖,請參閱 Maps JavaScript API 說明文件Places Library 說明文件,兩者都包含指南、教學課程、API 參考資料、更多程式碼範例和支援管道。熱門功能包括將資料匯入地圖開始設定地圖樣式,以及新增 Street View 服務

您最希望我們接下來建構哪種類型的程式碼研究室?

使用豐富的 Places 資訊的更多範例 使用 Maps Platform JavaScript API 的更多程式碼研究室 Android 的更多程式碼研究室 iOS 的更多程式碼研究室 在地圖上顯示位置資訊資料 地圖的自訂樣式 使用街景服務

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