أداة فحص واجهة برمجة التطبيقات للطرق

أداة فحص واجهة برمجة تطبيقات الطرق هي أداة تفاعلية يمكنك استخدامها لتجربة Roads API. في ما يلي بعض الاقتراحات لبدء استخدام الأداة:

  • انسخ عنوان URL لطلب Roads API في حقل النص وانقر على تخطيط دورة تدريبية للاطّلاع على نتائج الطلب. ولن تحتاج إلى تضمين مفتاح واجهة برمجة التطبيقات.
  • يمكنك استخدام الأمثلة المميزة لتجربة واجهة برمجة التطبيقات.
  • اختَر علامة لنقطة معيّنة لعرض نافذة معلومات تحتوي على تفاصيل حول النقطة.
  • افتح التجوّل الافتراضي بعد تحميل أحد الأمثلة لمشاهدة العلامات في بانوراما التجوّل الافتراضي.
  • يمكنك تبديل إعداد الدمج للاطّلاع على تأثيره في النتائج.
  • انقر على خيار تبديل المسافات لعرض المسافة بين نقطتين أصليتين أو إخفائها. يتم عرض المسار الذي لم يتم التقاطه كخط أخضر مستقيم من نقطة إلى أخرى. اختَر الخط للاطّلاع على المسافة.

في ما يلي الأمثلة المميزة:

  • مثال 1: نقاط على طول طريق في مدينة بيرمونت، سيدني.
  • مثال 2: نقاط على طول طريق في كانبيرا، إقليم العاصمة الأسترالية.
  • المثال 3: نقاط على طول طريق في مدينة كانبيرا تتضمن نقطة لا يمكن تطبيقها على طريق.
  • المثال 4: طريق في إلكين، كارولاينا الشمالية، مع مسار غير منتظم يوضِّح نتائج تبديل خيار الاستيفاء.

اطّلِع على هذا المثال في ملء الشاشة.

جربه بنفسك

JavaScript

// Replace with your own API key
var API_KEY = 'YOUR_API_KEY';

// Icons for markers
var RED_MARKER = 'https://maps.google.com/mapfiles/ms/icons/red-dot.png';
var GREEN_MARKER = 'https://maps.google.com/mapfiles/ms/icons/green-dot.png';
var BLUE_MARKER = 'https://maps.google.com/mapfiles/ms/icons/blue-dot.png';
var YELLOW_MARKER = 'https://maps.google.com/mapfiles/ms/icons/yellow-dot.png';

// URL for places requests
var PLACES_URL = 'https://maps.googleapis.com/maps/api/place/details/json?' +
                'key=' + API_KEY + '&placeid=';

// URL for Speed limits
var SPEED_LIMIT_URL = 'https://roads.googleapis.com/v1/speedLimits';

var coords;

/**
 * Current Roads API threshold (subject to change without notice)
 * @const {number}
 */
var DISTANCE_THRESHOLD_HIGH = 300;
var DISTANCE_THRESHOLD_LOW = 200;

/**
 * @type Array<ExtendedLatLng>
 */
var originals = [];     // the original input points, a list of ExtendedLatLng

var interpolate = true;
var map;
var placesService;
var originalCoordsLength;

// Settingup Arrays
var infoWindows = [];
var markers = [];
var placeIds = [];
var polylines = [];
var snappedCoordinates = [];
var distPolylines = [];

// Symbol that gets animated along the polyline
var lineSymbol = {
  path: google.maps.SymbolPath.CIRCLE,
  scale: 8,
  strokeColor: '#005db5',
  strokeWidth: '#005db5'
};

// Example 1 - Frolick around Sydney
var eg1 = '-33.870315,151.196532|-33.869979,151.197355|' +
    '-33.870044,151.197712|-33.870358,151.198206|' +
    '-33.870595,151.198376|-33.870640,151.198398|' +
    '-33.870620,151.198449|-33.870951,151.198525|' +
    '-33.871040,151.198528|-33.872031,151.198413';

// Example 2 - Lap around Canberra
var eg2 = '-35.274346,149.130168|-35.278012,149.129583|' +
    '-35.280329,149.129073|-35.280999,149.129293|' +
    '-35.281441,149.129846|-35.281945,149.130034|' +
    '-35.282825,149.129567|-35.283022,149.128811|' +
    '-35.284734,149.128366';

// Example 3 - Path with unsnappable point
var eg3 = '-35.274346,149.094000|-35.278012,149.129583|' +
    '-35.280329,149.129073|-35.280999,149.129293|' +
    '-35.281441,149.129846';

// Example 4 - Drive erratically in Elkin
var eg4 = '36.28881,-80.8525|36.287038,-80.85313|36.286161,-80.85369|' +
    '36.28654,-80.85418|36.2846,-80.84766|36.28355,-80.84669';

