65.9K
CodeProject 正在变化。 阅读更多。
Home

在 ASP.NET MVC 中使用 Google Maps 计算距离

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (19投票s)

2017年2月5日

CPOL

10分钟阅读

viewsIcon

47891

downloadIcon

3101

使用 Google Maps API 规划路线,并计算出从起点到最近目的地的距离

引言

Google Maps 和 Bing Maps 都是非常有用的工具。它们开放给开发者的 API 提供了极大的能力,可以为我们提供的解决方案增值。我们不必使用所有功能,有时只需snippets(代码片段)就可以为用户带来巨大的改变。本文将介绍我是如何利用一些 Google Maps API 来帮助办公室工作人员规划出他们工作中最佳的移动路线。

背景

当您有一个团队需要在外面工作,通常是开着汽车或货车,他们一般分为两类。一类是事先已经安排好的(例如商业运输配送车辆),另一类则需要计划,但可能需要在一天中根据需求的变化进行调整。本文主要关注的是后者。以水管工为例——他们白天确实有计划好的工作,但在一个大城市和一家繁忙的服务公司,白天可能会有多个紧急情况,如管道破裂、漏水等,需要优先处理并仔细、快速地规划交通路线。用于决定派遣人员去哪里的方法之一是一个“分诊系统”,它会问三个问题:

  • 任务有多重要/紧急?
  • 任务地点离我们哪位移动工作人员近?
  • 我们的移动工作人员从他们现在的位置到新地点最快需要多长时间?

这无疑会很快变得非常复杂,但通过结合不同的技术,例如移动设备上的 GPS/地理定位来监控远程工作人员的当前位置,以及您可能需要他们前往的各个地点的坐标,您可以为用户构建非常有用的解决方案。

我将要描述的代码允许用户输入远程工作人员的当前位置,以及一系列不同的“紧急呼叫”目的地。使用 Google Maps,我们将能够向用户显示工作人员距离每个位置的距离,以及到达每个位置所需的时间。有了这些信息,他们可以做出更好的决定,将哪些呼叫任务分配给哪些远程工作人员。

在这个示例代码的生产版本中,我结合了许多额外的信息来构建“您的最佳选择/路线是…”的算法。这包括:

  • 一个移动应用程序,用于跟踪工作人员,并每15分钟将他们的位置更新到中央数据库(这是使用 Visual Studio Cordova 编写的应用程序完成的——强烈推荐!)
  • 一个使用 fullcalendar 和 MVC 编写的调度/日历应用程序,与 这里 描述的非常相似,以及 本文 提到的使用 C# MVC 和 Razor 的多用户日历,其中包含远程工作人员的约会信息(预计时间 vs 实际花费时间等)
  • 一个特定于客户的后端应用程序,用于客户管理,存储客户位置数据

您可以看出,结合以上所有信息以及我们可以从地图 API 获取的数据,我们可以在解决方案中完成一些非常有益且节省时间的事情。


在我们开始编写代码之前,做一个快速的题外话,我在之前写的一篇文章中讨论过使用 Google Maps API,该文章描述了 如何在 ASP.NET MVC 解决方案中为 Google Maps API 实现自定义信息窗口弹出窗口和标记。您可能会觉得这篇文章对提供其他功能想法很有用。

Google API 密钥

要开始,您需要设置一个 Google API 密钥。注册过程非常简单,而且,除非您进行大规模使用,否则是免费的。在撰写本文时,您每24小时可以使用25,000次地图加载,这是相当慷慨的。如果您需要更多,我相信有人在付费,所以您可以获取商业许可。

以下是如何获取密钥的基本步骤……请注意,在我的示例代码中,您必须替换您自己的密钥……出于明显的原因,我不能给您我的密钥 :)

获取密钥非常简单……首先,请访问 Google Map API 页面,点击“Get API”按钮,输入项目名称,几秒钟后您就可以使用了……

妥善保管好密钥,我们稍后会用到它……

设置

我为本文编写的代码是 ASP.NET MVC,针对 .NET Framework 4.5,使用 Visual Studio 2015。要开始,我们创建一个 Web 项目,在这种情况下,我清理了 _Layout.cshtml 文件,因为我不希望标准的“应用程序登录”功能等出现在演示应用的顶部——这样我们就剩下类似下面的简单内容了。

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>@ViewBag.Title - My ASP.NET Application</title>
    @Styles.Render("~/Content/css")
    @Scripts.Render("~/bundles/modernizr")
