Roads API Inspector adalah alat interaktif yang dapat Anda gunakan untuk mencoba Roads API. Berikut beberapa saran untuk memulai alat ini:
- Salin URL permintaan Roads API ke kolom teks dan pilih Plot a Course, untuk menjelajahi hasil permintaan. Anda tidak perlu menyertakan kunci API.
- Gunakan contoh unggulan untuk bereksperimen dengan API.
- Pilih penanda untuk titik tertentu, untuk melihat jendela info dengan detail tentang titik tersebut.
- Buka Street View setelah memuat salah satu contoh untuk melihat penanda di panorama Street View.
- Alihkan setelan Interpolate untuk melihat pengaruhnya terhadap hasil.
- Pilih Toggle Distances untuk melihat atau menyembunyikan jarak antara dua titik asli. Rute yang tidak tersambung akan ditampilkan sebagai garis hijau lurus dari titik ke titik. Pilih garis untuk melihat jarak.
Contoh yang disediakan adalah:
- Contoh 1: Titik sepanjang jalan di Pyrmont, Sydney.
- Contoh 2: Titik sepanjang jalan di Canberra, Australian Capital Territory.
- Contoh 3: Titik di sepanjang jalan di Canberra dengan titik yang tidak dapat disambungkan ke jalan.
- Contoh 4: Rute di Elkin, NC, dengan jalur yang tidak menentu yang dengan baik menunjukkan hasil dari mengalihkan setelan interpolasi.
Lihat contoh ini dalam layar penuh.
Cobalah sendiri
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