// Initialize
function initialize() {
  $('#eg1').click(function(e) {
    $('#coords').val(eg1);
    $('#plot').trigger('click');
  });

  $('#eg2').click(function(e) {
    $('#coords').val(eg2);
    $('#plot').trigger('click');
  });

  $('#eg3').click(function(e) {
    $('#coords').val(eg3);
    $('#plot').trigger('click');
  });

  $('#eg4').click(function(e) {
    $('#coords').val(eg4);
    $('#plot').trigger('click');
  });

  $('#toggle').click(function(e) {
     if ($('#panel').css("display") != 'none') {
        $('#toggle').html("+");
        $('#panel').hide();
     } else {
        $('#toggle').html("&mdash;");
        $('#panel').show();
     }
  });

  // Centre the map on Sydney
  var mapOptions = {
    center: {'lat': -33.870315, 'lng': 151.196532},
    zoom: 14
  };

  // Map object
  map = new google.maps.Map(document.getElementById('map'), mapOptions);

  // Places object
  placesService = new google.maps.places.PlacesService(map);

  // Reset the map to a clean state and reset all variables
  // used for displaying each request
  function clearMap() {
    // Clear the polyline
    for (var i = 0; i < polylines.length; i++) {
      polylines[i].setMap(null);
    }
    // Clear all markers
    for (var i = 0; i < markers.length; i++) {
      markers[i].setMap(null);
    }
    // Clear all the distance polylines
    for (var i = 0; i < distPolylines.length; i++) {
      distPolylines[i].setMap(null);
    }
    // Clear all info windows
    for (var i = 0; i < infoWindows.length; i++) {
      infoWindows[i].close();
    }

    // Empty everything
    polylines = [];
    markers = [];
    distPolylines = [];
    snappedCoordinates = [];
    placeIds = [];
    infoWindows = [];
    $('#unsnappedPoints').empty();
    $('#warningMessage').empty();
  }

  // Parse the value in the input element
  // to get all coordinates
  function parseCoordsFromQuery(input) {
    var coords;
    input = decodeURIComponent(input);
    if (input.split('path=').length > 1) {
      input = decodeURIComponent(input);
      // Split on the ampersand to get all params
      var parts = input.split('&');
      // Check each part to see if it starts with 'path='
      // grabbing out the coordinates if it does
      for (var i = 0; i < parts.length; i++) {
        if (parts[i].split('path=').length > 1) {
          coords = parts[i].split('path=')[1];
          break;
        }
      }
    } else {
      coords = decodeURIComponent(input);
    }

    // Parse the "Lat,Lng|..." coordinates into an array of ExtendedLatLng
    originals = [];
    var points = coords.split('|');
    for (var i = 0; i < points.length; i++) {
      var point = points[i].split(',');
      originals.push({lat: Number(point[0]), lng: Number(point[1]), index:i});
    }

    return coords;
  }

  // Clear the map of any old data and plot the request
  $('#plot').click(function(e) {
    clearMap();
    bendAndSnap();
    drawDistance();
    e.preventDefault();
  });

  // Make AJAX request to the snapToRoadsAPI
  // with coordinates parsed from text input element.
  function bendAndSnap() {
    coords = parseCoordsFromQuery($('#coords').val());
    location.hash = coords;
    $.ajax({
      type: 'GET',
      url: 'https://roads.googleapis.com/v1/snapToRoads',
      data: {
        interpolate: $('#interpolate').is(':checked'),
        key: API_KEY,
        path: coords
      },
      success: function(data) {
        $('#requestURL').html('<a target="blank" href="' +
            this.url + '">Request URL</a>');
        processSnapToRoadResponse(data);
        drawSnappedPolyline(snappedCoordinates);
        drawOriginals(originals);
        fitBounds(markers);
      },
      error: function() {
        $('#requestURL').html('<strong>That query didn\'t work :(</strong>' +
            '<p>Try looking at the <a href="' + this.url +
            '">Request URL</a></p>');
        clearMap();
      }
    });
  }

  // Toggle the distance polylines of the original points to show on the maps
  $('#distance').click(function(e) {
    for (var i = 0; i < distPolylines.length; i++) {
        distPolylines[i].setVisible(!distPolylines[i].getVisible());
    }
    // Clear all infoWindows associated with distance polygons on toggle
    for (var i = 0; i < infoWindows.length; i++) {
      if (infoWindows[i].dist) {
        infoWindows[i].close();
      }
    }
    e.preventDefault();
  });

  /**
   * Compute the distance between each original point and create a polyline
   * for each pair. Polylines are initially hidden on creation
   */
  function drawDistance() {
    for (var i = 0; i < originals.length - 1; i++) {
      var origin = new google.maps.LatLng(originals[i]);
      var destination = new google.maps.LatLng(originals[i+1]);
      var distance =
    google.maps.geometry.spherical.computeDistanceBetween(origin, destination);

      // Round the distance value to two decimal places
      distance = Math.round(distance * 100) / 100;

      var color;
      var weight;
      if (distance > DISTANCE_THRESHOLD_HIGH) {
        color = '#CC0022';
        weight = 7;
      } else if (distance < DISTANCE_THRESHOLD_HIGH &&
                 distance > DISTANCE_THRESHOLD_LOW) {
        color = '#FF6600';
        weight = 6;
      } else {
        color = '#22CC00';
        weight = 5;
      }
      var polyline = new google.maps.Polyline({
        strokeColor: color,
        strokeOpacity: 0.4,
        strokeWeight: weight,
        geodesic: true,
        visible: false,
        map: map
      });
      polyline.setPath([origin, destination]);

      distPolylines.push(polyline);
      infoWindows.push(addPolyWindow(polyline, distance, i));
    }
  }

  /**
   * Add an info window to the polyline displaying the original
   * points and the distance
   */
  function addPolyWindow(polyline, distance, index) {
    var infoWindow = new google.maps.InfoWindow();
    var content = '<div style="width:100%"><p>' +
        '<strong>Original Index: </strong>' + index + '<br>' +
        '<strong>Coords:</strong> (' +
        originals[index].lat + ',' + originals[index].lng + ')' +
        '<br>to<br>' +
        '<strong>Original Index: </strong>' + (index+1) + '<br>' +
        '<strong>Coords:</strong> (' +
        originals[index+1].lat + ',' + originals[index+1].lng + ')<br><br>' +
        '<strong>Distance:  </strong>' +  distance + ' m<br>';
    if (distance > DISTANCE_THRESHOLD_HIGH) {
      content += '<span style="color:#CC0022;font-style:italic">' +
          '*Large distance (>300m) may affect snapping</span><br>' +
          'Please see <a href="https://developers.google.com/maps/' +
          'documentation/roads/snap#parameter_usage" ' +
          'target="_blank">Roads API documentation</a>';
    }
    content += '</p></div>';

    infoWindow.setContent(content);
    infoWindow.dist = true;

    polyline.addListener('click', function(e) {
        infoWindow.setPosition(e.latLng);
        infoWindow.open(map);
    });

    polyline.addListener('mouseover', function(e) {
        polyline.setOptions({strokeOpacity: 1.0});
    });

    polyline.addListener('mouseout', function(e) {
        polyline.setOptions({strokeOpacity: 0.4});
    });

    return infoWindow;
  }

  // Parse the value in the input element
  // to get all coordinates
  function getMissingPoints(originalIndexes, originalCoordsLength) {
    var unsnappedPoints = [];
    var coordsArray = coords.split('|');
    var hasMissingCoords = false;
    for (var i = 0; i < originalCoordsLength; i++) {
      if (originalIndexes.indexOf(i) < 0) {
        hasMissingCoords = true;
        var latlng = {
          'lat': parseFloat(coordsArray[i].split(',')[0]),
          'lng': parseFloat(coordsArray[i].split(',')[1])
        };

        unsnappedPoints.push(latlng);
        latlng.unsnapped = true;
      }
    }
    return unsnappedPoints;
  }

  // Parse response from snapToRoads API request
  // Store all coordinates in response
  // Calls functions to add markers to map for unsnapped coordinates
  function processSnapToRoadResponse(data) {
    var originalIndexes = [];
    var unsnappedMessage = '';

    for (var i = 0; i < data.snappedPoints.length; i++) {
      var latlng = {
        'lat': data.snappedPoints[i].location.latitude,
        'lng': data.snappedPoints[i].location.longitude
      };
      var interpolated = true;

      if (data.snappedPoints[i].originalIndex != undefined) {
        interpolated = false;
        originalIndexes.push(data.snappedPoints[i].originalIndex);
        latlng.originalIndex = data.snappedPoints[i].originalIndex;
      }

      latlng.interpolated = interpolated;
      snappedCoordinates.push(latlng);
      placeIds.push(data.snappedPoints[i].placeId);

      // Cross-reference the original point and this snapped point.
      latlng.related = originals[latlng.originalIndex];
      originals[latlng.originalIndex].related = latlng;
    }

    var unsnappedPoints = getMissingPoints(
        originalIndexes,
        coords.split('|').length
    );

    for (var i = 0; i < unsnappedPoints.length; i++) {
      var marker = addMarker(unsnappedPoints[i]);
      var infowindow = addBasicInfoWindow(marker, unsnappedPoints[i], i);
      infoWindows.push(infowindow);

      unsnappedMessage += unsnappedPoints[i].lat + ',' +
          unsnappedPoints[i].lng + '<br>';
    }

    if (unsnappedPoints.length) {
      unsnappedMessage = '<strong>' +
         'These points weren\'t snapped: ' +
         '</strong><br>' + unsnappedMessage;
      $('#unsnappedPoints').html(unsnappedMessage);
    }

    if (data.warningMessage) {
      $('#warningMessage').html('<span style="color:#CC0022;' +
          'font-style:italic;font-size:12px">' + data.warningMessage + '<br/>' +
          '<a target="_blank" href="https://developers.google.com/maps/' +
          'documentation/roads/snap">https://developers.google.com/maps/' +
          'documentation/roads/snap</a>');
      $('#distance').trigger('click');
    }
  }

  // Draw the polyline for the snapToRoads API response
  // Call functions to add markers and infowindows for each snapped
  // point along the polyline.
  function drawSnappedPolyline(snappedCoords) {
    var snappedPolyline = new google.maps.Polyline({
      path: snappedCoords,
      strokeColor: '#005db5',
      strokeWeight: 6,
      icons: [{
        icon: lineSymbol,
        offset: '100%'
      }]
    });

    snappedPolyline.setMap(map);
    animateCircle(snappedPolyline);

    polylines.push(snappedPolyline);

    for (var i = 0; i < snappedCoords.length; i++) {
      var marker = addMarker(snappedCoords[i]);
      var infoWindow = addDetailedInfoWindow(marker,
          snappedCoords[i],
          placeIds[i]);
      infoWindows.push(infoWindow);
    }
  }

  // Draw the original input.
  // Call functions to add markers and infowindows for each point.
  function drawOriginals(originalCoords) {
    for (var i = 0; i < originalCoords.length; i++) {
      var marker = addMarker(originalCoords[i]);
      var infoWindow = addBasicInfoWindow(marker, originalCoords[i], i);
      infoWindows.push(infoWindow);
    }
  }

  // Infowindow used for unsnappable coordinates
  function addBasicInfoWindow(marker, coords, index) {
    var infowindow = new google.maps.InfoWindow();
    var content = '<div style="width:99%"><p>' +
        '<strong>Lat/Lng:</strong><br>' +
        '(' + coords.lat + ',' + coords.lng + ')<br>' +
        (index != undefined ? '<strong>Index: </strong>' + index : '') +
        '</p></div>';

    infowindow.setContent(content);

    google.maps.event.addListener(marker, 'click', function() {
      openInfoWindow(infowindow, marker);
    });

    return infowindow;
  }

  // Infowindow used for snapped points
  // Makes request to Places Details API to get data about each
  // Place ID.
  // Requests speed limit of each location using Roads SpeedLimit API
  function addDetailedInfoWindow(marker, coords, placeId) {
    var infowindow = new google.maps.InfoWindow();
    var placesRequestUrl = PLACES_URL + placeId;
    var detailsUrl = '<a target="_blank" href="' +
        placesRequestUrl + '">' +
        placeId + '</a></li>';

    // On click we make a request to the Places API
    // This is to avoid OVER_QUERY_LIMIT if we just requested everything
    // at the same time
    google.maps.event.addListener(marker, 'click', function() {
      content = '<div style="width:99%"><p>';

      function finishInfoWindow(placeDetails) {
        content += '<strong>Place Details: </strong>' + placeDetails + '<br>' +
            '<strong>' +
            (coords.interpolated ? 'Coords' : 'Snapped coords') +
            ': </strong>' +
            '(' + coords.lat.toFixed(5) + ',' +
            coords.lng.toFixed(5) + ')<br>';

        if (!(coords.interpolated)) {
          var original = originals[coords.originalIndex];
          content += '<strong>Original coords: </strong>' +
              '(' + original.lat + ',' + original.lng + ')<br>' +
              '<strong>Original Index: </strong>' +
              coords.originalIndex;
        }
        content += '</p></div>';
        infowindow.setContent(content);
        openInfoWindow(infowindow, marker);
      };

      getPlaceDetails(placeId, function(place) {
        if (place.name) {
          content += '<strong>' + place.name + '</strong><br>';
        }
        getSpeedLimit(placeId, function(data) {
          if (data.speedLimits) {
            content += '<strong>Speed Limit: </strong>' +
                data.speedLimits[0].speedLimit + ' km/h <br>';
          }
          finishInfoWindow(detailsUrl);
        });
      }, function() { finishInfoWindow("<em>None available</em>"); });
    });
    return infowindow;
  }

  // Avoid infoWindows staying open if the pano changes
  listenForPanoChange();

  // If the user came to the page with a particular path or URL,
  // immediately plot it.
  if (location.hash.length > 1) {
    coords = parseCoordsFromQuery(location.hash.slice(1));
    $('#coords').val(coords);
    $('#plot').click();
  }
} // End init function

