Roads API Inspector 是一項互動式工具 Roads API。建議您參考下列建議,以便快速上手: 工具:
- 將 Roads API 要求網址複製到文字欄位,然後選取 繪製課程,瞭解要求結果。您自己 不需要包含 API 金鑰
- 運用精選範例來測試 API。
- 選取特定點的標記,即可查看包含詳細資料的資訊視窗 關於這一點
- 載入其中一個範例後,開啟街景服務以查看標記 。
- 切換「Interpolate」Interpolate設定,查看設定對 也就是預測結果
- 選取「切換距離」即可查看或隱藏距離 之間的間隔未遮蔽的路線顯示為直線 畫面上出了什麼綠色線條選取線條即可查看距離。
以下列舉幾個範例:
- 範例 1:位於雪梨的皮爾蒙特道路上。
- 範例 2:澳洲坎培拉州坎培拉道路的道路,
- 範例 3:沿著坎培拉道路的形狀, 。
- 範例 4:北卡羅來納州埃爾金的路線,有很大的路徑恰當 示範切換內插設定的結果。
查看全螢幕範例。
親自試試
JavaScript
// Replace with your own API key var API_KEY = 'YOUR_API_KEY'; // Icons for markers var RED_MARKER = 'https://maps.google.com/mapfiles/ms/icons/red-dot.png'; var GREEN_MARKER = 'https://maps.google.com/mapfiles/ms/icons/green-dot.png'; var BLUE_MARKER = 'https://maps.google.com/mapfiles/ms/icons/blue-dot.png'; var YELLOW_MARKER = 'https://maps.google.com/mapfiles/ms/icons/yellow-dot.png'; // URL for places requests var PLACES_URL = 'https://maps.googleapis.com/maps/api/place/details/json?' + 'key=' + API_KEY + '&placeid='; // URL for Speed limits var SPEED_LIMIT_URL = 'https://roads.googleapis.com/v1/speedLimits'; var coords; /** * Current Roads API threshold (subject to change without notice) * @const {number} */ var DISTANCE_THRESHOLD_HIGH = 300; var DISTANCE_THRESHOLD_LOW = 200; /** * @type Array<ExtendedLatLng> */ var originals = []; // the original input points, a list of ExtendedLatLng var interpolate = true; var map; var placesService; var originalCoordsLength; // Settingup Arrays var infoWindows = []; var markers = []; var placeIds = []; var polylines = []; var snappedCoordinates = []; var distPolylines = []; // Symbol that gets animated along the polyline var lineSymbol = { path: google.maps.SymbolPath.CIRCLE, scale: 8, strokeColor: '#005db5', strokeWidth: '#005db5' }; // Example 1 - Frolick around Sydney var eg1 = '-33.870315,151.196532|-33.869979,151.197355|' + '-33.870044,151.197712|-33.870358,151.198206|' + '-33.870595,151.198376|-33.870640,151.198398|' + '-33.870620,151.198449|-33.870951,151.198525|' + '-33.871040,151.198528|-33.872031,151.198413'; // Example 2 - Lap around Canberra var eg2 = '-35.274346,149.130168|-35.278012,149.129583|' + '-35.280329,149.129073|-35.280999,149.129293|' + '-35.281441,149.129846|-35.281945,149.130034|' + '-35.282825,149.129567|-35.283022,149.128811|' + '-35.284734,149.128366'; // Example 3 - Path with unsnappable point var eg3 = '-35.274346,149.094000|-35.278012,149.129583|' + '-35.280329,149.129073|-35.280999,149.129293|' + '-35.281441,149.129846'; // Example 4 - Drive erratically in Elkin var eg4 = '36.28881,-80.8525|36.287038,-80.85313|36.286161,-80.85369|' + '36.28654,-80.85418|36.2846,-80.84766|36.28355,-80.84669'; // Initialize function initialize() { $('#eg1').click(function(e) { $('#coords').val(eg1); $('#plot').trigger('click'); }); $('#eg2').click(function(e) { $('#coords').val(eg2); $('#plot').trigger('click'); }); $('#eg3').click(function(e) { $('#coords').val(eg3); $('#plot').trigger('click'); }); $('#eg4').click(function(e) { $('#coords').val(eg4); $('#plot').trigger('click'); }); $('#toggle').click(function(e) { if ($('#panel').css("display") != 'none') { $('#toggle').html("+"); $('#panel').hide(); } else { $('#toggle').html("—"); $('#panel').show(); } }); // Centre the map on Sydney var mapOptions = { center: {'lat': -33.870315, 'lng': 151.196532}, zoom: 14 }; // Map object map = new google.maps.Map(document.getElementById('map'), mapOptions); // Places object placesService = new google.maps.places.PlacesService(map); // Reset the map to a clean state and reset all variables // used for displaying each request function clearMap() { // Clear the polyline for (var i = 0; i < polylines.length; i++) { polylines[i].setMap(null); } // Clear all markers for (var i = 0; i < markers.length; i++) { markers[i].setMap(null); } // Clear all the distance polylines for (var i = 0; i < distPolylines.length; i++) { distPolylines[i].setMap(null); } // Clear all info windows for (var i = 0; i < infoWindows.length; i++) { infoWindows[i].close(); } // Empty everything polylines = []; markers = []; distPolylines = []; snappedCoordinates = []; placeIds = []; infoWindows = []; $('#unsnappedPoints').empty(); $('#warningMessage').empty(); } // Parse the value in the input element // to get all coordinates function parseCoordsFromQuery(input) { var coords; input = decodeURIComponent(input); if (input.split('path=').length > 1) { input = decodeURIComponent(input); // Split on the ampersand to get all params var parts = input.split('&'); // Check each part to see if it starts with 'path=' // grabbing out the coordinates if it does for (var i = 0; i < parts.length; i++) { if (parts[i].split('path=').length > 1) { coords = parts[i].split('path=')[1]; break; } } } else { coords = decodeURIComponent(input); } // Parse the "Lat,Lng|..." coordinates into an array of ExtendedLatLng originals = []; var points = coords.split('|'); for (var i = 0; i < points.length; i++) { var point = points[i].split(','); originals.push({lat: Number(point[0]), lng: Number(point[1]), index:i}); } return coords; } // Clear the map of any old data and plot the request $('#plot').click(function(e) { clearMap(); bendAndSnap(); drawDistance(); e.preventDefault(); }); // Make AJAX request to the snapToRoadsAPI // with coordinates parsed from text input element. function bendAndSnap() { coords = parseCoordsFromQuery($('#coords').val()); location.hash = coords; $.ajax({ type: 'GET', url: 'https://roads.googleapis.com/v1/snapToRoads', data: { interpolate: $('#interpolate').is(':checked'), key: API_KEY, path: coords }, success: function(data) { $('#requestURL').html('<a target="blank" href="' + this.url + '">Request URL</a>'); processSnapToRoadResponse(data); drawSnappedPolyline(snappedCoordinates); drawOriginals(originals); fitBounds(markers); }, error: function() { $('#requestURL').html('<strong>That query didn\'t work :(</strong>' + '<p>Try looking at the <a href="' + this.url + '">Request URL</a></p>'); clearMap(); } }); } // Toggle the distance polylines of the original points to show on the maps $('#distance').click(function(e) { for (var i = 0; i < distPolylines.length; i++) { distPolylines[i].setVisible(!distPolylines[i].getVisible()); } // Clear all infoWindows associated with distance polygons on toggle for (var i = 0; i < infoWindows.length; i++) { if (infoWindows[i].dist) { infoWindows[i].close(); } } e.preventDefault(); }); /** * Compute the distance between each original point and create a polyline * for each pair. Polylines are initially hidden on creation */ function drawDistance() { for (var i = 0; i < originals.length - 1; i++) { var origin = new google.maps.LatLng(originals[i]); var destination = new google.maps.LatLng(originals[i+1]); var distance = google.maps.geometry.spherical.computeDistanceBetween(origin, destination); // Round the distance value to two decimal places distance = Math.round(distance * 100) / 100; var color; var weight; if (distance > DISTANCE_THRESHOLD_HIGH) { color = '#CC0022'; weight = 7; } else if (distance < DISTANCE_THRESHOLD_HIGH && distance > DISTANCE_THRESHOLD_LOW) { color = '#FF6600'; weight = 6; } else { color = '#22CC00'; weight = 5; } var polyline = new google.maps.Polyline({ strokeColor: color, strokeOpacity: 0.4, strokeWeight: weight, geodesic: true, visible: false, map: map }); polyline.setPath([origin, destination]); distPolylines.push(polyline); infoWindows.push(addPolyWindow(polyline, distance, i)); } } /** * Add an info window to the polyline displaying the original * points and the distance */ function addPolyWindow(polyline, distance, index) { var infoWindow = new google.maps.InfoWindow(); var content = '<div style="width:100%"><p>' + '<strong>Original Index: </strong>' + index + '<br>' + '<strong>Coords:</strong> (' + originals[index].lat + ',' + originals[index].lng + ')' + '<br>to<br>' + '<strong>Original Index: </strong>' + (index+1) + '<br>' + '<strong>Coords:</strong> (' + originals[index+1].lat + ',' + originals[index+1].lng + ')<br><br>' + '<strong>Distance: </strong>' + distance + ' m<br>'; if (distance > DISTANCE_THRESHOLD_HIGH) { content += '<span style="color:#CC0022;font-style:italic">' + '*Large distance (>300m) may affect snapping</span><br>' + 'Please see <a href="https://developers.google.com/maps/' + 'documentation/roads/snap#parameter_usage" ' + 'target="_blank">Roads API documentation</a>'; } content += '</p></div>'; infoWindow.setContent(content); infoWindow.dist = true; polyline.addListener('click', function(e) { infoWindow.setPosition(e.latLng); infoWindow.open(map); }); polyline.addListener('mouseover', function(e) { polyline.setOptions({strokeOpacity: 1.0}); }); polyline.addListener('mouseout', function(e) { polyline.setOptions({strokeOpacity: 0.4}); }); return infoWindow; } // Parse the value in the input element // to get all coordinates function getMissingPoints(originalIndexes, originalCoordsLength) { var unsnappedPoints = []; var coordsArray = coords.split('|'); var hasMissingCoords = false; for (var i = 0; i < originalCoordsLength; i++) { if (originalIndexes.indexOf(i) < 0) { hasMissingCoords = true; var latlng = { 'lat': parseFloat(coordsArray[i].split(',')[0]), 'lng': parseFloat(coordsArray[i].split(',')[1]) }; unsnappedPoints.push(latlng); latlng.unsnapped = true; } } return unsnappedPoints; } // Parse response from snapToRoads API request // Store all coordinates in response // Calls functions to add markers to map for unsnapped coordinates function processSnapToRoadResponse(data) { var originalIndexes = []; var unsnappedMessage = ''; for (var i = 0; i < data.snappedPoints.length; i++) { var latlng = { 'lat': data.snappedPoints[i].location.latitude, 'lng': data.snappedPoints[i].location.longitude }; var interpolated = true; if (data.snappedPoints[i].originalIndex != undefined) { interpolated = false; originalIndexes.push(data.snappedPoints[i].originalIndex); latlng.originalIndex = data.snappedPoints[i].originalIndex; } latlng.interpolated = interpolated; snappedCoordinates.push(latlng); placeIds.push(data.snappedPoints[i].placeId); // Cross-reference the original point and this snapped point. latlng.related = originals[latlng.originalIndex]; originals[latlng.originalIndex].related = latlng; } var unsnappedPoints = getMissingPoints( originalIndexes, coords.split('|').length ); for (var i = 0; i < unsnappedPoints.length; i++) { var marker = addMarker(unsnappedPoints[i]); var infowindow = addBasicInfoWindow(marker, unsnappedPoints[i], i); infoWindows.push(infowindow); unsnappedMessage += unsnappedPoints[i].lat + ',' + unsnappedPoints[i].lng + '<br>'; } if (unsnappedPoints.length) { unsnappedMessage = '<strong>' + 'These points weren\'t snapped: ' + '</strong><br>' + unsnappedMessage; $('#unsnappedPoints').html(unsnappedMessage); } if (data.warningMessage) { $('#warningMessage').html('<span style="color:#CC0022;' + 'font-style:italic;font-size:12px">' + data.warningMessage + '<br/>' + '<a target="_blank" href="https://developers.google.com/maps/' + 'documentation/roads/snap">https://developers.google.com/maps/' + 'documentation/roads/snap</a>'); $('#distance').trigger('click'); } } // Draw the polyline for the snapToRoads API response // Call functions to add markers and infowindows for each snapped // point along the polyline. function drawSnappedPolyline(snappedCoords) { var snappedPolyline = new google.maps.Polyline({ path: snappedCoords, strokeColor: '#005db5', strokeWeight: 6, icons: [{ icon: lineSymbol, offset: '100%' }] }); snappedPolyline.setMap(map); animateCircle(snappedPolyline); polylines.push(snappedPolyline); for (var i = 0; i < snappedCoords.length; i++) { var marker = addMarker(snappedCoords[i]); var infoWindow = addDetailedInfoWindow(marker, snappedCoords[i], placeIds[i]); infoWindows.push(infoWindow); } } // Draw the original input. // Call functions to add markers and infowindows for each point. function drawOriginals(originalCoords) { for (var i = 0; i < originalCoords.length; i++) { var marker = addMarker(originalCoords[i]); var infoWindow = addBasicInfoWindow(marker, originalCoords[i], i); infoWindows.push(infoWindow); } } // Infowindow used for unsnappable coordinates function addBasicInfoWindow(marker, coords, index) { var infowindow = new google.maps.InfoWindow(); var content = '<div style="width:99%"><p>' + '<strong>Lat/Lng:</strong><br>' + '(' + coords.lat + ',' + coords.lng + ')<br>' + (index != undefined ? '<strong>Index: </strong>' + index : '') + '</p></div>'; infowindow.setContent(content); google.maps.event.addListener(marker, 'click', function() { openInfoWindow(infowindow, marker); }); return infowindow; } // Infowindow used for snapped points // Makes request to Places Details API to get data about each // Place ID. // Requests speed limit of each location using Roads SpeedLimit API function addDetailedInfoWindow(marker, coords, placeId) { var infowindow = new google.maps.InfoWindow(); var placesRequestUrl = PLACES_URL + placeId; var detailsUrl = '<a target="_blank" href="' + placesRequestUrl + '">' + placeId + '</a></li>'; // On click we make a request to the Places API // This is to avoid OVER_QUERY_LIMIT if we just requested everything // at the same time google.maps.event.addListener(marker, 'click', function() { content = '<div style="width:99%"><p>'; function finishInfoWindow(placeDetails) { content += '<strong>Place Details: </strong>' + placeDetails + '<br>' + '<strong>' + (coords.interpolated ? 'Coords' : 'Snapped coords') + ': </strong>' + '(' + coords.lat.toFixed(5) + ',' + coords.lng.toFixed(5) + ')<br>'; if (!(coords.interpolated)) { var original = originals[coords.originalIndex]; content += '<strong>Original coords: </strong>' + '(' + original.lat + ',' + original.lng + ')<br>' + '<strong>Original Index: </strong>' + coords.originalIndex; } content += '</p></div>'; infowindow.setContent(content); openInfoWindow(infowindow, marker); }; getPlaceDetails(placeId, function(place) { if (place.name) { content += '<strong>' + place.name + '</strong><br>'; } getSpeedLimit(placeId, function(data) { if (data.speedLimits) { content += '<strong>Speed Limit: </strong>' + data.speedLimits[0].speedLimit + ' km/h <br>'; } finishInfoWindow(detailsUrl); }); }, function() { finishInfoWindow("<em>None available</em>"); }); }); return infowindow; } // Avoid infoWindows staying open if the pano changes listenForPanoChange(); // If the user came to the page with a particular path or URL, // immediately plot it. if (location.hash.length > 1) { coords = parseCoordsFromQuery(location.hash.slice(1)); $('#coords').val(coords); $('#plot').click(); } } // End init function // Call the initialize function once everything has loaded google.maps.event.addDomListener(window, 'load', initialize); // Load the control panel in a floating div if it is not loaded in an iframe // after the textarea has been rendered $("#coords").ready(function() { if (!window.frameElement) { $('#panel').addClass("floating panel"); $('#button-div').addClass("button-div"); $('#coords').removeClass("coords-large").addClass("coords-small"); $('#toggle').show(); $('#map').height('100%'); } }); /** * latlng literal with extra properties to use with the RoadsAPI * @typedef {Object} ExtendedLatLng * lat:string|float * lng:string|float * interpolated:boolean * unsnapped:boolean */ /** * Add a line to the map for highlighting the connection between two * markers while the mouse is over it. * @param {ExtendedLatLng} from - The origin of the line * @param {ExtendedLatLng} to - The destination of the line * @return {!Object} line - the polyline object created */ function addOverline(from, to) { return addLine("overline", from, to, '#ff77ff', 4, 1.0, 2.0, false); } /** * Add a line to the map for highlighting the connection between two * markers while the mouse is NOT over it. * @param {ExtendedLatLng} from - The origin of the line * @param {ExtendedLatLng} to - The destination of the line * @return {!Object} line - the polyline object created */ function addOutline(from, to) { return addLine("outline", from, to, '#bb33bb', 2, 0.5, 1.35, true); } /** * Add a line to the map for highlighting the connection between two * markers. * @param {string} attrib - The attribute to use for managing the line * @param {ExtendedLatLng} from - The origin of the line * @param {ExtendedLatLng} to - The destination of the line * @param {string} color - The color of the line * @param {number} weight - The weight of the line * @param {number} opacity - The opacity of the line (0..1) * @param {number} scale - The scale of the arrow-head (pt) * @param {boolean} visible - The visibility of the line * @return {!Object} line - the polyline object created */ function addLine(attrib, from, to, color, weight, opacity, scale, visible) { from[attrib] = new google.maps.Polyline({ path: [from, to], strokeColor: color, strokeWeight: weight, strokeOpacity: opacity, icons:[{ offset: "0%", icon: { scale: scale/*pt*/, path: google.maps.SymbolPath.BACKWARD_CLOSED_ARROW } }] }); from[attrib].setVisible(visible); from[attrib].setMap(map); to[attrib] = from[attrib]; polylines.push(from[attrib]); return from[attrib]; } /** * Add a pair of lines to the map for highlighting the connection between two * markers; one visible while the mouse is over the marker (the "overline"), * the other while it is not (the "outline"). * @param {ExtendedLatLng} from - The origin of the line (the original input) * @param {ExtendedLatLng} to - The destination of the line (the snapped point) * @return {!Object} line - the polyline object created */ function addCorrespondence(coords, marker) { if (!coords.overline) { addOverline(coords, coords.related); } if (!coords.outline) { addOutline(coords, coords.related); } marker.addListener('mouseover', function(mevt) { coords.outline.setVisible(false); coords.overline.setVisible(true); coords.related.marker.setOpacity(1.0); }); marker.addListener('mouseout', function(mevt) { coords.overline.setVisible(false); coords.outline.setVisible(true); coords.related.marker.setOpacity(0.5); }); } /** * Add a marker to the map and check for special 'interpolated' * and 'unsnapped' properties to control which colour marker is used * @param {ExtendedLatLng} coords - Coords of where to add the marker * @return {!Object} marker - the marker object created */ function addMarker(coords) { var marker = new google.maps.Marker({ position: coords, title: coords.lat + ',' + coords.lng, map: map, opacity: 0.5, icon: RED_MARKER }); // Coord should NEVER be interpolated AND unsnapped if (coords.interpolated) { marker.setIcon(BLUE_MARKER); } else if (!coords.related) { marker.setIcon(YELLOW_MARKER); } else if (coords.originalIndex != undefined) { marker.setIcon(RED_MARKER); addCorrespondence(coords, marker); } else { marker.setIcon({url: GREEN_MARKER, scaledSize: {width: 20, height: 20}}); addCorrespondence(coords, marker); } // Make markers change opacity when the mouse scrubs across them marker.addListener('mouseover', function(mevt) { marker.setOpacity(1.0); }); marker.addListener('mouseout', function(mevt) { marker.setOpacity(0.5); }); coords.marker = marker; // Save a reference for easy access later markers.push(marker); return marker; } /** * Animate an icon along a polyline * @param {Object} polyline The line to animate the icon along */ function animateCircle(polyline) { var count = 0; // fallback icon if the poly has no icon to animate var defaultIcon = [ { icon: lineSymbol, offset: '100%' } ]; window.setInterval(function() { count = (count + 1) % 200; var icons = polyline.get('icons') || defaultIcon; icons[0].offset = (count / 2) + '%'; polyline.set('icons', icons); }, 20); } /** * Fit the map bounds to the current set of markers * @param {Array<Object>} markers Array of all map markers */ function fitBounds(markers) { var bounds = new google.maps.LatLngBounds; for (var i = 0; i < markers.length; i++) { bounds.extend(markers[i].getPosition()); } map.fitBounds(bounds); } /** * Uses Places library to get Place Details for a Place ID * @param {string} placeId The Place ID to look up * @param {Function} foundCallback Called if the place is found * @param {Function} missingCallback Called if nothing is found * @param {Function} errorCallback Called if request fails */ function getPlaceDetails(placeId, foundCallback, missingCallback, errorCallback) { var request = { placeId: placeId }; placesService.getDetails(request, function(place, status) { if (status == google.maps.places.PlacesServiceStatus.OK) { foundCallback(place); } else if (status == google.maps.places.PlacesServiceStatus.NOT_FOUND) { missingCallback(); } else if (errorCallback) { errorCallback(); } }); } /** * AJAX request to the Roads Speed Limit API. * Request the speed limit for the Place ID * @param {string} placeId Place ID to request the speed limit for * @param {Function} successCallback Called if request is successful * @param {Function} errorCallback Called if request fails */ function getSpeedLimit(placeId, successCallback, errorCallback) { $.ajax({ type: 'GET', url: SPEED_LIMIT_URL, data: { placeId: placeId, key: API_KEY }, success: successCallback, error: errorCallback }); } /** * Open an infowindow on either the map or the active streetview pano * @param {Object} infowindow Infowindow to be opened * @param {Object} marker Marker the infowindow is anchored to */ function openInfoWindow(infowindow, marker) { // If streetView is visible display the infoWindow over the pano // and anchor to the marker if (map.getStreetView().getVisible()) { infowindow.open(map.getStreetView(), marker); } // Otherwise open it on the map and anchor to the marker else { infowindow.open(map, marker); } } /** * Add event listener to for when the active pano changes */ function listenForPanoChange() { var pano = map.getStreetView(); // Close all open markers when the pano changes google.maps.event.addListener(pano, 'position_changed', function() { closeAllInfoWindows(infoWindows); }); } /** * Close all open infoWindows * @param {Array<Object>} infoWindows - all infowindow objects */ function closeAllInfoWindows(infoWindows) { for (var i = 0; i < infoWindows.length; i++) { infoWindows[i].close(); } }
JavaScript + HTML
<!DOCTYPE html> <html> <head> <title>Roads API Inspector</title> <style type="text/css"> html, body { height: 100%; margin: 0; padding: 0; font-family: Roboto, Noto, sans-serif; } #map { height: 500px; } #interpolate { width: 2em; height: 2em; } #coords { resize: vertical; min-height: 75px; max-height: 200px; } .block { clear: both; margin: 1.5em auto; text-align: center; } #legend { float: center; margin: 5px 15px; font-size: 13px; } .button { display: inline-block; position: relative; border: 0; padding: 0 1.7em; min-width: 120px; height: 32px; line-height: 32px; border-radius: 2px; font-size: 0.9em; background-color: #fff; color: #646464; } .button.narrow { width: 60px; } .button.grey { background-color: #eee; } .button.blue { background-color: #4285f4; color: #fff; } .button.green { background-color: #0f9d58; color: #fff; } .button.raised { transition: box-shadow 0.2s cubic-bezier(0.4, 0, 0.2, 1); transition-delay: 0.2s; box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.26); } .button.raised:active { box-shadow: 0 8px 17px 0 rgba(0, 0, 0, 0.2); transition-delay: 0s; } .floating { position: absolute; top: 10px; right: 10px; z-index: 5; background-color: rgba(255, 255, 255, 0.75); padding: 1px; border: 1px solid #999; text-align: center; line-height: 18px; } .floating.panel { width: 400px; } .coords-small { width: 350px; } .coords-large { width: 400px; } .button-div { padding: 0px 50px; width: 300px; line-height: 40px; } #toggle { width: 25px; z-index: 10; cursor: default; font-size: 2em; padding: 1px; color: #999; display: none; } </style> <script type="text/javascript" src="https://maps.googleapis.com/maps/api/js?libraries=places,geometry&key=AIzaSyAAUHO6lMMnE2VZMRYmAfVbCYCgsEEqNyM"> </script> <script src="https://www.gstatic.com/external_hosted/jquery2.min.js"></script> <script> // Replace with your own API key var API_KEY = 'YOUR_API_KEY'; // Icons for markers var RED_MARKER = 'https://maps.google.com/mapfiles/ms/icons/red-dot.png'; var GREEN_MARKER = 'https://maps.google.com/mapfiles/ms/icons/green-dot.png'; var BLUE_MARKER = 'https://maps.google.com/mapfiles/ms/icons/blue-dot.png'; var YELLOW_MARKER = 'https://maps.google.com/mapfiles/ms/icons/yellow-dot.png'; // URL for places requests var PLACES_URL = 'https://maps.googleapis.com/maps/api/place/details/json?' + 'key=' + API_KEY + '&placeid='; // URL for Speed limits var SPEED_LIMIT_URL = 'https://roads.googleapis.com/v1/speedLimits'; var coords; /** * Current Roads API threshold (subject to change without notice) * @const {number} */ var DISTANCE_THRESHOLD_HIGH = 300; var DISTANCE_THRESHOLD_LOW = 200; /** * @type Array<ExtendedLatLng> */ var originals = []; // the original input points, a list of ExtendedLatLng var interpolate = true; var map; var placesService; var originalCoordsLength; // Settingup Arrays var infoWindows = []; var markers = []; var placeIds = []; var polylines = []; var snappedCoordinates = []; var distPolylines = []; // Symbol that gets animated along the polyline var lineSymbol = { path: google.maps.SymbolPath.CIRCLE, scale: 8, strokeColor: '#005db5', strokeWidth: '#005db5' }; // Example 1 - Frolick around Sydney var eg1 = '-33.870315,151.196532|-33.869979,151.197355|' + '-33.870044,151.197712|-33.870358,151.198206|' + '-33.870595,151.198376|-33.870640,151.198398|' + '-33.870620,151.198449|-33.870951,151.198525|' + '-33.871040,151.198528|-33.872031,151.198413'; // Example 2 - Lap around Canberra var eg2 = '-35.274346,149.130168|-35.278012,149.129583|' + '-35.280329,149.129073|-35.280999,149.129293|' + '-35.281441,149.129846|-35.281945,149.130034|' + '-35.282825,149.129567|-35.283022,149.128811|' + '-35.284734,149.128366'; // Example 3 - Path with unsnappable point var eg3 = '-35.274346,149.094000|-35.278012,149.129583|' + '-35.280329,149.129073|-35.280999,149.129293|' + '-35.281441,149.129846'; // Example 4 - Drive erratically in Elkin var eg4 = '36.28881,-80.8525|36.287038,-80.85313|36.286161,-80.85369|' + '36.28654,-80.85418|36.2846,-80.84766|36.28355,-80.84669'; // Initialize function initialize() { $('#eg1').click(function(e) { $('#coords').val(eg1); $('#plot').trigger('click'); }); $('#eg2').click(function(e) { $('#coords').val(eg2); $('#plot').trigger('click'); }); $('#eg3').click(function(e) { $('#coords').val(eg3); $('#plot').trigger('click'); }); $('#eg4').click(function(e) { $('#coords').val(eg4); $('#plot').trigger('click'); }); $('#toggle').click(function(e) { if ($('#panel').css("display") != 'none') { $('#toggle').html("+"); $('#panel').hide(); } else { $('#toggle').html("—"); $('#panel').show(); } }); // Centre the map on Sydney var mapOptions = { center: {'lat': -33.870315, 'lng': 151.196532}, zoom: 14 }; // Map object map = new google.maps.Map(document.getElementById('map'), mapOptions); // Places object placesService = new google.maps.places.PlacesService(map); // Reset the map to a clean state and reset all variables // used for displaying each request function clearMap() { // Clear the polyline for (var i = 0; i < polylines.length; i++) { polylines[i].setMap(null); } // Clear all markers for (var i = 0; i < markers.length; i++) { markers[i].setMap(null); } // Clear all the distance polylines for (var i = 0; i < distPolylines.length; i++) { distPolylines[i].setMap(null); } // Clear all info windows for (var i = 0; i < infoWindows.length; i++) { infoWindows[i].close(); } // Empty everything polylines = []; markers = []; distPolylines = []; snappedCoordinates = []; placeIds = []; infoWindows = []; $('#unsnappedPoints').empty(); $('#warningMessage').empty(); } // Parse the value in the input element // to get all coordinates function parseCoordsFromQuery(input) { var coords; input = decodeURIComponent(input); if (input.split('path=').length > 1) { input = decodeURIComponent(input); // Split on the ampersand to get all params var parts = input.split('&'); // Check each part to see if it starts with 'path=' // grabbing out the coordinates if it does for (var i = 0; i < parts.length; i++) { if (parts[i].split('path=').length > 1) { coords = parts[i].split('path=')[1]; break; } } } else { coords = decodeURIComponent(input); } // Parse the "Lat,Lng|..." coordinates into an array of ExtendedLatLng originals = []; var points = coords.split('|'); for (var i = 0; i < points.length; i++) { var point = points[i].split(','); originals.push({lat: Number(point[0]), lng: Number(point[1]), index:i}); } return coords; } // Clear the map of any old data and plot the request $('#plot').click(function(e) { clearMap(); bendAndSnap(); drawDistance(); e.preventDefault(); }); // Make AJAX request to the snapToRoadsAPI // with coordinates parsed from text input element. function bendAndSnap() { coords = parseCoordsFromQuery($('#coords').val()); location.hash = coords; $.ajax({ type: 'GET', url: 'https://roads.googleapis.com/v1/snapToRoads', data: { interpolate: $('#interpolate').is(':checked'), key: API_KEY, path: coords }, success: function(data) { $('#requestURL').html('<a target="blank" href="' + this.url + '">Request URL</a>'); processSnapToRoadResponse(data); drawSnappedPolyline(snappedCoordinates); drawOriginals(originals); fitBounds(markers); }, error: function() { $('#requestURL').html('<strong>That query didn\'t work :(</strong>' + '<p>Try looking at the <a href="' + this.url + '">Request URL</a></p>'); clearMap(); } }); } // Toggle the distance polylines of the original points to show on the maps $('#distance').click(function(e) { for (var i = 0; i < distPolylines.length; i++) { distPolylines[i].setVisible(!distPolylines[i].getVisible()); } // Clear all infoWindows associated with distance polygons on toggle for (var i = 0; i < infoWindows.length; i++) { if (infoWindows[i].dist) { infoWindows[i].close(); } } e.preventDefault(); }); /** * Compute the distance between each original point and create a polyline * for each pair. Polylines are initially hidden on creation */ function drawDistance() { for (var i = 0; i < originals.length - 1; i++) { var origin = new google.maps.LatLng(originals[i]); var destination = new google.maps.LatLng(originals[i+1]); var distance = google.maps.geometry.spherical.computeDistanceBetween(origin, destination); // Round the distance value to two decimal places distance = Math.round(distance * 100) / 100; var color; var weight; if (distance > DISTANCE_THRESHOLD_HIGH) { color = '#CC0022'; weight = 7; } else if (distance < DISTANCE_THRESHOLD_HIGH && distance > DISTANCE_THRESHOLD_LOW) { color = '#FF6600'; weight = 6; } else { color = '#22CC00'; weight = 5; } var polyline = new google.maps.Polyline({ strokeColor: color, strokeOpacity: 0.4, strokeWeight: weight, geodesic: true, visible: false, map: map }); polyline.setPath([origin, destination]); distPolylines.push(polyline); infoWindows.push(addPolyWindow(polyline, distance, i)); } } /** * Add an info window to the polyline displaying the original * points and the distance */ function addPolyWindow(polyline, distance, index) { var infoWindow = new google.maps.InfoWindow(); var content = '<div style="width:100%"><p>' + '<strong>Original Index: </strong>' + index + '<br>' + '<strong>Coords:</strong> (' + originals[index].lat + ',' + originals[index].lng + ')' + '<br>to<br>' + '<strong>Original Index: </strong>' + (index+1) + '<br>' + '<strong>Coords:</strong> (' + originals[index+1].lat + ',' + originals[index+1].lng + ')<br><br>' + '<strong>Distance: </strong>' + distance + ' m<br>'; if (distance > DISTANCE_THRESHOLD_HIGH) { content += '<span style="color:#CC0022;font-style:italic">' + '*Large distance (>300m) may affect snapping</span><br>' + 'Please see <a href="https://developers.google.com/maps/' + 'documentation/roads/snap#parameter_usage" ' + 'target="_blank">Roads API documentation</a>'; } content += '</p></div>'; infoWindow.setContent(content); infoWindow.dist = true; polyline.addListener('click', function(e) { infoWindow.setPosition(e.latLng); infoWindow.open(map); }); polyline.addListener('mouseover', function(e) { polyline.setOptions({strokeOpacity: 1.0}); }); polyline.addListener('mouseout', function(e) { polyline.setOptions({strokeOpacity: 0.4}); }); return infoWindow; } // Parse the value in the input element // to get all coordinates function getMissingPoints(originalIndexes, originalCoordsLength) { var unsnappedPoints = []; var coordsArray = coords.split('|'); var hasMissingCoords = false; for (var i = 0; i < originalCoordsLength; i++) { if (originalIndexes.indexOf(i) < 0) { hasMissingCoords = true; var latlng = { 'lat': parseFloat(coordsArray[i].split(',')[0]), 'lng': parseFloat(coordsArray[i].split(',')[1]) }; unsnappedPoints.push(latlng); latlng.unsnapped = true; } } return unsnappedPoints; } // Parse response from snapToRoads API request // Store all coordinates in response // Calls functions to add markers to map for unsnapped coordinates function processSnapToRoadResponse(data) { var originalIndexes = []; var unsnappedMessage = ''; for (var i = 0; i < data.snappedPoints.length; i++) { var latlng = { 'lat': data.snappedPoints[i].location.latitude, 'lng': data.snappedPoints[i].location.longitude }; var interpolated = true; if (data.snappedPoints[i].originalIndex != undefined) { interpolated = false; originalIndexes.push(data.snappedPoints[i].originalIndex); latlng.originalIndex = data.snappedPoints[i].originalIndex; } latlng.interpolated = interpolated; snappedCoordinates.push(latlng); placeIds.push(data.snappedPoints[i].placeId); // Cross-reference the original point and this snapped point. latlng.related = originals[latlng.originalIndex]; originals[latlng.originalIndex].related = latlng; } var unsnappedPoints = getMissingPoints( originalIndexes, coords.split('|').length ); for (var i = 0; i < unsnappedPoints.length; i++) { var marker = addMarker(unsnappedPoints[i]); var infowindow = addBasicInfoWindow(marker, unsnappedPoints[i], i); infoWindows.push(infowindow); unsnappedMessage += unsnappedPoints[i].lat + ',' + unsnappedPoints[i].lng + '<br>'; } if (unsnappedPoints.length) { unsnappedMessage = '<strong>' + 'These points weren\'t snapped: ' + '</strong><br>' + unsnappedMessage; $('#unsnappedPoints').html(unsnappedMessage); } if (data.warningMessage) { $('#warningMessage').html('<span style="color:#CC0022;' + 'font-style:italic;font-size:12px">' + data.warningMessage + '<br/>' + '<a target="_blank" href="https://developers.google.com/maps/' + 'documentation/roads/snap">https://developers.google.com/maps/' + 'documentation/roads/snap</a>'); $('#distance').trigger('click'); } } // Draw the polyline for the snapToRoads API response // Call functions to add markers and infowindows for each snapped // point along the polyline. function drawSnappedPolyline(snappedCoords) { var snappedPolyline = new google.maps.Polyline({ path: snappedCoords, strokeColor: '#005db5', strokeWeight: 6, icons: [{ icon: lineSymbol, offset: '100%' }] }); snappedPolyline.setMap(map); animateCircle(snappedPolyline); polylines.push(snappedPolyline); for (var i = 0; i < snappedCoords.length; i++) { var marker = addMarker(snappedCoords[i]); var infoWindow = addDetailedInfoWindow(marker, snappedCoords[i], placeIds[i]); infoWindows.push(infoWindow); } } // Draw the original input. // Call functions to add markers and infowindows for each point. function drawOriginals(originalCoords) { for (var i = 0; i < originalCoords.length; i++) { var marker = addMarker(originalCoords[i]); var infoWindow = addBasicInfoWindow(marker, originalCoords[i], i); infoWindows.push(infoWindow); } } // Infowindow used for unsnappable coordinates function addBasicInfoWindow(marker, coords, index) { var infowindow = new google.maps.InfoWindow(); var content = '<div style="width:99%"><p>' + '<strong>Lat/Lng:</strong><br>' + '(' + coords.lat + ',' + coords.lng + ')<br>' + (index != undefined ? '<strong>Index: </strong>' + index : '') + '</p></div>'; infowindow.setContent(content); google.maps.event.addListener(marker, 'click', function() { openInfoWindow(infowindow, marker); }); return infowindow; } // Infowindow used for snapped points // Makes request to Places Details API to get data about each // Place ID. // Requests speed limit of each location using Roads SpeedLimit API function addDetailedInfoWindow(marker, coords, placeId) { var infowindow = new google.maps.InfoWindow(); var placesRequestUrl = PLACES_URL + placeId; var detailsUrl = '<a target="_blank" href="' + placesRequestUrl + '">' + placeId + '</a></li>'; // On click we make a request to the Places API // This is to avoid OVER_QUERY_LIMIT if we just requested everything // at the same time google.maps.event.addListener(marker, 'click', function() { content = '<div style="width:99%"><p>'; function finishInfoWindow(placeDetails) { content += '<strong>Place Details: </strong>' + placeDetails + '<br>' + '<strong>' + (coords.interpolated ? 'Coords' : 'Snapped coords') + ': </strong>' + '(' + coords.lat.toFixed(5) + ',' + coords.lng.toFixed(5) + ')<br>'; if (!(coords.interpolated)) { var original = originals[coords.originalIndex]; content += '<strong>Original coords: </strong>' + '(' + original.lat + ',' + original.lng + ')<br>' + '<strong>Original Index: </strong>' + coords.originalIndex; } content += '</p></div>'; infowindow.setContent(content); openInfoWindow(infowindow, marker); }; getPlaceDetails(placeId, function(place) { if (place.name) { content += '<strong>' + place.name + '</strong><br>'; } getSpeedLimit(placeId, function(data) { if (data.speedLimits) { content += '<strong>Speed Limit: </strong>' + data.speedLimits[0].speedLimit + ' km/h <br>'; } finishInfoWindow(detailsUrl); }); }, function() { finishInfoWindow("<em>None available</em>"); }); }); return infowindow; } // Avoid infoWindows staying open if the pano changes listenForPanoChange(); // If the user came to the page with a particular path or URL, // immediately plot it. if (location.hash.length > 1) { coords = parseCoordsFromQuery(location.hash.slice(1)); $('#coords').val(coords); $('#plot').click(); } } // End init function // Call the initialize function once everything has loaded google.maps.event.addDomListener(window, 'load', initialize); // Load the control panel in a floating div if it is not loaded in an iframe // after the textarea has been rendered $("#coords").ready(function() { if (!window.frameElement) { $('#panel').addClass("floating panel"); $('#button-div').addClass("button-div"); $('#coords').removeClass("coords-large").addClass("coords-small"); $('#toggle').show(); $('#map').height('100%'); } }); /** * latlng literal with extra properties to use with the RoadsAPI * @typedef {Object} ExtendedLatLng * lat:string|float * lng:string|float * interpolated:boolean * unsnapped:boolean */ /** * Add a line to the map for highlighting the connection between two * markers while the mouse is over it. * @param {ExtendedLatLng} from - The origin of the line * @param {ExtendedLatLng} to - The destination of the line * @return {!Object} line - the polyline object created */ function addOverline(from, to) { return addLine("overline", from, to, '#ff77ff', 4, 1.0, 2.0, false); } /** * Add a line to the map for highlighting the connection between two * markers while the mouse is NOT over it. * @param {ExtendedLatLng} from - The origin of the line * @param {ExtendedLatLng} to - The destination of the line * @return {!Object} line - the polyline object created */ function addOutline(from, to) { return addLine("outline", from, to, '#bb33bb', 2, 0.5, 1.35, true); } /** * Add a line to the map for highlighting the connection between two * markers. * @param {string} attrib - The attribute to use for managing the line * @param {ExtendedLatLng} from - The origin of the line * @param {ExtendedLatLng} to - The destination of the line * @param {string} color - The color of the line * @param {number} weight - The weight of the line * @param {number} opacity - The opacity of the line (0..1) * @param {number} scale - The scale of the arrow-head (pt) * @param {boolean} visible - The visibility of the line * @return {!Object} line - the polyline object created */ function addLine(attrib, from, to, color, weight, opacity, scale, visible) { from[attrib] = new google.maps.Polyline({ path: [from, to], strokeColor: color, strokeWeight: weight, strokeOpacity: opacity, icons:[{ offset: "0%", icon: { scale: scale/*pt*/, path: google.maps.SymbolPath.BACKWARD_CLOSED_ARROW } }] }); from[attrib].setVisible(visible); from[attrib].setMap(map); to[attrib] = from[attrib]; polylines.push(from[attrib]); return from[attrib]; } /** * Add a pair of lines to the map for highlighting the connection between two * markers; one visible while the mouse is over the marker (the "overline"), * the other while it is not (the "outline"). * @param {ExtendedLatLng} from - The origin of the line (the original input) * @param {ExtendedLatLng} to - The destination of the line (the snapped point) * @return {!Object} line - the polyline object created */ function addCorrespondence(coords, marker) { if (!coords.overline) { addOverline(coords, coords.related); } if (!coords.outline) { addOutline(coords, coords.related); } marker.addListener('mouseover', function(mevt) { coords.outline.setVisible(false); coords.overline.setVisible(true); coords.related.marker.setOpacity(1.0); }); marker.addListener('mouseout', function(mevt) { coords.overline.setVisible(false); coords.outline.setVisible(true); coords.related.marker.setOpacity(0.5); }); } /** * Add a marker to the map and check for special 'interpolated' * and 'unsnapped' properties to control which colour marker is used * @param {ExtendedLatLng} coords - Coords of where to add the marker * @return {!Object} marker - the marker object created */ function addMarker(coords) { var marker = new google.maps.Marker({ position: coords, title: coords.lat + ',' + coords.lng, map: map, opacity: 0.5, icon: RED_MARKER }); // Coord should NEVER be interpolated AND unsnapped if (coords.interpolated) { marker.setIcon(BLUE_MARKER); } else if (!coords.related) { marker.setIcon(YELLOW_MARKER); } else if (coords.originalIndex != undefined) { marker.setIcon(RED_MARKER); addCorrespondence(coords, marker); } else { marker.setIcon({url: GREEN_MARKER, scaledSize: {width: 20, height: 20}}); addCorrespondence(coords, marker); } // Make markers change opacity when the mouse scrubs across them marker.addListener('mouseover', function(mevt) { marker.setOpacity(1.0); }); marker.addListener('mouseout', function(mevt) { marker.setOpacity(0.5); }); coords.marker = marker; // Save a reference for easy access later markers.push(marker); return marker; } /** * Animate an icon along a polyline * @param {Object} polyline The line to animate the icon along */ function animateCircle(polyline) { var count = 0; // fallback icon if the poly has no icon to animate var defaultIcon = [ { icon: lineSymbol, offset: '100%' } ]; window.setInterval(function() { count = (count + 1) % 200; var icons = polyline.get('icons') || defaultIcon; icons[0].offset = (count / 2) + '%'; polyline.set('icons', icons); }, 20); } /** * Fit the map bounds to the current set of markers * @param {Array<Object>} markers Array of all map markers */ function fitBounds(markers) { var bounds = new google.maps.LatLngBounds; for (var i = 0; i < markers.length; i++) { bounds.extend(markers[i].getPosition()); } map.fitBounds(bounds); } /** * Uses Places library to get Place Details for a Place ID * @param {string} placeId The Place ID to look up * @param {Function} foundCallback Called if the place is found * @param {Function} missingCallback Called if nothing is found * @param {Function} errorCallback Called if request fails */ function getPlaceDetails(placeId, foundCallback, missingCallback, errorCallback) { var request = { placeId: placeId }; placesService.getDetails(request, function(place, status) { if (status == google.maps.places.PlacesServiceStatus.OK) { foundCallback(place); } else if (status == google.maps.places.PlacesServiceStatus.NOT_FOUND) { missingCallback(); } else if (errorCallback) { errorCallback(); } }); } /** * AJAX request to the Roads Speed Limit API. * Request the speed limit for the Place ID * @param {string} placeId Place ID to request the speed limit for * @param {Function} successCallback Called if request is successful * @param {Function} errorCallback Called if request fails */ function getSpeedLimit(placeId, successCallback, errorCallback) { $.ajax({ type: 'GET', url: SPEED_LIMIT_URL, data: { placeId: placeId, key: API_KEY }, success: successCallback, error: errorCallback }); } /** * Open an infowindow on either the map or the active streetview pano * @param {Object} infowindow Infowindow to be opened * @param {Object} marker Marker the infowindow is anchored to */ function openInfoWindow(infowindow, marker) { // If streetView is visible display the infoWindow over the pano // and anchor to the marker if (map.getStreetView().getVisible()) { infowindow.open(map.getStreetView(), marker); } // Otherwise open it on the map and anchor to the marker else { infowindow.open(map, marker); } } /** * Add event listener to for when the active pano changes */ function listenForPanoChange() { var pano = map.getStreetView(); // Close all open markers when the pano changes google.maps.event.addListener(pano, 'position_changed', function() { closeAllInfoWindows(infoWindows); }); } /** * Close all open infoWindows * @param {Array<Object>} infoWindows - all infowindow objects */ function closeAllInfoWindows(infoWindows) { for (var i = 0; i < infoWindows.length; i++) { infoWindows[i].close(); } } </script> </head> <body> <div class="floating" id="toggle">—</div> <div id="panel"> <div class="block"> <strong>Sample Queries</strong> <div id="button-div"> <button id="eg1" class="button raised blue">EXAMPLE 1</button> <button id="eg2" class="button raised blue">EXAMPLE 2</button> <button id="eg3" class="button raised blue">EXAMPLE 3</button> <button id="eg4" class="button raised blue">EXAMPLE 4</button> </div> </div> <form id="controls"> <div class="block"> <div> <strong><span id="requestURL">Request URL</span> or Path (Pipe Separated)</strong><br> <textarea id="coords" class="u-full-width coords-large" type="text" placeholder="-35.123,150.332 | 80.654,22.439" id="exampleEmailInput"></textarea> </div> <div> <label>Interpolate: </label> <input for="interpolate" id="interpolate" type="checkbox" checked/> </div> </div> <div> <div class="block"> <button id="plot" class="button raised blue">Plot a Course</button> <button id="distance" class="button raised blue">Toggle Distances</button> </div> <div id="legend"> <img src="https://maps.google.com/mapfiles/ms/icons/green-dot.png" style="height:16px;"> Original <img src="https://maps.google.com/mapfiles/ms/icons/red-dot.png"/> Snapped <img src="https://maps.google.com/mapfiles/ms/icons/blue-dot.png"/> Interpolated <img src="https://maps.google.com/mapfiles/ms/icons/yellow-dot.png"/> Unsnappable </div> <div> <p id="warningMessage"></p> <p id="unsnappedPoints"></p> </div> </div> </form> </div> <div id="map"> </div> </body> </html>