Javascript – Drive time polygons with Google Maps API

google mapsgoogle-maps-api-3javascript

I'm looking for a pure (or as-pure-as-possible) Google Maps JavaScript API V3 solution to add drive-time polygon functionality to my Google Maps app.

Basically the workflow is that the user clicks a Marker or point on the map, and a polygon is generated around the Marker/point indicating the area where you can drive within 5 minutes in any direction.

I'm not sure if it is even possible to do this as a pure Google Maps API solution, because it would require too many calls to the Directions Service with random routes and then any that don't fall within the time threshold are thrown away. Not sure if this is feasible or what the best way to engineer this would be. It might be necessary to use some backend processing for this, but I want to rule out out all other options first, but any examples of backend solutions are welcome.

Any help, suggestions, or examples are greatly appreciated. The following are some examples I have come across, but they either aren't a pure Google Maps API solution and/or aren't using drive-times:

  1. Google Maps Utility Library example – draws 1, 2, and 3 minutes drive-time polygons around the point you click on, but it uses Esri's sample Geoprocessing services to run the calculations and generate the polygon geometry, so that's not going to work for my requirements.

  2. 30 mile directions – it's a pure Google Maps API solution using the Directions API and draws a nice polygon, but it doesn't take drive-times into the calculation, just distance from the point to 30 miles out.

  3. Mapnificent – seems to be a pure Google Maps API solution, and they have an API, but they are dealing with public transit times pulled from public transit data, not drive times. So the areas that "open up" as you increase the time indicate where from the point you can get on public transit within the specified time. I want this, but with driving times instead of transit times.

Best Answer

What you call "30 mile directions" is my mashup, though it's old and written with the V2 API. You can use the same principle as that mashup uses, namely calculate a circle around the start point and use its points as destinations, then for each route cut it off when it reaches the desired driving time.

the original link is dead (code snippet below)

In that example, the function shortenAndShow() cuts off the portion of the route that goes beyond 30 miles. Since the 30 mile destination circle has a straight line radius, it is unlikely to be reached because roads have curves, so you reach the 30 miles before you reach the edge of the blue circle. You can do the same, but cutting the route off based on drive time instead of distance.

Performance will depend on the interval, (in degrees), along the circle, at which you set a possible destination point. An attempt every 1 degree will take 10 times more requests than an attempt every 10 degrees (default is 30 degrees).

code snippet:

var map;
var container;
var zoom = 9;
var centerPoint = new google.maps.LatLng(35.149534, -90.04898);
var dirService = new google.maps.DirectionsService();
var centerMarker;
var circleMarkers = Array();
var circlePoints = Array();
var drivePolyPoints = Array();
var searchPolygon, drivePolygon;
var distToDrive = 30; // miles
var pointInterval = 30;
var searchPoints = [];
var polyline;
var polylines = [];
var redIcon8 = "https://maps.gstatic.com/intl/en_us/mapfiles/markers2/measle.png";

function initialize() {
  map = new google.maps.Map(
    document.getElementById("map_canvas"), {
      center: centerPoint,
      zoom: 9,
      mapTypeId: google.maps.MapTypeId.ROADMAP
    });
  google.maps.event.addListener(map, "click", mapClick);
}
google.maps.event.addDomListener(window, "load", initialize);

function mapClick(evt) {
  // map.clearOverlays();
  circleMarkers = Array();
  if (!centerMarker) {
    centerMarker = new google.maps.Marker({
      position: evt.latLng,
      map: map
    });
  } else {
    centerMarker.setMap(null);
    centerMarker.setPosition(evt.latLng);
  }
  centerMarker.setMap(map);
  searchPoints = getCirclePoints(evt.latLng, distToDrive);
  drivePolyPoints = Array();
  getDirections();
}

function getCirclePoints(center, radius) {
  var bounds = new google.maps.LatLngBounds();
  var circlePoints = Array();
  var searchPoints = Array();
  with(Math) {
    var rLat = (radius / 3963.189) * (180 / PI); // miles
    var rLng = rLat / cos(center.lat() * (PI / 180));
    for (var a = 0; a < 361; a++) {
      var aRad = a * (PI / 180);
      var x = center.lng() + (rLng * cos(aRad));
      var y = center.lat() + (rLat * sin(aRad));
      var point = new google.maps.LatLng(parseFloat(y), parseFloat(x), true);
      bounds.extend(point);
      circlePoints.push(point);
      if (a % pointInterval == 0) {
        searchPoints.push(point);
      }
    }
  }
  searchPolygon = new google.maps.Polygon({
    paths: circlePoints,
    strokeColor: '#0000ff',
    strokeWeight: 1,
    strokeOpacity: 1,
    fillColor: '#0000ff',
    fillOpacity: 0.2
  });
  searchPolygon.setMap(map);
  map.fitBounds(bounds);
  return searchPoints;
}