// Call the initialize function once everything has loaded
google.maps.event.addDomListener(window, 'load', initialize);

// Load the control panel in a floating div if it is not loaded in an iframe
// after the textarea has been rendered
$("#coords").ready(function() {
    if (!window.frameElement) {
       $('#panel').addClass("floating panel");
       $('#button-div').addClass("button-div");
       $('#coords').removeClass("coords-large").addClass("coords-small");
       $('#toggle').show();
       $('#map').height('100%');
    }
});

/**
*  latlng literal with extra properties to use with the RoadsAPI
*  @typedef {Object} ExtendedLatLng
*   lat:string|float
*   lng:string|float
*   interpolated:boolean
*   unsnapped:boolean
*/

/**
 * Add a line to the map for highlighting the connection between two
 * markers while the mouse is over it.
 * @param {ExtendedLatLng} from - The origin of the line
 * @param {ExtendedLatLng} to - The destination of the line
 * @return {!Object} line - the polyline object created
 */
function addOverline(from, to) {
  return addLine("overline", from, to, '#ff77ff', 4, 1.0, 2.0, false);
}

/**
 * Add a line to the map for highlighting the connection between two
 * markers while the mouse is NOT over it.
 * @param {ExtendedLatLng} from - The origin of the line
 * @param {ExtendedLatLng} to - The destination of the line
 * @return {!Object} line - the polyline object created
 */
function addOutline(from, to) {
  return addLine("outline", from, to, '#bb33bb', 2, 0.5, 1.35, true);
}

/**
 * Add a line to the map for highlighting the connection between two
 * markers.
 * @param {string}         attrib  - The attribute to use for managing the line
 * @param {ExtendedLatLng} from    - The origin of the line
 * @param {ExtendedLatLng} to      - The destination of the line
 * @param {string}         color   - The color of the line
 * @param {number}         weight  - The weight of the line
 * @param {number}         opacity - The opacity of the line (0..1)
 * @param {number}         scale   - The scale of the arrow-head (pt)
 * @param {boolean}        visible - The visibility of the line
 * @return {!Object}       line    - the polyline object created
 */
