« Back to Index

Google Maps with native HTML5 geolocation + audio

View original Gist on GitHub

Tags: #js

Description.md

We had a meeting with a new client to discuss a mobile app (client had asked for an app that worked on the “iPhone”). This app needed to play back audio depending on the users location (specifically if they were within 5 miles of the relevant location).

We decided that we would try and guide the client to build a mobile web app and so I knocked together a quick prototype of a Google Map that used a JavaScript version of it’s Places API to load McDonald restaurants within 1 mile of the users current location. I used native browser geolocation to determine the users location. I started to include the watchPosition method which would have been used to demonstrate the app tracking our location but wasn’t added in the end because we realised we wouldn’t be able to properly demo that :-)

I dropped in custom markers onto the map and also decided to exclude IE9 from the demo because we had noticed issues with its geolocation algorithm (I checked online for any feedback I could find and apparently Microsofts database of Wifi locations just wasn’t as good as other browsers and so the results were hideously off base).

map.html

<!doctype html>
<html lang="en" dir="ltr">
	<head>
		<meta charset="utf-8">
		<title>Google Map Prototype</title>
		<style type="text/css">
			body {
				margin: 0;
				padding: 0;
			}
			
			/* We use set width/height dimensions because using percentages causes iOS resources to be exhausted!? */
			#map {
				height: 500px;
				left: 50%;
				margin: -225px 0 0 -350px;
				position: absolute;
				top: 50%;
				width: 700px;
			}
			
			#audiofile {
				display: none;
				
				/* iOS devices aren't hiding the element so we use a CSS3 trick to get the element out of view! */
				-webkit-transform: translate(-999em, 0);
				-moz-transform: translate(-999em, 0);
				-o-transform: translate(-999em, 0);
				transform: translate(-999em, 0);
			}
		</style>
	</head>
	<body>
		<audio id="audiofile" controls preload="auto" autobuffer> 
			<source src="sample.mp3" />
			<source src="sample.ogg" />
		</audio>
		<div id="map"></div>
		<!--
			Not using just standard Google Maps
			<script src="http://maps.google.com/maps/api/js?sensor=true"></script>
		-->
		<!--
			Now using Places library which also loads the Maps api
			Reference: https://code.google.com/apis/maps/documentation/javascript/places.html
		-->
		<script src="http://maps.googleapis.com/maps/api/js?libraries=places&sensor=true"></script>
		<script type="text/javascript">
			// Set-up new map instance (no location details specified yet)
			var map = new google.maps.Map(document.getElementById('map'), {
					mapTypeControl: true,
					mapTypeControlOptions: { 
						style: google.maps.MapTypeControlStyle.DROPDOWN_MENU 
					},
					mapTypeId: google.maps.MapTypeId.ROADMAP,
					streetViewControl: true
				}),
				infowindow = new google.maps.InfoWindow(),
				marker,
				latlng,
				watchID,
				audioelement = document.getElementById('audiofile');				
				
			/**
			 * Following property indicates whether the current rendering engine is Trident (i.e. Internet Explorer)
			 * 
			 * @return v { Integer|undefined } if IE then returns the version, otherwise returns 'undefined' to indicate NOT a IE browser
			 */
			var isIE = (function() {
				var undef,
					v = 3,
					div = document.createElement('div'),
					all = div.getElementsByTagName('i');
			
				while (
					div.innerHTML = '<!--[if gt IE ' + (++v) + ']><i></i><![endif]-->',
					all[0]
				);
			
				return v > 4 ? v : undef;	
			}());
			
			var whichPrefix = (function(){
				// check if the browser supports CSS animation
				var temp = document.createElement('div'),
					prefixes = 'Webkit Moz O ms Khtml'.split(' '),
					prefix = false;
				
				for(var i = 0, len = prefixes.length; i < len; i++) {
					if(temp.style[prefixes[i] + 'Transform'] !== undefined) {
						prefix = prefixes[i];
						break;
					}
				}
				
				return prefix;
			}());
			
			function isHostMethod(object, property) {
				var type = typeof object[property];
	
				return type == 'function' || // For Safari 3 typeof result being 'function' instead of 'object'
					   (type == 'object' && !!object[property]) || // Protect against ES3 'null' typeof result being 'object'
					   type == 'unknown' || // For IE < 9 when Microsoft used ActiveX objects for Native Functions
					   type == 'string'; // typeof for 'document.body[outerHTML]' results in 'string'
			}
			
			function isHostObject(object, property) {
				// object[property] protects against ES3 specification which allows null to be typeof 'object'
				// so we check if 'object' is returned and that object[property] coerces to true
				// then we group both checks (&& operator returns 2nd expression if 1st expression evaluates to true) and convert result into boolean
				return !!(typeof(object[property]) == 'object' && object[property]);
			}
			
			function createMarker(place) {
				var placeLoc = place.geometry.location,
					logo = new google.maps.MarkerImage('marker-mcdonalds.png', new google.maps.Size(73,75), new google.maps.Point(0,0)),
					marker = new google.maps.Marker({
						map: map,
						position: place.geometry.location,
						icon: logo
					}),
					content = '<strong>' + place.name + '</strong><br>' + place.vicinity + '<br><a href="#" id="audiolink">Show audio player</a><br><br>';
				
				google.maps.event.addListener(marker, 'click', function() {
					infowindow.setContent(content);
					infowindow.open(map, this);
				});
			}
			
			function processLocation(position) {
				// Get position
				var lat = position.coords.latitude,
					lng = position.coords.longitude,
					latlng = new google.maps.LatLng(lat, lng);
				
				// Set map location
				map.setOptions({
		        	center: latlng,
		        	scrollwheel: false,
		        	zoom: 12
		        });
		        
		        // Add marker to map
		        marker = new google.maps.Marker({
					position: latlng,
					map: map,
					title: 'Test Title'
				});
				
				// Event listener for users current location marker
				google.maps.event.addListener(marker, 'click', function() {
					infowindow.setContent('This is your current location!<br>We\'re now showing you all the McDonald\'s in a 5 mile radius');
					infowindow.open(map, this);
				});
				
				// Open the window when the app has loaded
				google.maps.event.trigger(marker, 'click', function() {
					infowindow.setContent('This is your current location!<br>We\'re now showing you all the McDonald\'s in a 5 mile radius');
					infowindow.open(map, this);
				});
				
				// Request any McDonald's within 1 mile (in meters) from the current location
				var request = {
					location: latlng,
					radius: 8046.72, // 5 miles => http://www.unitconversion.org/length/meters-to-miles-conversion.html
					types: ['food'],
					name: 'McDonald'
				};
				
				// Create the Places request
				var service = new google.maps.places.PlacesService(map);
					service.search(request, function(results, status){
						if (status == google.maps.places.PlacesServiceStatus.OK) {
							for (var i = 0; i < results.length; i++) {
								var place = results[i];
								createMarker(results[i]); 
							}
						}
					});
			}
			
			// Doesn't appear to be executed??
			function handleLocationErrors(err) {
				switch(err.code) {
					case err.PERMISSION_DENIED:
						alert('You have decided not to share your location information');
						break;
					case err.POSITION_UNAVAILABLE:
						alert('I\'m sorry but we could not detect your location');
						break;
					case err.TIMEOUT:
						alert('I\'m sorry but the system timed out while waiting to retrieve your location information');
						break;
					default:
						alert('I\'m sorry but an unknown error occurred');
						break;
				}
			}
			
			// Handle audio playback
			function handleAudio(e) {
				var targ = e.target,
					audio;
				
				if (targ.id === 'audiolink') {
					// Make a copy of the <audio> element hidden in the page
					audio = audioelement.cloneNode(true);
					
					// Remove the link and replace with an audio tag
					targ.parentNode.replaceChild(audio, targ);
					audio.style.display = 'block';
					
					if (whichPrefix !== false) {
						audio.style[whichPrefix + 'Transform'] = 'translate(0, 0)';
					}
				}
			}
			
			// Bind Event Delegation to links
			document.body.addEventListener('click', handleAudio, false);
			
			// Because of IE9's rubbish implementation of geolocation makes it as useful as the basic ip address lookup polyfills (so not very useful)
			if(isHostObject(navigator, 'geolocation') || isIE > 9) {
				// This will ask the user to authorise the request for their location (only if geolocation is natively supported)
				navigator.geolocation.getCurrentPosition(processLocation, handleLocationErrors);
				
				// If the user starts moving around then watch their position
				watchID = navigator.geolocation.watchPosition(function(){
					// DO SOMETHING
				});
			} else {
				alert('I\'m sorry, your device isn\'t capable of supporting the geolocation api which is required for this application to work correctly');
			}
		</script>
	</body>
</html>