function getDirections() {
  if (!searchPoints.length) {
    return;
  }
  var to = searchPoints.shift();
  var request = {
    origin: centerMarker.getPosition(),
    destination: to,
    travelMode: google.maps.TravelMode.DRIVING
  };
  dirService.route(request, function(result, status) {
    if (status == google.maps.DirectionsStatus.OK) {
      var distance = parseInt(result.routes[0].legs[0].distance.value / 1609);
      var duration = parseFloat(result.routes[0].legs[0].duration.value / 3600).toFixed(2);
      var path = result.routes[0].overview_path;
      var legs = result.routes[0].legs;
      if (polyline && polyline.setPath) {
        polyline.setPath([]);
      } else {
        polyline = new google.maps.Polyline({
          path: [],
          // map: map,
          strokeColor: "#FF0000",
          strokeOpacity: 1
        });
      }
      for (i = 0; i < legs.length; i++) {
        var steps = legs[i].steps;
        for (j = 0; j < steps.length; j++) {
          var nextSegment = steps[j].path;
          for (k = 0; k < nextSegment.length; k++) {
            polyline.getPath().push(nextSegment[k]);
            // bounds.extend(nextSegment[k]);
          }
        }
      }
      // polyline.setMap(map);
      shortenAndShow(polyline);
      getDirections();
    } else {
      console.log("Directions request failed, status=" + status + " [from:" + request.origin + " to:" + request.destination + "]");
      getDirections();
    }
  });
}

function shortenAndShow(polyline) {
  var distToDriveM = distToDrive * 1609;
  var dist = 0;
  var cutoffIndex = 0;
  var copyPoints = Array();
  for (var n = 0; n < polyline.getPath().getLength() - 1; n++) {
    dist += google.maps.geometry.spherical.computeDistanceBetween(polyline.getPath().getAt(n), polyline.getPath().getAt(n + 1));
    //GLog.write(dist + ' - ' + distToDriveM);
    if (dist < distToDriveM) {
      copyPoints.push(polyline.getPath().getAt(n));
    } else {
      break;
    }
  }
  var lastPoint = copyPoints[copyPoints.length - 1];
  var newLine = new google.maps.Polyline({
    path: copyPoints,
    strokeColor: '#ff0000',
    strokeWeight: 2,
    strokeOpacity: 1
  });
  newLine.setMap(map);
  polylines.push(newLine);
  drivePolyPoints.push(lastPoint);
  addBorderMarker(lastPoint, dist)
  if (drivePolyPoints.length > 3) {
    if (drivePolygon) {
      drivePolygon.setMap(null);
    }
    drivePolygon = new google.maps.Polygon({
      paths: drivePolyPoints,
      strokeColor: '#00ff00',
      strokeWeight: 1,
      strokeOpacity: 1,
      fillColor: '#00ff00',
      fillOpacity: 0.4
    });
    drivePolygon.setMap(map);
  }
}

function addBorderMarker(pt, d) {
  var str = pt.lat().toFixed(6) + ',' + pt.lng().toFixed(6) + ' - Driving Distance: ' + (d / 1609).toFixed(2) + ' miles';
  var marker = new google.maps.Marker({
    position: pt,
    icon: redIcon8,
    title: str
  });
  circleMarkers.push(marker);
  marker.setMap(map);
}

function clearOverlays() {
  for (var i = 0; i < circleMarkers.length; i++) {
    circleMarkers[i].setMap(null);
  }
  circleMarkers = [];
  for (var i = 0; i < circlePoints.length; i++) {
    circlePoints[i].setMap(null);
  }
  circlePoints = [];
  for (var i = 0; i < polylines.length; i++) {
    polylines[i].setMap(null);
  }
  polylines = [];
  if (searchPolygon && searchPolygon.setMap) searchPolygon.setMap(null);
  if (drivePolygon && drivePolygon.setMap) drivePolygon.setMap(null);
  if (centerMarker && centerMarker.setMap) centerMarker.setMap(null);
}
html,
body,
#map_canvas {
  height: 100%;
  width: 100%;
  margin: 0px;
  padding: 0px
}
<a href="#" onclick="clearOverlays();">Clear</a>&nbsp;|&nbsp;
<a href="#" onclick="pointInterval=30;clearOverlays();">interval 30 (default)</a>&nbsp;|&nbsp;
<a href="#" onclick="pointInterval=20;clearOverlays();">interval 20</a>&nbsp;|&nbsp;

<script src="https://maps.googleapis.com/maps/api/js?libraries=geometry"></script>
<div id="map_canvas"></div>