我正在尝试创建一个谷歌地图,用户可以在上面标出他走过/跑过/骑过的路线,并查看他跑了多长时间。为此,GPolyline
类及其getLength()
方法非常有帮助(至少对于Google Maps API V2),但我想添加基于距离的标记,例如1公里,5公里,10公里等,但似乎没有明显的方法可以根据沿着线路的距离找到一点。 有什么建议吗?
我正在尝试创建一个谷歌地图,用户可以在上面标出他走过/跑过/骑过的路线,并查看他跑了多长时间。为此,GPolyline
类及其getLength()
方法非常有帮助(至少对于Google Maps API V2),但我想添加基于距离的标记,例如1公里,5公里,10公里等,但似乎没有明显的方法可以根据沿着线路的距离找到一点。 有什么建议吗?
我在几个月前回答了一个类似的问题,是如何在SQL Server 2008服务器端处理此问题。现在我将相同的算法移植到JavaScript中,使用Google Maps API v2。
为了举例说明,我们使用一个简单的4点折线,总长度约为8800米。下面的片段将定义这条折线并在地图上呈现:
var map = new GMap2(document.getElementById('map_canvas'));
var points = [
new GLatLng(47.656, -122.360),
new GLatLng(47.656, -122.343),
new GLatLng(47.690, -122.310),
new GLatLng(47.690, -122.270)
];
var polyline = new GPolyline(points, '#f00', 6);
map.setCenter(new GLatLng(47.676, -122.343), 12);
map.addOverlay(polyline);
在我们开始实际算法之前,我们需要一个函数,当给定起始点、终止点和沿该线路行驶的距离时,返回目标点。幸运的是,Chris Veness在计算纬度/经度点之间距离、方位和更多内容中提供了一些方便的JavaScript实现。
特别地,我已经从上述来源中调整了以下两种方法以与Google的GLatLng
类一起使用:
这些方法被用来扩展Google的GLatLng
类,并创建了一个moveTowards()
方法,当给定另一个点和一个距离(以米为单位)时,它将返回从原点沿那条线路向传递的点前进指定距离后的另一个GLatLng
。
GLatLng.prototype.moveTowards = function(point, distance) {
var lat1 = this.lat().toRad();
var lon1 = this.lng().toRad();
var lat2 = point.lat().toRad();
var lon2 = point.lng().toRad();
var dLon = (point.lng() - this.lng()).toRad();
// Find the bearing from this point to the next.
var brng = Math.atan2(Math.sin(dLon) * Math.cos(lat2),
Math.cos(lat1) * Math.sin(lat2) -
Math.sin(lat1) * Math.cos(lat2) *
Math.cos(dLon));
var angDist = distance / 6371000; // Earth's radius.
// Calculate the destination point, given the source and bearing.
lat2 = Math.asin(Math.sin(lat1) * Math.cos(angDist) +
Math.cos(lat1) * Math.sin(angDist) *
Math.cos(brng));
lon2 = lon1 + Math.atan2(Math.sin(brng) * Math.sin(angDist) *
Math.cos(lat1),
Math.cos(angDist) - Math.sin(lat1) *
Math.sin(lat2));
if (isNaN(lat2) || isNaN(lon2)) return null;
return new GLatLng(lat2.toDeg(), lon2.toDeg());
}
有了这个方法,我们现在可以按照以下方式解决问题:
如果第2步中的距离大于我们需要沿着路径行进的距离:
...那么目标点就在这个点和下一个点之间。只需向当前点应用 moveTowards()
方法,传递下一个点和需要行进的距离。返回结果并结束迭代。
否则:
...目标点在迭代中下一个点的后面更远处。我们需要从沿着路径行进的总距离中减去此点与下一个点之间的距离。使用修改后的距离继续迭代。
您可能已经注意到,我们可以很容易地通过递归来实现上述过程,而不是通过迭代。因此让我们这样做:
function moveAlongPath(points, distance, index) {
index = index || 0; // Set index to 0 by default.
if (index < points.length) {
// There is still at least one point further from this point.
// Construct a GPolyline to use its getLength() method.
var polyline = new GPolyline([points[index], points[index + 1]]);
// Get the distance from this point to the next point in the polyline.
var distanceToNextPoint = polyline.getLength();
if (distance <= distanceToNextPoint) {
// distanceToNextPoint is within this point and the next.
// Return the destination point with moveTowards().
return points[index].moveTowards(points[index + 1], distance);
}
else {
// The destination is further from the next point. Subtract
// distanceToNextPoint from distance and continue recursively.
return moveAlongPath(points,
distance - distanceToNextPoint,
index + 1);
}
}
else {
// There are no further points. The distance exceeds the length
// of the full path. Return null.
return null;
}
}
使用上述方法,如果我们定义了一个GLatLng
点的数组,并且我们使用这个点数组以及距离为2,500米来调用我们的moveAlongPath()
函数,它将返回该路径上距离第一个点2.5公里处的GLatLng
。
var points = [
new GLatLng(47.656, -122.360),
new GLatLng(47.656, -122.343),
new GLatLng(47.690, -122.310),
new GLatLng(47.690, -122.270)
];
var destinationPointOnPath = moveAlongPath(points, 2500);
// destinationPointOnPath will be a GLatLng on the path
// at 2.5km from the start.
因此,我们只需要为路径上的每个检查点调用 moveAlongPath()
。如果你需要在1km、5km和10km处有三个标记点,你可以简单地这样做:moveAlongPath()
。如果您需要在1公里、5公里和10公里处有三个标记,则可以简单执行以下操作:map.addOverlay(new GMarker(moveAlongPath(points, 1000)));
map.addOverlay(new GMarker(moveAlongPath(points, 5000)));
map.addOverlay(new GMarker(moveAlongPath(points, 10000)));
请注意,moveAlongPath()
可能会返回null
,如果我们请求的检查点距路径总长度更远,因此在传递给new GMarker()
之前最好先检查返回值。
我们可以将这些内容组合起来以完成完整的实现。在此示例中,我们沿着之前定义的8.8公里路径每隔1,000米放置一个标记:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8"/>
<title>Google Maps - Moving point along a path</title>
<script src="http://maps.google.com/maps?file=api&v=2&sensor=false"
type="text/javascript"></script>
</head>
<body onunload="GUnload()">
<div id="map_canvas" style="width: 500px; height: 300px;"></div>
<script type="text/javascript">
Number.prototype.toRad = function() {
return this * Math.PI / 180;
}
Number.prototype.toDeg = function() {
return this * 180 / Math.PI;
}
GLatLng.prototype.moveTowards = function(point, distance) {
var lat1 = this.lat().toRad();
var lon1 = this.lng().toRad();
var lat2 = point.lat().toRad();
var lon2 = point.lng().toRad();
var dLon = (point.lng() - this.lng()).toRad();
// Find the bearing from this point to the next.
var brng = Math.atan2(Math.sin(dLon) * Math.cos(lat2),
Math.cos(lat1) * Math.sin(lat2) -
Math.sin(lat1) * Math.cos(lat2) *
Math.cos(dLon));
var angDist = distance / 6371000; // Earth's radius.
// Calculate the destination point, given the source and bearing.
lat2 = Math.asin(Math.sin(lat1) * Math.cos(angDist) +
Math.cos(lat1) * Math.sin(angDist) *
Math.cos(brng));
lon2 = lon1 + Math.atan2(Math.sin(brng) * Math.sin(angDist) *
Math.cos(lat1),
Math.cos(angDist) - Math.sin(lat1) *
Math.sin(lat2));
if (isNaN(lat2) || isNaN(lon2)) return null;
return new GLatLng(lat2.toDeg(), lon2.toDeg());
}
function moveAlongPath(points, distance, index) {
index = index || 0; // Set index to 0 by default.
if (index < points.length) {
// There is still at least one point further from this point.
// Construct a GPolyline to use the getLength() method.
var polyline = new GPolyline([points[index], points[index + 1]]);
// Get the distance from this point to the next point in the polyline.
var distanceToNextPoint = polyline.getLength();
if (distance <= distanceToNextPoint) {
// distanceToNextPoint is within this point and the next.
// Return the destination point with moveTowards().
return points[index].moveTowards(points[index + 1], distance);
}
else {
// The destination is further from the next point. Subtract
// distanceToNextPoint from distance and continue recursively.
return moveAlongPath(points,
distance - distanceToNextPoint,
index + 1);
}
}
else {
// There are no further points. The distance exceeds the length
// of the full path. Return null.
return null;
}
}
var map = new GMap2(document.getElementById('map_canvas'));
var points = [
new GLatLng(47.656, -122.360),
new GLatLng(47.656, -122.343),
new GLatLng(47.690, -122.310),
new GLatLng(47.690, -122.270)
];
var polyline = new GPolyline(points, '#f00', 6);
var nextMarkerAt = 0; // Counter for the marker checkpoints.
var nextPoint = null; // The point where to place the next marker.
map.setCenter(new GLatLng(47.676, -122.343), 12);
// Draw the path on the map.
map.addOverlay(polyline);
// Draw the checkpoint markers every 1000 meters.
while (true) {
// Call moveAlongPath which will return the GLatLng with the next
// marker on the path.
nextPoint = moveAlongPath(points, nextMarkerAt);
if (nextPoint) {
// Draw the marker on the map.
map.addOverlay(new GMarker(nextPoint));
// Add +1000 meters for the next checkpoint.
nextMarkerAt += 1000;
}
else {
// moveAlongPath returned null, so there are no more check points.
break;
}
}
</script>
</body>
</html>
上面示例的截图,显示每1000米的标记:
以下是所需函数的原型:
google.maps.Polygon.prototype.Distance = function() {
var dist = 0;
for (var i=1; i < this.getPath().getLength(); i++) {
dist += this.getPath().getAt(i).distanceFrom(this.getPath().getAt(i-1));
}
return dist;
}
google.maps.LatLng.prototype.distanceFrom = function(newLatLng) {
//var R = 6371; // km (change this constant to get miles)
var R = 6378100; // meters
var lat1 = this.lat();
var lon1 = this.lng();
var lat2 = newLatLng.lat();
var lon2 = newLatLng.lng();
var dLat = (lat2-lat1) * Math.PI / 180;
var dLon = (lon2-lon1) * Math.PI / 180;
var a = Math.sin(dLat/2) * Math.sin(dLat/2) +
Math.cos(lat1 * Math.PI / 180 ) * Math.cos(lat2 * Math.PI / 180 ) *
Math.sin(dLon/2) * Math.sin(dLon/2);
var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
var d = R * c;
return d;
}
function init() {
var mapOptions = {
zoom: 15,
center: new google.maps.LatLng(-6.208437004433984, 106.84543132781982),
suppressInfoWindows: true,
};
// Get all html elements for map
var mapElement = document.getElementById('map1');
// Create the Google Map using elements
map = new google.maps.Map(mapElement, mapOptions);
var nextMarkerAt = 0; // Counter for the marker checkpoints.
var nextPoint = null; // The point where to place the next marker.
while (true) {
var routePoints = [ new google.maps.LatLng(47.656, -122.360),
new google.maps.LatLng(47.656, -122.343),
new google.maps.LatLng(47.690, -122.310),
new google.maps.LatLng(47.690, -122.270)];
nextPoint = moveAlongPath(routePoints, nextMarkerAt);
if (nextPoint) {
//Adding marker from localhost
MarkerIcon = "http://192.168.1.1/star.png";
var marker = new google.maps.Marker
({position: nextPoint,
map: map,
icon: MarkerIcon
});
// Add +1000 meters for the next checkpoint.
nextMarkerAt +=1000;
}
else {
// moveAlongPath returned null, so there are no more check points.
break;
}
}
}
Number.prototype.toRad = function () {
return this * Math.PI / 180;
}
Number.prototype.toDeg = function () {
return this * 180 / Math.PI;
}
function moveAlongPath(point, distance, index) {
index = index || 0; // Set index to 0 by default.
var routePoints = [];
for (var i = 0; i < point.length; i++) {
routePoints.push(point[i]);
}
if (index < routePoints.length) {
// There is still at least one point further from this point.
// Construct a GPolyline to use the getLength() method.
var polyline = new google.maps.Polyline({
path: [routePoints[index], routePoints[index + 1]],
strokeColor: '#FF0000',
strokeOpacity: 0.8,
strokeWeight: 2,
fillColor: '#FF0000',
fillOpacity: 0.35
});
// Get the distance from this point to the next point in the polyline.
var distanceToNextPoint = polyline.Distance();
if (distance <= distanceToNextPoint) {
// distanceToNextPoint is within this point and the next.
// Return the destination point with moveTowards().
return moveTowards(routePoints, distance,index);
}
else {
// The destination is further from the next point. Subtract
// distanceToNextPoint from distance and continue recursively.
return moveAlongPath(routePoints,
distance - distanceToNextPoint,
index + 1);
}
}
else {
// There are no further points. The distance exceeds the length
// of the full path. Return null.
return null;
}
}
function moveTowards(point, distance,index) {
var lat1 = point[index].lat.toRad();
var lon1 = point[index].lng.toRad();
var lat2 = point[index+1].lat.toRad();
var lon2 = point[index+1].lng.toRad();
var dLon = (point[index + 1].lng - point[index].lng).toRad();
// Find the bearing from this point to the next.
var brng = Math.atan2(Math.sin(dLon) * Math.cos(lat2),
Math.cos(lat1) * Math.sin(lat2) -
Math.sin(lat1) * Math.cos(lat2) *
Math.cos(dLon));
var angDist = distance / 6371000; // Earth's radius.
// Calculate the destination point, given the source and bearing.
lat2 = Math.asin(Math.sin(lat1) * Math.cos(angDist) +
Math.cos(lat1) * Math.sin(angDist) *
Math.cos(brng));
lon2 = lon1 + Math.atan2(Math.sin(brng) * Math.sin(angDist) *
Math.cos(lat1),
Math.cos(angDist) - Math.sin(lat1) *
Math.sin(lat2));
if (isNaN(lat2) || isNaN(lon2)) return null;
return new google.maps.LatLng(lat2.toDeg(), lon2.toDeg());
}
google.maps.Polyline.prototype.Distance = function () {
var dist = 0;
for (var i = 1; i < this.getPath().getLength(); i++) {
dist += this.getPath().getAt(i).distanceFrom(this.getPath().getAt(i - 1));
}
return dist;
}
google.maps.LatLng.prototype.distanceFrom = function (newLatLng) {
//var R = 6371; // km (change this constant to get miles)
var R = 6378100; // meters
var lat1 = this.lat();
var lon1 = this.lng();
var lat2 = newLatLng.lat();
var lon2 = newLatLng.lng();
var dLat = (lat2 - lat1) * Math.PI / 180;
var dLon = (lon2 - lon1) * Math.PI / 180;
var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
Math.sin(dLon / 2) * Math.sin(dLon / 2);
var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
var d = R * c;
return d;
}
可能最好的方法是计算这些点的位置。
作为基本算法,您可以遍历 Polyline 中的所有点,并计算累积距离 - 如果下一个线段将您超过您的距离,则可以插值到达距离的点 - 然后为此在地图上添加一个感兴趣的点。
我想将Daniel Vassalo的答案移植到iOS,但它没有正常工作,一些标记错位了,直到我进行了更改。
var dLon = (point.lng() - this.lng()).toRad();
到
var dLon = point.lng().toRad() - this.lng().toRad();
所以如果有人遇到标记错位的问题,可以尝试这个方法,或许会有所帮助。
getLength()
函数也假设地球是一个球体,所以在 v2 演示中处理更大的距离时应该也会发生同样的情况。假设地球是一个球体可以使数学计算变得简单许多。 - Daniel Vassallo