function addLine(attrib, from, to, color, weight, opacity, scale, visible) {
  from[attrib] = new google.maps.Polyline({
    path:         [from, to],
    strokeColor:  color,
    strokeWeight:  weight,
    strokeOpacity: opacity,
    icons:[{
      offset: "0%",
      icon: {
        scale: scale/*pt*/,
        path:  google.maps.SymbolPath.BACKWARD_CLOSED_ARROW
      }
    }]
  });
  from[attrib].setVisible(visible);
  from[attrib].setMap(map);
  to[attrib] = from[attrib];
  polylines.push(from[attrib]);
  return from[attrib];
}

/**
 * Add a pair of lines to the map for highlighting the connection between two
 * markers; one visible while the mouse is over the marker (the "overline"),
 * the other while it is not (the "outline").
 * @param {ExtendedLatLng} from - The origin of the line (the original input)
 * @param {ExtendedLatLng} to - The destination of the line (the snapped point)
 * @return {!Object} line - the polyline object created
 */
function addCorrespondence(coords, marker) {
  if (!coords.overline) { addOverline(coords, coords.related); }
  if (!coords.outline)  { addOutline(coords, coords.related); }

  marker.addListener('mouseover', function(mevt) {
    coords.outline.setVisible(false);
    coords.overline.setVisible(true);
    coords.related.marker.setOpacity(1.0);
  });
  marker.addListener('mouseout', function(mevt) {
    coords.overline.setVisible(false);
    coords.outline.setVisible(true);
    coords.related.marker.setOpacity(0.5);
  });
}

/**
 * Add a marker to the map and check for special 'interpolated'
 * and 'unsnapped' properties to control which colour marker is used
 * @param {ExtendedLatLng} coords - Coords of where to add the marker
 * @return {!Object} marker - the marker object created
 */
function addMarker(coords) {
  var marker = new google.maps.Marker({
    position: coords,
    title: coords.lat + ',' + coords.lng,
    map: map,
    opacity: 0.5,
    icon: RED_MARKER
  });

  // Coord should NEVER be interpolated AND unsnapped
  if (coords.interpolated) {
    marker.setIcon(BLUE_MARKER);
  } else if (!coords.related) {
    marker.setIcon(YELLOW_MARKER);
  } else if (coords.originalIndex != undefined) {
    marker.setIcon(RED_MARKER);
    addCorrespondence(coords, marker);
  } else {
    marker.setIcon({url: GREEN_MARKER,
                    scaledSize: {width: 20, height: 20}});
    addCorrespondence(coords, marker);
  }

  // Make markers change opacity when the mouse scrubs across them
  marker.addListener('mouseover', function(mevt) {
    marker.setOpacity(1.0);
  });
  marker.addListener('mouseout', function(mevt) {
    marker.setOpacity(0.5);
  });

  coords.marker = marker;  // Save a reference for easy access later
  markers.push(marker);

  return marker;
}

/**
 * Animate an icon along a polyline
 * @param {Object} polyline The line to animate the icon along
 */
function animateCircle(polyline) {
  var count = 0;
  // fallback icon if the poly has no icon to animate
  var defaultIcon = [
    {
      icon: lineSymbol,
      offset: '100%'
    }
  ];
  window.setInterval(function() {
    count = (count + 1) % 200;
    var icons = polyline.get('icons') || defaultIcon;
    icons[0].offset = (count / 2) + '%';
    polyline.set('icons', icons);
  }, 20);
}

/**
 * Fit the map bounds to the current set of markers
 * @param {Array<Object>} markers Array of all map markers
 */
function fitBounds(markers) {
  var bounds = new google.maps.LatLngBounds;
  for (var i = 0; i < markers.length; i++) {
    bounds.extend(markers[i].getPosition());
  }
  map.fitBounds(bounds);
}

/**
 * Uses Places library to get Place Details for a Place ID
 * @param {string}   placeId         The Place ID to look up
 * @param {Function} foundCallback   Called if the place is found
 * @param {Function} missingCallback Called if nothing is found
 * @param {Function} errorCallback   Called if request fails
 */
function getPlaceDetails(placeId,
                         foundCallback, missingCallback, errorCallback) {
  var request = {
    placeId: placeId
  };

  placesService.getDetails(request, function(place, status) {
    if (status == google.maps.places.PlacesServiceStatus.OK) {
      foundCallback(place);
    } else if (status == google.maps.places.PlacesServiceStatus.NOT_FOUND) {
      missingCallback();
    } else if (errorCallback) {
      errorCallback();
    }
  });
}

/**
 * AJAX request to the Roads Speed Limit API.
 * Request the speed limit for the Place ID
 * @param {string}   placeId         Place ID to request the speed limit for
 * @param {Function} successCallback Called if request is successful
 * @param {Function} errorCallback   Called if request fails
 */
function getSpeedLimit(placeId, successCallback, errorCallback) {
  $.ajax({
    type: 'GET',
    url: SPEED_LIMIT_URL,
    data: {
      placeId: placeId,
      key: API_KEY
    },
    success: successCallback,
    error: errorCallback
  });
}

/**
 * Open an infowindow on either the map or the active streetview pano
 * @param {Object} infowindow Infowindow to be opened
 * @param {Object} marker Marker the infowindow is anchored to
 */
function openInfoWindow(infowindow, marker) {
  // If streetView is visible display the infoWindow over the pano
  // and anchor to the marker
  if (map.getStreetView().getVisible()) {
    infowindow.open(map.getStreetView(), marker);
  }
  // Otherwise open it on the map and anchor to the marker
  else {
    infowindow.open(map, marker);
  }
}

/**
 * Add event listener to for when the active pano changes
 */
function listenForPanoChange() {
  var pano = map.getStreetView();

  // Close all open markers when the pano changes
  google.maps.event.addListener(pano, 'position_changed', function() {
    closeAllInfoWindows(infoWindows);
  });
}

/**
 * Close all open infoWindows
 * @param {Array<Object>} infoWindows - all infowindow objects
 */
function closeAllInfoWindows(infoWindows) {
  for (var i = 0; i < infoWindows.length; i++) {
    infoWindows[i].close();
  }
}

    

JavaScript + HTML