</head>
<body>
    <div class="container body-content">
        @RenderBody()
        <hr />
        <footer>
            <p>&copy; @DateTime.Now.Year - My ASP.NET Application</p>
        </footer>
    </div>

    @Scripts.Render("~/bundles/jquery")
    @Scripts.Render("~/bundles/bootstrap")
    @RenderSection("scripts", required: false)
</body>
</html>

在 index.cshtml 页面中,我使用了 HTML 来让用户在本文的两个不同的代码演示之间切换,并有相应的控制器。

<div class="row">
    <div class="col-md-12">
        <h2><a href="demo1">Example 1</a> - simple distance between two points</h2>
        <p>
            This demonstrates getting and displaying the distance between two points
        </p>

      
    </div>
</div>

<div class="row">
    <div class="col-md-12">
        <h2><a href="demo2">Example 2</a> - distance between one origin and multiple target destinations</h2>
        <p>
            This demonstrates getting and displaying the distance between multiple points
        </p>
    </div>
</div>

 

        public ActionResult Demo1()
        {
            return View();
        }

        public ActionResult Demo2()
        {
            return View();
        }

 

两点之间的距离和路线

为了理解基础知识,让我们转到演示 1。在这里,我们将设置一个 Google Maps,并使用 API 请求地图上两点之间的路线和距离。

我们首先要做的是设置 HTML 并布局页面。页面上有一个地图,以及两个输入框,允许用户输入他们开始的位置,以及他们想要前往的目的地。为了在测试时节省输入(一直很懒!),我将这些输入值默认设置为英国的地点。

<div class="row">
    <div class="col-md-12">
        <h2>Example 1 - simple distance between two points</h2>
        <p>
            This demonstrates getting and displaying the distance between two points
        </p>

        <div>
            <div>
                Travel From : <input id="travelfrom" type="text" name="name" value="Chichester, UK" />
                To : <input id="travelto" type="text" name="name" value="Goodwood aerodrome, UK" />
                <input type="button" value="Get Route" onclick="GetRoute()" />

            </div>
            <br />
            <div>
                <div id="dvDistance">
                </div>
            </div>

        </div>

        <div id="dvMap" style="min-height:500px"></div>

    </div>
</div>

上面的关键控件是输入框 'travelfrom'、'travelto' 和调用 'GetRoute()' 函数的按钮。

其余的代码都在脚本部分。在这里,我们开始使用之前生成的 API 密钥。

