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






4.88/5 (19投票s)
使用 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>© @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 布局中,与第一个版本相比,主要区别在于我们添加了一些东西:
- 我们在 Javascript 中有一个可能的目的地数组,所以有一个 '
PushDestination()
' 方法可以让我们将目的地添加到数组中。 - 我的懒惰提供了一个快速测试地点的方法 '
setDestination()
'。 - 在计算我们的距离和路线 *之前*,'destinations' div 存储了目的地的列表。
- 最后,我们有一个表格,以格式化的方式显示 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 日