<!DOCTYPE html>
<html>
  <head>
    <title>Roads API Inspector</title>
    <style type="text/css">
      html, body {
        height: 100%;
        margin: 0;
        padding: 0;
        font-family: Roboto, Noto, sans-serif;
      }

      #map {
        height: 500px;
      }

      #interpolate {
        width: 2em;
        height: 2em;
      }

      #coords {
        resize: vertical;
        min-height: 75px;
        max-height: 200px;
      }

      .block {
        clear: both;
        margin: 1.5em auto;
        text-align: center;
      }

      #legend {
        float: center;
        margin: 5px 15px;
        font-size: 13px;
      }

      .button {
      display: inline-block;
      position: relative;
      border: 0;
      padding: 0 1.7em;
      min-width: 120px;
      height: 32px;
      line-height: 32px;
      border-radius: 2px;
      font-size: 0.9em;
      background-color: #fff;
      color: #646464;
    }

    .button.narrow {
      width: 60px;
    }

    .button.grey {
      background-color: #eee;
    }

    .button.blue {
      background-color: #4285f4;
      color: #fff;
    }

    .button.green {
      background-color: #0f9d58;
      color: #fff;
    }

    .button.raised {
      transition: box-shadow 0.2s cubic-bezier(0.4, 0, 0.2, 1);
      transition-delay: 0.2s;
      box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.26);
    }

    .button.raised:active {
      box-shadow: 0 8px 17px 0 rgba(0, 0, 0, 0.2);
      transition-delay: 0s;
    }

    .floating {
      position: absolute;
      top: 10px;
      right: 10px;
      z-index: 5;
      background-color: rgba(255, 255, 255, 0.75);
      padding: 1px;
      border: 1px solid #999;
      text-align: center;
      line-height: 18px;
    }

    .floating.panel {
      width: 400px;
    }

    .coords-small {
      width: 350px;
    }

    .coords-large {
      width: 400px;
    }

    .button-div {
      padding: 0px 50px;
      width: 300px;
      line-height: 40px;
    }

    #toggle {
      width: 25px;
      z-index: 10;
      cursor: default;
      font-size: 2em;
      padding: 1px;
      color: #999;
      display: none;
    }

    </style>
    <script type="text/javascript" src="https://maps.googleapis.com/maps/api/js?libraries=places,geometry&key=AIzaSyAAUHO6lMMnE2VZMRYmAfVbCYCgsEEqNyM">
    </script>
    <script src="https://www.gstatic.com/external_hosted/jquery2.min.js"></script>
    <script>
    // Replace with your own API key
var API_KEY = 'YOUR_API_KEY';

// Icons for markers
var RED_MARKER = 'https://maps.google.com/mapfiles/ms/icons/red-dot.png';
var GREEN_MARKER = 'https://maps.google.com/mapfiles/ms/icons/green-dot.png';
var BLUE_MARKER = 'https://maps.google.com/mapfiles/ms/icons/blue-dot.png';
var YELLOW_MARKER = 'https://maps.google.com/mapfiles/ms/icons/yellow-dot.png';

// URL for places requests
var PLACES_URL = 'https://maps.googleapis.com/maps/api/place/details/json?' +
                'key=' + API_KEY + '&placeid=';

// URL for Speed limits
var SPEED_LIMIT_URL = 'https://roads.googleapis.com/v1/speedLimits';

var coords;

/**
 * Current Roads API threshold (subject to change without notice)
 * @const {number}
 */
var DISTANCE_THRESHOLD_HIGH = 300;
var DISTANCE_THRESHOLD_LOW = 200;

/**
 * @type Array<ExtendedLatLng>
 */
var originals = [];     // the original input points, a list of ExtendedLatLng

var interpolate = true;
var map;
var placesService;
var originalCoordsLength;

// Settingup Arrays
var infoWindows = [];
var markers = [];
var placeIds = [];
var polylines = [];
var snappedCoordinates = [];
var distPolylines = [];

// Symbol that gets animated along the polyline
var lineSymbol = {
  path: google.maps.SymbolPath.CIRCLE,
  scale: 8,
  strokeColor: '#005db5',
  strokeWidth: '#005db5'
};

// Example 1 - Frolick around Sydney
var eg1 = '-33.870315,151.196532|-33.869979,151.197355|' +
    '-33.870044,151.197712|-33.870358,151.198206|' +
    '-33.870595,151.198376|-33.870640,151.198398|' +
    '-33.870620,151.198449|-33.870951,151.198525|' +
    '-33.871040,151.198528|-33.872031,151.198413';

// Example 2 - Lap around Canberra
var eg2 = '-35.274346,149.130168|-35.278012,149.129583|' +
    '-35.280329,149.129073|-35.280999,149.129293|' +
    '-35.281441,149.129846|-35.281945,149.130034|' +
    '-35.282825,149.129567|-35.283022,149.128811|' +
    '-35.284734,149.128366';

// Example 3 - Path with unsnappable point
var eg3 = '-35.274346,149.094000|-35.278012,149.129583|' +
    '-35.280329,149.129073|-35.280999,149.129293|' +
    '-35.281441,149.129846';

// Example 4 - Drive erratically in Elkin
var eg4 = '36.28881,-80.8525|36.287038,-80.85313|36.286161,-80.85369|' +
    '36.28654,-80.85418|36.2846,-80.84766|36.28355,-80.84669';

