Inspektor Roads API to interaktywne narzędzie, za pomocą którego możesz wypróbować Roads API. Oto kilka sugestii, które pomogą Ci zacząć korzystać z narzędzia:
- Skopiuj adres URL Roads API do pola tekstowego i kliknij Narysuj kurs, aby wyświetlić wyniki żądania. Nie musisz dołączać klucza interfejsu API.
- Korzystaj z przykładów, aby eksperymentować z interfejsem API.
- Wybierz znacznik dla konkretnego punktu, aby wyświetlić okno z informacjami o tym punkcie.
- Po załadowaniu jednego z przykładów otwórz Street View, aby zobaczyć znaczniki na panoramie Street View.
- Przełącz ustawienie Interpoluj, aby zobaczyć jego wpływ na wyniki.
- Kliknij Przełącz odległość, aby wyświetlić lub ukryć odległość między 2 punktami początkowymi. Trasa nieprzeskakująca jest wyświetlana jako prosta linia zielona od punktu do punktu. Wybierz linię, aby zobaczyć odległość.
Polecane przykłady:
- Przykład 1. Punkty na drodze w Pyrmont w Sydney.
- Przykład 2. Punkty na drodze w Canberrze, Terytorium Stołeczne Australii.
- Przykład 3. Punkty na drodze w Canberrze, w tym punkt, który nie może zostać dopasowany do drogi.
- Przykład 4. Trasa w Elkin w Północnej Karolinie o nieregularnym przebiegu, która dobrze obrazuje wyniki włączania i wyłączania ustawienia interpolacji.
Wyświetl ten przykład w pełnym ekranie.
Sprawdź to
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