使用 Google Maps Platform (JavaScript) 构建附近商家搜索服务

1. 准备工作

了解如何使用 Google Maps Platform 地图和 Places API 构建本地商家搜索功能,该功能可对用户进行地理定位并显示附近有趣的地方。该应用集成了地理定位、地点详情、地点照片等功能。

前提条件

  • 已掌握 HTML、CSS 和 JavaScript 方面的基础知识
  • 具有结算账号的项目(如果您没有此类项目,请按照下一步中的说明操作)。
  • 若要完成下面的启用步骤,您需要启用 Maps JavaScript APIPlaces API
  • 上述项目的 API 密钥。

Google Maps Platform 使用入门

如果您之前从未使用过 Google Maps Platform,请参阅 Google Maps Platform 使用入门指南或观看 Google Maps Platform 使用入门播放列表中的视频,完成以下步骤:

  1. 创建一个结算账号。
  2. 创建一个项目。
  3. 启用所需的 Google Maps Platform 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/

如果上述方法不起作用,请点击下方按钮下载此 Codelab 的全部代码,然后解压缩该文件:

下载代码

  1. 切换到您刚刚克隆或下载的目录。
cd google-maps-nearby-search-js

stepN 文件夹包含本 Codelab 的每个步骤的预期结束状态。这些内容可供参考。您可以在名为 work 的目录中处理所有代码。

2. 创建具有默认中心的地图

在网页上创建 Google 地图需要执行以下三个步骤:

  1. 创建 HTML 网页
  2. 添加地图
  3. 粘贴 API 密钥

1. 创建 HTML 网页

下图展示了在此步骤中创建的地图。地图以澳大利亚悉尼的悉尼歌剧院为中心。如果用户拒绝授予获取其位置信息的权限,地图会默认显示此位置,但仍会提供有趣的搜索结果。

569b9781658fec74.png

  1. 将目录切换为 work/ 文件夹。在本 Codelab 的其余部分中,请在 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 --> 后面的行中,复制脚本源网址中 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 库服务,请更新脚本源网址以引入 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. 调用地点附近搜索请求并处理响应

接下来,构建 PlaceSearch 请求。以下字段为必填字段:

以下字段为必填字段:

  • bounds,它必须是用于定义矩形搜索区域的 google.maps.LatLngBounds 对象,或者 locationradius;前者采用 google.maps.LatLng 对象,后者采用简单的整数,代表圆形区域的半径范围(以米为单位)。允许的最大半径范围为 50,000 米。请注意,将 rankBy 设置为 DISTANCE 时,您必须指定地理位置,但不能指定半径或边界。
  • 要与所有可用字段(包括但不限于名称、类型和地址,以及客户评价和其他第三方内容)进行匹配的 keyword将结果限制为仅包含与指定类型相匹配的地点的 type。只能指定一个类型(如果提供多个类型,系统会忽略第一个条目之后的所有类型)。请参阅支持的类型列表

在本 Codelab 中,您将使用用户的当前位置作为搜索位置,并按距离对结果进行排名。

  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. 将这行代码添加到 initMap 函数末尾的注释 TODO: Step 3B2 处。
/* TODO: Step 3B2, Call the Places Nearby Search */
// Call Places Nearby Search on user's location
getNearbyPlaces(pos);
  1. 将这行代码添加到 handleLocationError 函数末尾的注释 TODO: Step 3B3 处。
/* 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. 按需显示地点详情

获得地点的地点 ID(作为附近搜索结果中的一个字段提供)后,您可以请求有关该地点的更多详细信息,例如完整地址、电话号码以及用户评分和评价。在此 Codelab 中,您将创建一个侧边栏来显示丰富的地点详情,并使标记具有互动性,以便用户可以选择地点来查看详情。

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. initMap() 函数中 TODO: Step 4A3 注释后面,按如下所示初始化 infoPane 变量:
/* TODO: Step 4A3: Add a generic sidebar */
infoPane = document.getElementById('panel');

B. 向标记添加点击监听器

  1. createMarkers 函数中,在创建每个标记时为其添加点击监听器。

点击监听器会提取与相应标记关联的地点详情,并调用函数来显示这些详情。

  1. 将以下代码粘贴到 createMarkers 函数中代码注释 TODO: Step 4B 处。

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 库文档,这两份文档均包含指南、教程、API 参考、更多代码示例和支持渠道。一些热门功能包括将数据导入到地图中开始设置地图样式以及添加街景服务

您最希望我们接下来制作哪种类型的 Codelab?

使用丰富的地点信息的更多示例 使用 Maps Platform JavaScript API 的更多 Codelab 面向 Android 的更多 Codelab 面向 iOS 的更多 Codelab 在地图上直观呈现基于位置的数据 地图的自定义样式 使用 StreetView

上面没有列出您希望了解的 Codelab?没关系,请在此处通过创建新问题的方式申请 Codelab