// Initialize
function initialize() {
  $('#eg1').click(function(e) {
    $('#coords').val(eg1);
    $('#plot').trigger('click');
  });

  $('#eg2').click(function(e) {
    $('#coords').val(eg2);
    $('#plot').trigger('click');
  });

  $('#eg3').click(function(e) {
    $('#coords').val(eg3);
    $('#plot').trigger('click');
  });

  $('#eg4').click(function(e) {
    $('#coords').val(eg4);
    $('#plot').trigger('click');
  });

  $('#toggle').click(function(e) {
     if ($('#panel').css("display") != 'none') {
        $('#toggle').html("+");
        $('#panel').hide();
     } else {
        $('#toggle').html("&mdash;");
        $('#panel').show();
     }
  });

  // Centre the map on Sydney
  var mapOptions = {
    center: {'lat': -33.870315, 'lng': 151.196532},
    zoom: 14
  };

  // Map object
  map = new google.maps.Map(document.getElementById('map'), mapOptions);

  // Places object
  placesService = new google.maps.places.PlacesService(map);

  // Reset the map to a clean state and reset all variables
  // used for displaying each request
  function clearMap() {
    // Clear the polyline
    for (var i = 0; i < polylines.length; i++) {
      polylines[i].setMap(null);
    }
    // Clear all markers
    for (var i = 0; i < markers.length; i++) {
      markers[i].setMap(null);
    }
    // Clear all the distance polylines
    for (var i = 0; i < distPolylines.length; i++) {
      distPolylines[i].setMap(null);
    }
    // Clear all info windows
    for (var i = 0; i < infoWindows.length; i++) {
      infoWindows[i].close();
    }

    // Empty everything
    polylines = [];
    markers = [];
    distPolylines = [];
    snappedCoordinates = [];
    placeIds = [];
    infoWindows = [];
    $('#unsnappedPoints').empty();
    $('#warningMessage').empty();
  }

  // Parse the value in the input element
  // to get all coordinates
  function parseCoordsFromQuery(input) {
    var coords;
    input = decodeURIComponent(input);
    if (input.split('path=').length > 1) {
      input = decodeURIComponent(input);
      // Split on the ampersand to get all params
      var parts = input.split('&');
      // Check each part to see if it starts with 'path='
      // grabbing out the coordinates if it does
      for (var i = 0; i < parts.length; i++) {
        if (parts[i].split('path=').length > 1) {
          coords = parts[i].split('path=')[1];
          break;
        }
      }
    } else {
      coords = decodeURIComponent(input);
    }

    // Parse the "Lat,Lng|..." coordinates into an array of ExtendedLatLng
    originals = [];
    var points = coords.split('|');
    for (var i = 0; i < points.length; i++) {
      var point = points[i].split(',');
      originals.push({lat: Number(point[0]), lng: Number(point[1]), index:i});
    }

    return coords;
  }

  // Clear the map of any old data and plot the request
  $('#plot').click(function(e) {
    clearMap();
    bendAndSnap();
    drawDistance();
    e.preventDefault();
  });

  // Make AJAX request to the snapToRoadsAPI
  // with coordinates parsed from text input element.
  function bendAndSnap() {
    coords = parseCoordsFromQuery($('#coords').val());
    location.hash = coords;
    $.ajax({
      type: 'GET',
      url: 'https://roads.googleapis.com/v1/snapToRoads',
      data: {
        interpolate: $('#interpolate').is(':checked'),
        key: API_KEY,
        path: coords
      },
      success: function(data) {
        $('#requestURL').html('<a target="blank" href="' +
            this.url + '">Request URL</a>');
        processSnapToRoadResponse(data);
        drawSnappedPolyline(snappedCoordinates);
        drawOriginals(originals);
        fitBounds(markers);
      },
      error: function() {
        $('#requestURL').html('<strong>That query didn\'t work :(</strong>' +
            '<p>Try looking at the <a href="' + this.url +
            '">Request URL</a></p>');
        clearMap();
      }
    });
  }

  // Toggle the distance polylines of the original points to show on the maps
  $('#distance').click(function(e) {
    for (var i = 0; i < distPolylines.length; i++) {
        distPolylines[i].setVisible(!distPolylines[i].getVisible());
    }
    // Clear all infoWindows associated with distance polygons on toggle
    for (var i = 0; i < infoWindows.length; i++) {
      if (infoWindows[i].dist) {
        infoWindows[i].close();
      }
    }
    e.preventDefault();
  });

  /**
   * Compute the distance between each original point and create a polyline
   * for each pair. Polylines are initially hidden on creation
   */
  function drawDistance() {
    for (var i = 0; i < originals.length - 1; i++) {
      var origin = new google.maps.LatLng(originals[i]);
      var destination = new google.maps.LatLng(originals[i+1]);
      var distance =
    google.maps.geometry.spherical.computeDistanceBetween(origin, destination);

      // Round the distance value to two decimal places
      distance = Math.round(distance * 100) / 100;

      var color;
      var weight;
      if (distance > DISTANCE_THRESHOLD_HIGH) {
        color = '#CC0022';
        weight = 7;
      } else if (distance < DISTANCE_THRESHOLD_HIGH &&
                 distance > DISTANCE_THRESHOLD_LOW) {
        color = '#FF6600';
        weight = 6;
      } else {
        color = '#22CC00';
        weight = 5;
      }
      var polyline = new google.maps.Polyline({
        strokeColor: color,
        strokeOpacity: 0.4,
        strokeWeight: weight,
        geodesic: true,
        visible: false,
        map: map
      });
      polyline.setPath([origin, destination]);

      distPolylines.push(polyline);
      infoWindows.push(addPolyWindow(polyline, distance, i));
    }
  }

  /**
   * Add an info window to the polyline displaying the original
   * points and the distance
   */
  function addPolyWindow(polyline, distance, index) {
    var infoWindow = new google.maps.InfoWindow();
    var content = '<div style="width:100%"><p>' +
        '<strong>Original Index: </strong>' + index + '<br>' +
        '<strong>Coords:</strong> (' +
        originals[index].lat + ',' + originals[index].lng + ')' +
        '<br>to<br>' +
        '<strong>Original Index: </strong>' + (index+1) + '<br>' +
        '<strong>Coords:</strong> (' +
        originals[index+1].lat + ',' + originals[index+1].lng + ')<br><br>' +
        '<strong>Distance:  </strong>' +  distance + ' m<br>';
    if (distance > DISTANCE_THRESHOLD_HIGH) {
      content += '<span style="color:#CC0022;font-style:italic">' +
          '*Large distance (>300m) may affect snapping</span><br>' +
          'Please see <a href="https://developers.google.com/maps/' +
          'documentation/roads/snap#parameter_usage" ' +
          'target="_blank">Roads API documentation</a>';
    }
    content += '</p></div>';

    infoWindow.setContent(content);
    infoWindow.dist = true;

    polyline.addListener('click', function(e) {
        infoWindow.setPosition(e.latLng);
        infoWindow.open(map);
    });

    polyline.addListener('mouseover', function(e) {
        polyline.setOptions({strokeOpacity: 1.0});
    });

    polyline.addListener('mouseout', function(e) {
        polyline.setOptions({strokeOpacity: 0.4});
    });

    return infoWindow;
  }

  // Parse the value in the input element
  // to get all coordinates
  function getMissingPoints(originalIndexes, originalCoordsLength) {
    var unsnappedPoints = [];
    var coordsArray = coords.split('|');
    var hasMissingCoords = false;
    for (var i = 0; i < originalCoordsLength; i++) {
      if (originalIndexes.indexOf(i) < 0) {
        hasMissingCoords = true;
        var latlng = {
          'lat': parseFloat(coordsArray[i].split(',')[0]),
          'lng': parseFloat(coordsArray[i].split(',')[1])
        };

        unsnappedPoints.push(latlng);
        latlng.unsnapped = true;
      }
    }
    return unsnappedPoints;
  }

  // Parse response from snapToRoads API request
  // Store all coordinates in response
  // Calls functions to add markers to map for unsnapped coordinates
  function processSnapToRoadResponse(data) {
    var originalIndexes = [];
    var unsnappedMessage = '';

    for (var i = 0; i < data.snappedPoints.length; i++) {
      var latlng = {
        'lat': data.snappedPoints[i].location.latitude,
        'lng': data.snappedPoints[i].location.longitude
      };
      var interpolated = true;

      if (data.snappedPoints[i].originalIndex != undefined) {
        interpolated = false;
        originalIndexes.push(data.snappedPoints[i].originalIndex);
        latlng.originalIndex = data.snappedPoints[i].originalIndex;
      }

      latlng.interpolated = interpolated;
      snappedCoordinates.push(latlng);
      placeIds.push(data.snappedPoints[i].placeId);

      // Cross-reference the original point and this snapped point.
      latlng.related = originals[latlng.originalIndex];
      originals[latlng.originalIndex].related = latlng;
    }

    var unsnappedPoints = getMissingPoints(
        originalIndexes,
        coords.split('|').length
    );

    for (var i = 0; i < unsnappedPoints.length; i++) {
      var marker = addMarker(unsnappedPoints[i]);
      var infowindow = addBasicInfoWindow(marker, unsnappedPoints[i], i);
      infoWindows.push(infowindow);

      unsnappedMessage += unsnappedPoints[i].lat + ',' +
          unsnappedPoints[i].lng + '<br>';
    }

    if (unsnappedPoints.length) {
      unsnappedMessage = '<strong>' +
         'These points weren\'t snapped: ' +
         '</strong><br>' + unsnappedMessage;
      $('#unsnappedPoints').html(unsnappedMessage);
    }

    if (data.warningMessage) {
      $('#warningMessage').html('<span style="color:#CC0022;' +
          'font-style:italic;font-size:12px">' + data.warningMessage + '<br/>' +
          '<a target="_blank" href="https://developers.google.com/maps/' +
          'documentation/roads/snap">https://developers.google.com/maps/' +
          'documentation/roads/snap</a>');
      $('#distance').trigger('click');
    }
  }

  // Draw the polyline for the snapToRoads API response
  // Call functions to add markers and infowindows for each snapped
  // point along the polyline.
  function drawSnappedPolyline(snappedCoords) {
    var snappedPolyline = new google.maps.Polyline({
      path: snappedCoords,
      strokeColor: '#005db5',
      strokeWeight: 6,
      icons: [{
        icon: lineSymbol,
        offset: '100%'
      }]
    });

    snappedPolyline.setMap(map);
    animateCircle(snappedPolyline);

    polylines.push(snappedPolyline);

    for (var i = 0; i < snappedCoords.length; i++) {
      var marker = addMarker(snappedCoords[i]);
      var infoWindow = addDetailedInfoWindow(marker,
          snappedCoords[i],
          placeIds[i]);
      infoWindows.push(infoWindow);
    }
  }

  // Draw the original input.
  // Call functions to add markers and infowindows for each point.
  function drawOriginals(originalCoords) {
    for (var i = 0; i < originalCoords.length; i++) {
      var marker = addMarker(originalCoords[i]);
      var infoWindow = addBasicInfoWindow(marker, originalCoords[i], i);
      infoWindows.push(infoWindow);
    }
  }

  // Infowindow used for unsnappable coordinates
  function addBasicInfoWindow(marker, coords, index) {
    var infowindow = new google.maps.InfoWindow();
    var content = '<div style="width:99%"><p>' +
        '<strong>Lat/Lng:</strong><br>' +
        '(' + coords.lat + ',' + coords.lng + ')<br>' +
        (index != undefined ? '<strong>Index: </strong>' + index : '') +
        '</p></div>';

    infowindow.setContent(content);

    google.maps.event.addListener(marker, 'click', function() {
      openInfoWindow(infowindow, marker);
    });

    return infowindow;
  }

  // Infowindow used for snapped points
  // Makes request to Places Details API to get data about each
  // Place ID.
  // Requests speed limit of each location using Roads SpeedLimit API
  function addDetailedInfoWindow(marker, coords, placeId) {
    var infowindow = new google.maps.InfoWindow();
    var placesRequestUrl = PLACES_URL + placeId;
    var detailsUrl = '<a target="_blank" href="' +
        placesRequestUrl + '">' +
        placeId + '</a></li>';

    // On click we make a request to the Places API
    // This is to avoid OVER_QUERY_LIMIT if we just requested everything
    // at the same time
    google.maps.event.addListener(marker, 'click', function() {
      content = '<div style="width:99%"><p>';

      function finishInfoWindow(placeDetails) {
        content += '<strong>Place Details: </strong>' + placeDetails + '<br>' +
            '<strong>' +
            (coords.interpolated ? 'Coords' : 'Snapped coords') +
            ': </strong>' +
            '(' + coords.lat.toFixed(5) + ',' +
            coords.lng.toFixed(5) + ')<br>';

        if (!(coords.interpolated)) {
          var original = originals[coords.originalIndex];
          content += '<strong>Original coords: </strong>' +
              '(' + original.lat + ',' + original.lng + ')<br>' +
              '<strong>Original Index: </strong>' +
              coords.originalIndex;
        }
        content += '</p></div>';
        infowindow.setContent(content);
        openInfoWindow(infowindow, marker);
      };

      getPlaceDetails(placeId, function(place) {
        if (place.name) {
          content += '<strong>' + place.name + '</strong><br>';
        }
        getSpeedLimit(placeId, function(data) {
          if (data.speedLimits) {
            content += '<strong>Speed Limit: </strong>' +
                data.speedLimits[0].speedLimit + ' km/h <br>';
          }
          finishInfoWindow(detailsUrl);
        });
      }, function() { finishInfoWindow("<em>None available</em>"); });
    });
    return infowindow;
  }

  // Avoid infoWindows staying open if the pano changes
  listenForPanoChange();

  // If the user came to the page with a particular path or URL,
  // immediately plot it.
  if (location.hash.length > 1) {
    coords = parseCoordsFromQuery(location.hash.slice(1));
    $('#coords').val(coords);
    $('#plot').click();
  }
} // End init function