@section scripts{

   <!--ENSURE YOU CHANGE TO YOUR OWN API KEY HERE !!! --> 
   <script src="https://maps.googleapis.com/maps/api/js?libraries=places&key=YOUR_KEY">
   </script>

...

** 重要 ** - 您需要将上面的 'Your_key' 替换为您自己生成的密钥。

我们首先设置一些 Javascript 变量。这些变量将包含调用地图 API 时使用的值。

 var source, destination;
        var directionsDisplay;
        var directionsService = new google.maps.DirectionsService();

接下来,我们获取之前在 HTML 标记中指定的 'dvMap' div 元素,并告诉地图 API 使用它作为绘制地图的容器。我还使用英格兰奇切斯特的纬度和经度来居中地图。在生产环境中,我有一些脚本可以将此中心视图在主办公室、远程工作人员和客户位置之间切换——使用户更容易定位他们的规划。
 

        // initialize the location of the map on Chichester in England (ref lat and lng)
        var map = new google.maps.Map(document.getElementById('dvMap'), {
            center: { lat: 50.834697, lng: -0.773792 },
            zoom: 13,
            mapTypeId: 'roadmap'
        });


地图 API 非常庞大,并且包含许多小技巧,这些技巧在编写解决方案时非常有用。例如,有一个 API 可以将输入框与“键入即时”位置查找功能关联起来——这使得用户更容易找到地点。在接下来的代码片段中,我们调用 'Searchbox' 方法将我们的 'travelfrom' 和 'travelto' 输入框链接到这个类型提示服务。还有一些代码允许用户通过鼠标点击/拖动来移动地图。

        google.maps.event.addDomListener(window, 'load', function () {
            new google.maps.places.SearchBox(document.getElementById('travelfrom'));
            new google.maps.places.SearchBox(document.getElementById('travelto'));
            directionsDisplay = new google.maps.DirectionsRenderer({ 'draggable': true });
        });


示例 1 - 主要代码

我们有一个核心函数叫做 'GetRoute()'。它接收 'travelfrom' 和 'travelto' 位置作为输入,将它们发送到 API,然后将 API 的结果绘制在用户可见的地图上。

我们首先设置要发送到 API 的请求参数。然后,我们调用 'directionsService.route' API 方法来绘制响应给用户('directionsDisplay.setDirections')。

            directionsDisplay.setMap(map);

            source = document.getElementById("travelfrom").value;
            destination = document.getElementById("travelto").value;

            var request = {
                origin: source,
                destination: destination,
                travelMode: google.maps.TravelMode.DRIVING
            };

            directionsService.route(request, function (response, status) {
                if (status == google.maps.DirectionsStatus.OK) {
                    directionsDisplay.setDirections(response);
                }
            });

向用户显示漂亮的地图是一回事,但对我们来说,我们还想获取路线上的具体详细信息,例如两点之间的距离,以及旅行所需的时间(在本例中,假设远程工作人员是乘汽车/货车旅行)。为了计算这个,我们将调用“距离矩阵服务”。

第一步是设置,提供起点和终点位置,将旅行模式设置为 DRIVING(驾车),测量系统设置为 METRIC(公制)。

            var service = new google.maps.DistanceMatrixService();
            service.getDistanceMatrix({
                origins: [source],
                destinations: [destination],
                travelMode: google.maps.TravelMode.DRIVING,
                unitSystem: google.maps.UnitSystem.METRIC,
                avoidHighways: false,
                avoidTolls: false

第二步处理来自 API 的响应,提取距离和持续时间的值,并将它们显示给用户,放在 HTML 元素 'dvDistance' 中。

            }, function (response, status) {

                if (status == google.maps.DistanceMatrixStatus.OK && 
                              response.rows[0].elements[0].status != "ZERO_RESULTS") {
                    var distance = response.rows[0].elements[0].distance.text;
                    var duration = response.rows[0].elements[0].duration.value;
                    var dvDistance = document.getElementById("dvDistance");
                    duration = parseFloat(duration / 60).toFixed(2);
                    dvDistance.innerHTML = "";
                    dvDistance.innerHTML += "Distance: " + distance + "<br />";
                    dvDistance.innerHTML += "Time:" + duration + " min";


渲染后,输出看起来就像这样,显示用户所需的指标。
 

多点之间的距离和路线

为了给用户更多的灵活性,让他们能够规划一段时间和远程工作人员的行程,现在我们将了解如何使用 Google Maps 的“途经点”(waypoint)API 来计算多个点之间的距离,并在地图视图中直观地呈现给用户。

我们要做的第一件事是设置 UI,以便用户可以输入一系列可能的目的地。在我的生产版本中,所有这些都自动从现有客户地址/未分配但待处理的日历预约数据库中获取。

正如你所见,我又懒惰了,所以为了测试,我硬编码了一个起点(奇切斯特)和一些可能的目的地(Tagmere 和 Bosham)。
 

在我们的 HTML 布局中,与第一个版本相比,主要区别在于我们添加了一些东西:

  1. 我们在 Javascript 中有一个可能的目的地数组,所以有一个 'PushDestination()' 方法可以让我们将目的地添加到数组中。
  2. 我的懒惰提供了一个快速测试地点的方法 'setDestination()'。
  3. 在计算我们的距离和路线 *之前*,'destinations' div 存储了目的地的列表。
  4. 最后,我们有一个表格,以格式化的方式显示 API 的结果。
  <div> Add Destination</div>
        <div>
            <input id="travelto" type="text" name="name" value="Oving, UK" />
            <input type="button" value="Add" onclick="PushDestination()" />
            <a href="#" onclick="setDestination('Tagmere, UK')">Tagmere, UK. </a>
            <a href="#" onclick="setDestination('Bosham, UK')">Bosham, UK</a>
        </div>
        <div id="destinations"></div><br />
        Source : <input id="travelfrom" type="text" name="name" value="Chichester, UK" />   
           
        <input type="button" value="Calculate" onclick="GetRoute()" />
        <p></p>
        <br />
        <div id="dvDistance">
            <table id="tblResults" border="1" cellpadding="10">
                <tr>
                    <th> Start </th>
                    <th> End </th>
                    <th> Distance </th>
                    <th> Duration </th>
                </tr>
            </table>

        </div>

        <div id="dvMap" style="min-height:500px"></div>

Javascript 在之前的代码基础上进行扩展,设置与之前相同。这里不同的是添加了一个 'locations' 数组。

 var source, destination;
        var locations = [];
        var directionsDisplay;
        var directionsService = new google.maps.DirectionsService();

        // initialize the location of the map on Chichester in England (ref lat and lng)
        var map = new google.maps.Map(document.getElementById('dvMap'), {
            center: { lat: 50.834697, lng: -0.773792 },
            zoom: 13,
            mapTypeId: 'roadmap'
        });

        google.maps.event.addDomListener(window, 'load', function () {
            new google.maps.places.SearchBox(document.getElementById('travelfrom'));
            new google.maps.places.SearchBox(document.getElementById('travelto'));
            directionsDisplay = new google.maps.DirectionsRenderer({ 'draggable': true });
        });

我们的目的地位置数组由 'PushDestination()' 方法管理。它获取 'travelto' 输入控件中的值,并将其添加到数组中。它还更新屏幕,将新项添加到 'destinations' div 中。

        function PushDestination() {
            destination = document.getElementById("travelto").value;
            locations.push(destination);
            document.getElementById("travelto").value = "";
            destinationArray = document.getElementById("destinations");            
            destinationArray.innerHTML += destination + "<br />";
        }

我的辅助方法将项目添加到输入框,并调用 PushDestination() 方法以保持流程进行。

        function setDestination(dest)
        {
            document.getElementById('travelto').value = dest;
            PushDestination();
        }

GetRoute() 方法开始时与之前一样……

        function GetRoute() {

            directionsDisplay.setMap(map);

            source = document.getElementById("travelfrom").value;
            destination = document.getElementById("travelto").value;

但是然后添加了一个“途经点”数组。

            var waypoints = [];
            for (var i = 0; i < locations.length; i++) {
                var address = locations[i];
                if (address !== "") {
                    waypoints.push({
                        location: address,
                        stopover: true
                    });
                }
            }

请求参数被微调,因为涉及不止一个目的地。我们添加了途经点数组,并告诉 API 优化途经点。

            var request = {
                origin: source,
                destination: waypoints[0].location,
                waypoints: waypoints, //an array of waypoints
                optimizeWaypoints: true, //set to true if you want Google to determine the 
                                         // shortest route or false to use the order specified.
                travelMode: google.maps.DirectionsTravelMode.DRIVING
            };

最后,我们发送请求,并解析返回的途经点结果,将输出显示在我们已准备好的 HTML 表格中。正如您所看到的,关键在于查看返回的路线响应中的每个“LEG”(段),并从中提取我们想要的信息。

            directionsService.route(request, function (response, status) {
                if (status == google.maps.DirectionsStatus.OK) {
                    var dvDistance = document.getElementById("dvDistance");
                    var distance = 0;
                    var minute = 0.00;
                    response.routes[0].legs.forEach(function (item, index) {
                        if (index < response.routes[0].legs.length - 1) {
                            distance = distance + parseInt(item.distance.text);
                            minute = parseFloat(minute) + parseFloat(item.duration.value / 60);

                            tbl = document.getElementById("tblResults");
                            var row = tbl.insertRow(1);
                            var cell = row.insertCell(0);
                            cell.innerText = source;
                            var cell = row.insertCell(1);
                            cell.innerText = item.end_address;
                            var cell = row.insertCell(2);
                            cell.innerText = distance;
                            var cell = row.insertCell(3);
                            cell.innerText = minute.toFixed(2) + " min";
                        }
                    });
                    directionsDisplay.setDirections(response);
                }
                else {
                    //handle error
                }
            })

这是最终的输出——正是我们想要的。

这只是实现此类解决方案的一种方式——我希望您觉得它很有用。与所有事物一样,API 会不断变化,所以如果您遇到错误,最好查看浏览器控制台以获取 API 的任何反馈!

我附带了一个示例项目供您使用——别忘了获取您自己的 API 密钥并将其插入到需要的地方!

关注点

在我之前写的一篇关于 使用 Google Maps API 的文章中,使用该代码不需要 API 密钥,事实上,如果您在 JSFiddle 等地方尝试运行代码,似乎也不需要密钥。然而,在这样的测试环境之外使用它会导致 API 在浏览器控制台中抛出错误,说明您需要一个 API……所以要小心,当事情不按预期工作时,请务必查看控制台!

历史

版本 1 - 发布日期 2017 年 2 月 5 日

 

© . All rights reserved.