Roads API Inspector

Roads API Inspector は、Roads API を試すことができるインタラクティブなツールです。ツールの使用を開始する際のヒントをいくつかご紹介します。

  • Roads API リクエスト URL をテキスト フィールドにコピーし、[コースをプロット] を選択して、リクエストの結果を確認します。API キーを含める必要はありません。
  • 注目の例を使用して API を試す。
  • 特定のポイントのマーカーを選択すると、そのポイントの詳細を含む情報ウィンドウが表示されます。
  • いずれかの例を読み込んだらストリートビューを開き、ストリートビュー パノラマにマーカーが表示されていることを確認します。
  • [補間] 設定を切り替えて、結果への影響をご確認ください。
  • [距離を切り替える] を選択して、2 つの元の点間の距離を表示または非表示にします。スナップされていないルートは、ポイントからポイントへの直線の緑色の線として表示されます。線を選択すると距離が表示されます。

特徴的な例として、以下の例が提供されています。

  • 例 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/j&son?' +
                '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_THR<ESHOLD_HIGH = >300;
var DISTANCE_THRESHOLD_LOW = 200;

/**
 * @type ArrayExtendedLatLng
 */
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 mapOp<tions = {
    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].cl>ose();
    }

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

  /</ Parse the value in the input element
  // to get all coordinate>s
  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 p<art 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 <AJ>AX 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.goo<g>leapis.com/v1/snapT<oRoads',
      data: {
        in>terpolate: <$(><&#>39;#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);
        drawOrigin<als(originals);
        fitBounds(markers);
      },
      error: function() {
        $('#requestURL').html('strongThat query didn\'t work :(/strong' +
            'pTry looking at the a href="' + this.url +
            '"Request URL/a/p');
        clearMap();
      }
    });
  }

  // Toggl<e 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 eac<h original point and crea&&te 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&#<39;;
        weight = ><6>;
      } els<e {
  >      color = &#<39;#22C>C00';
   <  >   weight = 5<;
    >  }
   <   var >polyline = new google.maps.Polyline({
        strokeColor: color,
        strokeO<pa>ci<ty>: 0.4,
      <  stro>keWeight: weight<,
     >   geodesic: true<,
>        visib<le: fa>lse,
  <      m>ap: map
      });
      polyline.setPath([origin, destination]);

      <di><st>Polylines.pus<h(poly>line);
    <  infoW>indows.push(addPoly<Wi>ndow(polyline, dista>nce, i));
    }
  }

  /**
   * Add an info wi<ndow to the polyline displaying the original>
   * points and the distance
  > */
  function addPolyWin<dow(p><ol>yline, distance, index) {
<    var infoWindow = new google.maps.InfoWindow();
    var content = 'div style="width:100%"p' +
        'st>rongOriginal Index: /st<ro>ng' + index + 'br<&#><39; >+
        'strongCoords:/strong (' +
        originals[index].lat + ',' + originals[index].lng + ')' +
        'brtobr' +
        'strongOriginal Index: /strong' + (index+1) + 'br' +
        'strongCoords:/strong (' +
        originals[index+1].lat + ',' + originals[index+1].lng + ')brbr' +
        'strongDistance:  /strong' +  distance + ' mbr';
    if (distance  DISTANCE_THRESHOLD_HIGH) {
      content += 'span style="color:#CC0022;font-style:italic"' +
          '*Large distance (300m) may affect snapping/spanbr' +
          'Please see a href="https://developers.google.com/maps/<9; +
          '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;
  }

  // P<arse 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 originalInde<xes = [];
    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 interpolate<d >= true;

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

      latlng.interpolated = interpolated;
  <    snappedCoordinates.push(latlng);
      placeIds.push(data.snappedPoint>s[i].placeId);

      // Cr<oss>-reference the <original point and this snapped point.
      latlng.related = originals[latlng.originalIndex];
    >  originals[latlng.originalIndex].related = latlng;
    }

    var unsnapp<ed>Points = 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: ' +
         '/strongbr' + <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 d<rawSnappedPolyline(sn><a>ppedCoords) {<
    v>ar snapp<edPolyl><in>e = new google.maps.Polyline({
      path: snappedCo<or>ds,
      strokeColor: '#005db5<',>
      <strokeW>eight: 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(origi<nalCoords) {
    for (var i = 0; i  originalCoords.length>; i++) {
      var mark<er>< = >addMarker(originalCoords[i]);
      var infoWindow = addBasicInfoWindow(marker, originalCoords[i], i);
      infoWindows.push(infoWindow);
    }
  }

  // Infowindow used for unsnappable coordinates
  function addBasicInfoWindow(marke<r, coords, index) {
 >< >  var infowindow = new google.maps.InfoWindow();
    var content = '<div st>yle="width<:99%&qu>ot;p' +
        <&#>39;strongLat/Lng:</stron>gbr' +
        '(' + coords.lat + ',' + coords.lng + ')br'<; +
   >     (index != undefined ? 'strongIndex: /strong' + index : '') +
        <&#>39;/p/div';

    infowindow.setContent(content);

    google.maps.event.addListener(marker, 'click', function(<) {
  >    openInfoWindo<w(infow>indow, marker);
    });

    return infowindow;
  }

  // Info<wi>ndow used for snapp<ed poi>nts
  // Makes r<equest >to Places Details API to get data about each
  // Place ID.
  // Reque<st><s sp>eed limit of each location using Roads SpeedLimit API
  function addDetailedInfoWindow(marker, coords, placeId) {
    var infowindow = new google.maps.InfoWindow();
    var placesRequestUrl = PL<ACES_U>RL + placeId;
    <var det><ai>lsUrl = 'a target="_blank" href="' +
        placesRequestUrl + '"' +
        place<Id + &>#39;/a/li'<;;

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

      function finishInfoWindow(placeDetails) {
        content += 'strongPlace Details: /strong' + placeDetails + 'br' +
            >9;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 += 'strongOriginal coords: /strong' +
              '(' + original.lat + ',' + original.lng + ')br' +
              'strongOriginal Index: /strong' +
              coords.originalIndex;
        }
        content += '/p/div';
        infowindow.setContent(content);
        openInfoWindow(infowindow, marker);
      };

      getPlaceDetails(placeId, function(place) {
        if (place.name) {
          content += 'strong' + place.name + '/strongbr';
        }
        getSpeedLimit(placeId, function(data) {
          if (data.speedLimits) {
            content += 'strongSpeed Limit: /strong' +
                data.speedLimits[0].speedLimit + ' km/h br';
          }
          finishInfoWindow(detailsUrl);
        });
      }, function() { finishInfoWindow("emNone 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< {Exte>ndedLatLng} 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 {ArrayObject} 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.g<etDeta>ils(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 {ArrayObject} 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,geometrykey=AIzaSyAAUHO6lMM>nE2VZ<MRYmAfV>bCYCg<sEEqNyM"
    /script
    script src="https://www.gstatic.><com/ext>ernal<_hoste>d/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 {nu>mber}
 */
var DISTANCE_THRESHOLD_HIGH = 300;
var DISTANCE_THRESHOLD_LOW = 200;

/**
 * @type ArrayExtendedLatLng
 */
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(eg&3);
    $('#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.196<532},
    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 rese<t 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  infoWindow>s.length; i++) {
      infoWindows[i].close();
    }

    // Empty everything
    polylines = [];
    markers = [];
    distPoly&lines = [];
    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(functio<n(e) {
    clearMap();
    bendAndSnap();
    drawDi>stance();
 <  > e.preventDefault();
  });

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

  // 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 (in>foWindows[i].dist) {
        infoWindows[i].close();
      }
    }
    e.preventDefault();
  });

  /<**
   * Compute the dista&&nce between each original p>oint 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_THRESHO<LD_LOW) {
        colo><r> = '#FF66<00'>;;
        weigh<t = 6;
>      } else <{
>        color< = >9;#22CC<00'>;
        weight = 5;
      }
      var polyline = new google.maps.Polyline({
   <  >  < s>trokeColor: c<olor,
>        strokeOp<acity: >0.4,
        stro<ke>Weight: weigh<t,
   >     ge<odesic:> true,
        visible: false,
        map: map
      });
      polyline<.s><et>Path([origin,< desti>nation]);

<      d>istPolylines.push(p<ol>yline);
      infoWi>ndows.push(addPolyWindow(polyline, distance, i<));
    }
  }

  /**
   * Add an info window> to the polyline displaying the >original
   * points and <the d><is>tance
   */
  function add<PolyWindow(polyline, distance, index) {
    var infoWindow = new google.maps.InfoWindow();
    var content = 'div style=">width:100%"p' <+
>        'strongOrigin<al>< Ind>ex: /strong' + index + 'br' +
        'strongCoords:/strong (' +
        originals[index].lat + ',' + originals[index].lng + ')' +
        'brtobr' +
        'strongOriginal Index: /strong' + (index+1) + 'br' +
        'strongCoords:/strong (' +
        originals[index+1].lat + ',' + originals[index+1].lng + ')brbr' +
        'strongDistance:  /strong' +  distance + ' mbr';
    if (distance  DISTANCE_THRESHOLD_HIGH) {
      content += 'span style="color:#CC0022;font-style:italic"' +
          '*Large distance (300m) may affect snapping/spanbr' +
          'Please see a href=&quo<t;https://developers.google.com/maps/' +
          'documen<tation/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 processSnapToR<oadResponse(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.<lo>ngitude
      };
      var interpolated = true;

      if (data.snappe<dPoint>s[i].originalIndex != undefined) {
        interpolated = f<alse;
 ><  >     originalIndexes.push(data.snappedPoints[i].originalIndex);
        latlng.originalIndex = data.snappedPoints[i].originalIndex;
      }

    <  latlng.interpolated = interpolated;
      snappedCoordinates.push(latlng>);
      placeIds.push(data<.sn>appedPoints[i].<placeId);

      // Cross-reference the original point and this snapped point.
      latlng.related> = originals[latlng.originalIndex];
      originals[latlng.originalIndex].<re>lated = 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:< ' +
         '/strongbr' + 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 pol><y>line.
  funct<ion dr>awSnappe<dPolyli><ne>(snappedCoords) {
    var snappedPolyline = new goog<le>.maps.Polyline({
      path: snappe<dCoord>s,
    <  strok>eColor: '#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 ea<ch point.
  function drawOriginals(originalCoords) {
    >for (var i = 0; i  orig<in><alC>oords.length; i++) {
      var marker = addMarker(originalCoords[i]);
      var infoWindow = addBasicInfoWindow(marker, originalCoords[i], i);
      infoWindows.push(infoWindow);
    }
  }

  // Infowindow used for unsnappable coordin<ates
  function addBa><s>icInfoWindow(marker, coords, index) {
    var infowindow = new google.ma<ps.Inf>oWindow();
    <var con>tent = 'div styl<e=>"width:99%&q<uot;p&>#39; +
        'strongLat/Lng:/strongbr' +
        '(' + coords.lat +< ',>' + coords.lng + ')br' +
        (index != undefined ? 'strongIndex: /stro<ng>' + index : '') +
        '/p/div';

    infowindow.setContent(content);

    google.maps.event.addLis<tener(>marker, 'clic<k',> function() {
      openInfoWindow(infowindow, marker);
    })<;<>/span>

    return infowin<dow;
 > }

  // Infowin<dow use>d for snapped points
  // Makes request to Places Details API to get d<at><a ab>out each
  // Place ID.
  // Requests speed limit of each location using Roads SpeedLimit API
  function addDetailedInfoWindow(marker, coords, placeId) {
    var infowindow = new google.maps.Inf<oWindo>w();
    var place<sReques><tU>rl = PLACES_URL + placeId;
    var detailsUrl = 'a target="_blank" href="' +
        placesReque<stUrl >+ '"<' +>
        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 += 'strongPlace Details: /strong' + place>Details + '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 += 'strongOriginal coords: /strong' +
              '(' + original.lat + ',' + original.lng + ')br' +
              'strongOriginal Index: /strong' +
              coords.originalIndex;
        }
        content += '/p/div';
        infowindow.setContent(content);
        openInfoWindow(infowindow, marker);
      };

      getPlaceDetails(placeId, function(place) {
        if (place.name) {
          content += 'strong' + place.name + '/strongbr';
        }
        getSpeedLimit(placeId, function(data) {
          if (data.speedLimits) {
            content += 'strongSpeed Limit: /strong' +
                data.speedLimits[0].speedLimit + ' km/h br';
          }
          finishInfoWindow(detailsUrl);
        });
      }, function() { finishInfoWindow("emNone 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 contr<ol whi>ch colour marker is used
 * @param {ExtendedLatLng} coords - Coords of where to add the marker
 * @return {!Object} marker - the marke<r 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 {ArrayObject} 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 = {
    <placeI>d: placeId
  };

  placesService.getDetails(request, function(place, status) {
    if (status == google.map<s.places.PlacesServiceStatus.OK) {
      foundCallback(place);
   < } else> if< (sta>tus< == >googl<e.maps.places.PlacesServiceStatu>&s.NOT_<FOUN>D) {
<      missingC>allback<();
    } else if> (errorCa<llback>) {
      erro<rCallba>ck();
   < }
  });
}

/**
 * >AJAX reques<t to the Roads Speed Limit API.
 * Request> the spee<d limit> for the Pl<ace ID
 * @param {string}   placeId       >  Place I<D to re>quest the s<peed limit for
 * @param {Function} succes>sCallback< Called> if request< is successful
 * @param {Function} errorC>allback  < Called> if reque<st f>ails
 *</
fu>nction <getSpeedLimit(plac>eId, succ<essCallback, erro>rCallback) <{
 > $.ajax({
   < type:>< 'GET',
    >url: SPEED_<LIMIT>_URL,
    data: {
      p<laceId:>< p>laceId,
     < key: API_KEY
    },
    success: successCallback,
    error: errorCallback
  });
}

/**
 * Open an infowindow on either the map or the>< active s>treetview p<ano<>/span>
 * @param {<Obj>ect} infowind<ow In>fowindow to b<e open>ed
 * @param <{Object} marker Marker the infowindow is anchored to
 */
function> openInfoWi<ndow>(infowind<ow, >marker) {<
  >// If stree<tView is visible >display the i<nfoWindow over the pano
  // and anchor to >the marker
  <if (map>.getStreetVie<w().getVisible()) {
    infowindow.open(map.get>StreetView(), ma<rker);
>  }
  // Ot<herw>ise open it< on the map and> anchor to th<e marker
  else {
    infowindow.open(map, marker);
  }
}

/**
 * Add event listener t>o for when the active <pano changes
 */
function listenForPanoChange() {
  var pano = m>ap.getStreetView();

<  // Close all open markers when the pano changes
  google.maps.e>vent.addListener(pano, <9;position_changed', function() {
    closeAllInfoWindows(infoW>indows);
  });
}

/**
 <* Cl>ose all ope<n i>nfoWindows
 *< @param {ArrayObject}>< i>nfoWindows - <all infowindow objects><
 >*/
function< clo>seAllInfo<Wind>ows(inf<oWind>ows) <{
  >for (<var i = 0; i>  inf<oWin>dow<s.len>g<th; i>++) {
    infoWindows[i].close();
  }
}
    /script
  /head
  body
    div class="floating" id="toggle"mdash;/div
    div id="panel"
      div class="block"
        strongSample 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
            strongspan id="requestURL"Request URL/span or Path (Pipe Separated)/strongbr
            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
            labelInterpolate: /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