// Call the initialize function once everything has loaded
google.maps.event.addDomListener(window, 'load', initialize);

// Load the control panel in a floating div if it is not loaded in an iframe
// after the textarea has been rendered
$("#coords").ready(function() {
    if (!window.frameElement) {
       $('#panel').addClass("floating panel");
       $('#button-div').addClass("button-div");
       $('#coords').removeClass("coords-large").addClass("coords-small");
       $('#toggle').show();
       $('#map').height('100%');
    }
});
    /**
*  latlng literal with extra properties to use with the RoadsAPI
*  @typedef {Object} ExtendedLatLng
*   lat:string|float
*   lng:string|float
*   interpolated:boolean
*   unsnapped:boolean
*/

/**
 * Add a line to the map for highlighting the connection between two
 * markers while the mouse is over it.
 * @param {ExtendedLatLng} from - The origin of the line
 * @param {ExtendedLatLng} to - The destination of the line
 * @return {!Object} line - the polyline object created
 */
function addOverline(from, to) {
  return addLine("overline", from, to, '#ff77ff', 4, 1.0, 2.0, false);
}

/**
 * Add a line to the map for highlighting the connection between two
 * markers while the mouse is NOT over it.
 * @param {ExtendedLatLng} from - The origin of the line
 * @param {ExtendedLatLng} to - The destination of the line
 * @return {!Object} line - the polyline object created
 */
function addOutline(from, to) {
  return addLine("outline", from, to, '#bb33bb', 2, 0.5, 1.35, true);
}

/**
 * Add a line to the map for highlighting the connection between two
 * markers.
 * @param {string}         attrib  - The attribute to use for managing the line
 * @param {ExtendedLatLng} from    - The origin of the line
 * @param {ExtendedLatLng} to      - The destination of the line
 * @param {string}         color   - The color of the line
 * @param {number}         weight  - The weight of the line
 * @param {number}         opacity - The opacity of the line (0..1)
 * @param {number}         scale   - The scale of the arrow-head (pt)
 * @param {boolean}        visible - The visibility of the line
 * @return {!Object}       line    - the polyline object created
 */
function addLine(attrib, from, to, color, weight, opacity, scale, visible) {
  from[attrib] = new google.maps.Polyline({
    path:         [from, to],
    strokeColor:  color,
    strokeWeight:  weight,
    strokeOpacity: opacity,
    icons:[{
      offset: "0%",
      icon: {
        scale: scale/*pt*/,
        path:  google.maps.SymbolPath.BACKWARD_CLOSED_ARROW
      }
    }]
  });
  from[attrib].setVisible(visible);
  from[attrib].setMap(map);
  to[attrib] = from[attrib];
  polylines.push(from[attrib]);
  return from[attrib];
}

/**
 * Add a pair of lines to the map for highlighting the connection between two
 * markers; one visible while the mouse is over the marker (the "overline"),
 * the other while it is not (the "outline").
 * @param {ExtendedLatLng} from - The origin of the line (the original input)
 * @param {ExtendedLatLng} to - The destination of the line (the snapped point)
 * @return {!Object} line - the polyline object created
 */
