使用地点自动补全和 Routes API 构建路线规划器

1. 概览

无论您是准备开启公路旅行、规划日常通勤路线,还是在繁华的城市中导航,从 A 点前往 B 点不仅仅是知道要去哪里。可靠的路线生成工具至关重要。

借助 Google Maps Platform,您可以向应用中添加动态地图,让用户通过自动补全功能快速输入位置信息,并在地图上显示路线。

此 Codelab 将引导开发者使用 Maps JavaScript API地点自动补全Routes API 构建 Web 应用。您将通过可自定义的教程了解如何集成多个 Google Maps Platform API。

构建内容

此 Codelab 将指导您使用 HTML、CSS、JavaScript 和 Node.js 后端构建 Web 应用。

路线规划工具 Web 应用架构

路线规划工具 Web 应用

学习内容

  • 如何启用 Google Maps Platform API
  • 如何将动态地图集成到 Web 应用中
  • 如何集成地点自动补全服务
  • 如何通过 Routes API 请求路线
  • 如何在动态地图上显示路线
  • 如何创建地图 ID
  • 如何向动态地图添加高级标记

所需条件

示例代码

GitHub 上提供了完整的解决方案和分步代码。代码不包含必需的 Node 软件包。在执行代码之前,请安装必要的依赖项。所需软件包的详细信息可在 package.json 文件中找到(第 3 步中对此进行了说明)。

2. 设置项目并启用 API

在启用步骤中,您需要启用 Maps JavaScript APIPlace AutocompleteRoutes API

设置 Google Maps Platform

如果您还没有已启用结算功能的 Google Cloud Platform 账号和项目,请参阅 Google Maps Platform 使用入门指南,创建结算账号和项目。

  1. Cloud 控制台中,点击项目下拉菜单,然后选择要用于此 Codelab 的项目。选择项目
  2. Maps API 库页面中,启用此 Codelab 所需的 Google Maps Platform API。为此,请按照此视频或此文档中的步骤操作。
  3. 在 Cloud Console 的凭据页面中生成 API 密钥。您可以按照此视频或此文档中的步骤操作。向 Google Maps Platform 发出的所有请求都需要 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 步)。

此外,您还可以随意安装自己喜欢的框架和模块。此实验使用 Web 框架(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. 在 public 目录中,创建 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. 在 public 目录中创建 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. 在 public 目录中,创建 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。详细了解如何为应用创建地图 ID

在 index.html 中,将 app.js 链接到 body 结束标记之前,并放在加载 Maps JavaScript API 的脚本标记之后。

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

完整示例代码

截至目前为止的完整代码可在 GitHub 上找到:step1_createDynamicMap

5. 输入出发地和目的地的地址

  • 在 index.html 中添加两个文本字段,用于输入出发地和目的地
  • 导入自动补全库
  • 将自动补全服务绑定到出发地和目的地文本字段

添加文本字段

在 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 类是一个根据用户文本输入提供地点预测的 widget。它会附加到文本类型的输入元素,并监听该字段中的文本输入。预测结果列表以下拉列表的形式显示,并随着文本的输入而更新。

在 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 Places 数据库中和 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 的结束标记之前添加一个类为 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. 从 Web 客户端收集数据
  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 响应会返回给 Web 客户端。在下一步中,我们将了解如何在地图上显示路线。

完整示例代码

截至目前为止的完整代码可在 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 提供的便捷的视口信息,将地图视口居中放置在检索到的路线上。在 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 Maps Platform API
  • 将 Google Maps JavaScript API 加载到 HTML 页面中
  • 导入地点库 (Maps JavaScript API)
  • 将地点自动补全服务绑定到文本字段
  • 通过 Routes API 请求路线
  • 在动态地图上显示路线
  • 创建地图 ID
  • 创建高级标记

了解详情

您还想查看其他哪些 Codelab?

地图上的数据可视化 更多关于自定义地图样式的信息 在地图中构建 3D 交互

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