function addCorrespondence(coords, marker) {
  if (!coords.overline) { addOverline(coords, coords.related); }
  if (!coords.outline)  { addOutline(coords, coords.related); }

  marker.addListener('mouseover', function(mevt) {
    coords.outline.setVisible(false);
    coords.overline.setVisible(true);
    coords.related.marker.setOpacity(1.0);
  });
  marker.addListener('mouseout', function(mevt) {
    coords.overline.setVisible(false);
    coords.outline.setVisible(true);
    coords.related.marker.setOpacity(0.5);
  });
}

/**
 * Add a marker to the map and check for special 'interpolated'
 * and 'unsnapped' properties to control which colour marker is used
 * @param {ExtendedLatLng} coords - Coords of where to add the marker
 * @return {!Object} marker - the marker object created
 */
function addMarker(coords) {
  var marker = new google.maps.Marker({
    position: coords,
    title: coords.lat + ',' + coords.lng,
    map: map,
    opacity: 0.5,
    icon: RED_MARKER
  });

  // Coord should NEVER be interpolated AND unsnapped
  if (coords.interpolated) {
    marker.setIcon(BLUE_MARKER);
  } else if (!coords.related) {
    marker.setIcon(YELLOW_MARKER);
  } else if (coords.originalIndex != undefined) {
    marker.setIcon(RED_MARKER);
    addCorrespondence(coords, marker);
  } else {
    marker.setIcon({url: GREEN_MARKER,
                    scaledSize: {width: 20, height: 20}});
    addCorrespondence(coords, marker);
  }

  // Make markers change opacity when the mouse scrubs across them
  marker.addListener('mouseover', function(mevt) {
    marker.setOpacity(1.0);
  });
  marker.addListener('mouseout', function(mevt) {
    marker.setOpacity(0.5);
  });

  coords.marker = marker;  // Save a reference for easy access later
  markers.push(marker);

  return marker;
}

/**
 * Animate an icon along a polyline
 * @param {Object} polyline The line to animate the icon along
 */
function animateCircle(polyline) {
  var count = 0;
  // fallback icon if the poly has no icon to animate
  var defaultIcon = [
    {
      icon: lineSymbol,
      offset: '100%'
    }
  ];
  window.setInterval(function() {
    count = (count + 1) % 200;
    var icons = polyline.get('icons') || defaultIcon;
    icons[0].offset = (count / 2) + '%';
    polyline.set('icons', icons);
  }, 20);
}

/**
 * Fit the map bounds to the current set of markers
 * @param {Array<Object>} markers Array of all map markers
 */
function fitBounds(markers) {
  var bounds = new google.maps.LatLngBounds;
  for (var i = 0; i < markers.length; i++) {
    bounds.extend(markers[i].getPosition());
  }
  map.fitBounds(bounds);
}

/**
 * Uses Places library to get Place Details for a Place ID
 * @param {string}   placeId         The Place ID to look up
 * @param {Function} foundCallback   Called if the place is found
 * @param {Function} missingCallback Called if nothing is found
 * @param {Function} errorCallback   Called if request fails
 */
function getPlaceDetails(placeId,
                         foundCallback, missingCallback, errorCallback) {
  var request = {
    placeId: placeId
  };

  placesService.getDetails(request, function(place, status) {
    if (status == google.maps.places.PlacesServiceStatus.OK) {
      foundCallback(place);
    } else if (status == google.maps.places.PlacesServiceStatus.NOT_FOUND) {
      missingCallback();
    } else if (errorCallback) {
      errorCallback();
    }
  });
}

/**
 * AJAX request to the Roads Speed Limit API.
 * Request the speed limit for the Place ID
 * @param {string}   placeId         Place ID to request the speed limit for
 * @param {Function} successCallback Called if request is successful
 * @param {Function} errorCallback   Called if request fails
 */
function getSpeedLimit(placeId, successCallback, errorCallback) {
  $.ajax({
    type: 'GET',
    url: SPEED_LIMIT_URL,
    data: {
      placeId: placeId,
      key: API_KEY
    },
    success: successCallback,
    error: errorCallback
  });
}

/**
 * Open an infowindow on either the map or the active streetview pano
 * @param {Object} infowindow Infowindow to be opened
 * @param {Object} marker Marker the infowindow is anchored to
 */
function openInfoWindow(infowindow, marker) {
  // If streetView is visible display the infoWindow over the pano
  // and anchor to the marker
  if (map.getStreetView().getVisible()) {
    infowindow.open(map.getStreetView(), marker);
  }
  // Otherwise open it on the map and anchor to the marker
  else {
    infowindow.open(map, marker);
  }
}

/**
 * Add event listener to for when the active pano changes
 */
function listenForPanoChange() {
  var pano = map.getStreetView();

  // Close all open markers when the pano changes
  google.maps.event.addListener(pano, 'position_changed', function() {
    closeAllInfoWindows(infoWindows);
  });
}

/**
 * Close all open infoWindows
 * @param {Array<Object>} infoWindows - all infowindow objects
 */
function closeAllInfoWindows(infoWindows) {
  for (var i = 0; i < infoWindows.length; i++) {
    infoWindows[i].close();
  }
}
    </script>
  </head>
  <body>
    <div class="floating" id="toggle">&mdash;</div>
    <div id="panel">
      <div class="block">
        <strong>Sample Queries</strong>
        <div id="button-div">
          <button id="eg1" class="button raised blue">EXAMPLE 1</button>
          <button id="eg2" class="button raised blue">EXAMPLE 2</button>
          <button id="eg3" class="button raised blue">EXAMPLE 3</button>
          <button id="eg4" class="button raised blue">EXAMPLE 4</button>
        </div>
      </div>
      <form id="controls">
        <div class="block">
          <div>
            <strong><span id="requestURL">Request URL</span> or Path (Pipe Separated)</strong><br>
            <textarea id="coords" class="u-full-width coords-large" type="text" placeholder="-35.123,150.332 | 80.654,22.439" id="exampleEmailInput"></textarea>
          </div>
          <div>
            <label>Interpolate: </label>
            <input for="interpolate" id="interpolate" type="checkbox" checked/>
          </div>
        </div>
        <div>
          <div class="block">
            <button id="plot" class="button raised blue">Plot a Course</button>
            <button id="distance" class="button raised blue">Toggle Distances</button>
          </div>
          <div id="legend">
            <img src="https://maps.google.com/mapfiles/ms/icons/green-dot.png" style="height:16px;"> Original
            <img src="https://maps.google.com/mapfiles/ms/icons/red-dot.png"/> Snapped
            <img src="https://maps.google.com/mapfiles/ms/icons/blue-dot.png"/> Interpolated
            <img src="https://maps.google.com/mapfiles/ms/icons/yellow-dot.png"/> Unsnappable
          </div>
          <div>
            <p id="warningMessage"></p>
            <p id="unsnappedPoints"></p>
          </div>
        </div>
      </form>
    </div>
    <div id="map">
    </div>
  </body>
</html>