Pin My Friends Application (using Google Maps and Facebook APIs) – Part 3: Displaying the friends on the map


This part will handle the displaying of the user’s friends as markers on the map:

  • T.6 – Display makers on the map for each friend that has a location
  • T.7 – Display the friend profile when when the mouse hovers the marker
  • T.8 – Handle the friends with no location

T.6 – Display makers on the map for each friend that has a location

  1. In order to display a marker on the Google Map we need the Latitude and Longitude for the marker’s position. We can get the Latitude and Longitude in two ways:
    • Make and additional call to using the Facebook API to get more details about the location object.  By using the id of the location object we can get another object that has a location property with latitude and longitude <- easy but boring
    • Use the Google Geocoding API to get the latitude and longitude based on the location name property <- a little harder but we learn something new in the process. This option is better not just because is something different to learn but because the Geocoder will unify locations like these:
      • Bucharest, Romania
      • Bukarest, Bucharest, Romania
      • București
  2. Before jumping in let’s think for a moment. We have lots of friends in the same location so instead of looping over the entire list of friends and Geocode each location it will be better if we group the friends by their location. In this way we can use the Geocoder to get the latitude and longitude for the unique address list (so my 86 friends will be grouped in 7 unique address groups + 1 for friends that don’t have any address specified). That’s a huge performance boost because the Geocoder if called in a loop will return OVER_QUERY_LIMIT unless the request aren’t timmed to ~1 second apart (that’s 7s down for 86s):
    //Group the friends by their location
    var uniqueLocations = {};
    var param = 'location';
    $.each(friends, function () {
        if (!uniqueLocations[this[param]])
            uniqueLocations[this[param]] = [];
        uniqueLocations[this[param]].push(this);
    });
    
    //Display the friends on the map
    for (var locationName in uniqueLocations) {
        if (locationName != "Location N/A") {
            (function (location, friends) {
                setTimeout(function () { displayFriendsMarkers(location, friends) }, 1000); 
                })(locationName, uniqueLocations[locationName]);
        }
    }
  3. Now that we have the friends grouped by their location the next step is to implement the displayFriendsMarkers function:
    //TODO: Proper error handling (what happens if geocoding fails?)
    function displayFriendsMarkers(locationName, friends) {
        //Call the geocoder to get the latitude and longitude for the locationName
        var geocoder = new google.maps.Geocoder();
        geocoder.geocode({ address: locationName }, function (results, status) {
            if (status == google.maps.GeocoderStatus.ZERO_RESULTS) {
                return
            }
            if (status != google.maps.GeocoderStatus.OK) {
                //alert("We're having difficulties, could you please try again in a moment? " + locationName);
                return
            }
    
            for (var i = 0; i < friends.length; i++) {
                var friend = friends[i];
                //Set the result from of geocoding
                friend.latlng = results[0].geometry.location;
                //Display the marker
                plotMarker(createMarkerOptions(friend));
            }
        });
    }
  4. At this point we have the latitude and longitude for each friend that has a location specified. We have now all the details we need to create a marker for each friend but first the option object  used to create the marker need to be initialized:
    function createMarkerOptions(friend) {
    
        var lat = friend.latlng.lat();
        var lng = friend.latlng.lng();
    
        //Distribute the markers with identical locations 
        //Dangerous Voodoo magic- don't try this at home :)
        lat += parseFloat(((Math.floor(Math.random() * 200) - 99) / 5000).toString() + friend.id);
        lng += parseFloat(((Math.floor(Math.random() * 200) - 99) / 5000).toString() + friend.id);
        friend.latlng = new google.maps.LatLng(lat, lng);
    
        var image = null;
        if (friend.gender == "M") {
            image = new google.maps.MarkerImage(
                "http://www.google.com/intl/en_us/mapfiles/ms/micons/blue-dot.png"
            );
        }
        else if (friend.gender == "F") {
            image = new google.maps.MarkerImage(
                "http://www.google.com/intl/en_us/mapfiles/ms/micons/red-dot.png"
            );
        }
        else {
            image = new google.maps.MarkerImage(
                "http://www.google.com/intl/en_us/mapfiles/ms/micons/yellow-dot.png"
            );
        }
    
        var options = {
            position: friend.latlng,
            icon: image,
            title: friend.name
        };
    
        return options;
    }

    There are tow things happening here. One is related to the fact that for a particular location all friends will have the same latitude and longitude values so all the markers will stack up on each other. So I had to do a quick and dirty algorithm to spread the markers around the original position (it is visible at higher zoom levels). The second things deals with setting the marker icon depending on gender (male: blue, female: red, unspecified: yellow).

  5. The plotMarker function is very simple at this point:
    function plotMarker(options) {
        var marker = new google.maps.Marker({ map: map });
        marker.setOptions(options);
        markersArray.push(marker);
    
        return marker;
    }
    

    Figure 3-1. Markers at the original zoom level

    Figure 3-2. Markers at a higher zoom level

  6. I’m not a very big fun of the default Google Map icons so let’s replace them with something that looks better. There is a nice map icons collection here: and in combination with the Google Map Custom Marker Maker we get the following:
    function createMarkerOptions(friend) {
    
        var lat = friend.latlng.lat();
        var lng = friend.latlng.lng();
    
        //Distribute the markers with identical locations 
        //Dangeours Woodoo magic- don't try this at home :)
        lat += parseFloat(((Math.floor(Math.random() * 200) - 99) / 5000).toString() + friend.id);
        lng += parseFloat(((Math.floor(Math.random() * 200) - 99) / 5000).toString() + friend.id);
        friend.latlng = new google.maps.LatLng(lat, lng);
    
        var image = null;
        var shadow = new google.maps.MarkerImage(
            'Content/images/shadow.png',
            new google.maps.Size(54, 37),
            new google.maps.Point(0, 0),
            new google.maps.Point(16, 37)
        );
        shape = {
            coord: [30, 0, 31, 1, 31, 2, 31, 3, 31, 4, 31, 5, 31, 6, 31, 7, 31, 8, 31, 9, 31, 10, 31, 11, 31, 12, 31, 13, 31, 14, 31, 15, 31, 16, 31, 17, 31, 18, 31, 19, 31, 20, 31, 21, 31, 22, 31, 23, 31, 24, 31, 25, 31, 26, 31, 27, 31, 28, 31, 29, 31, 30, 30, 31, 24, 32, 23, 33, 22, 34, 21, 35, 20, 36, 11, 36, 10, 35, 9, 34, 8, 33, 7, 32, 1, 31, 0, 30, 0, 29, 0, 28, 0, 27, 0, 26, 0, 25, 0, 24, 0, 23, 0, 22, 0, 21, 0, 20, 0, 19, 0, 18, 0, 17, 0, 16, 0, 15, 0, 14, 0, 13, 0, 12, 0, 11, 0, 10, 0, 9, 0, 8, 0, 7, 0, 6, 0, 5, 0, 4, 0, 3, 0, 2, 0, 1, 0, 0, 30, 0],
            type: 'poly'
        };
        if (friend.gender == "M") {
            image = new google.maps.MarkerImage(
                'Content/images/male.png',
                new google.maps.Size(32, 37),
                new google.maps.Point(0, 0),
                new google.maps.Point(16, 37)
            );
        }
        else if (friend.gender == "F") {
            image = new google.maps.MarkerImage(
                'Content/images/female.png',
                new google.maps.Size(32, 37),
                new google.maps.Point(0, 0),
                new google.maps.Point(16, 37)
            );
        }
        else {
            image = new google.maps.MarkerImage(
                'Content/images/unknown.png',
                new google.maps.Size(32, 37),
                new google.maps.Point(0, 0),
                new google.maps.Point(16, 37)
            );
        }
    
        var options = {
            position: friend.latlng,
            icon: image,
            shadow: shadow,
            shape: shape,
            content: friend
        };
    
        return options;
    }
    

    Figure 3-3. Markers at the original zoom level (new icons)

    Figure 3-4. Markers at a higher zoom level (new icons). Notice that the position is different because we use random in the spreading algorithm

T.7 – Display the friend profile when when the mouse hovers the marker

  1. You may have noticed that in the last createMarkerOptions function version the title is gone from the options object and that content: friend is there. We’re going to reuse the template for displaying the friends list (with little modifications) to show the friend profile when the mouse is over the marker:
    function plotMarker(options) {
        var marker = new google.maps.Marker({ map: map });
        marker.setOptions(options);
        markersArray.push(marker);
        google.maps.event.addListener(marker, "mouseover", function () {
            //there is no way to get the marker position (screen coordinates) in v3
            //create a dummy overlay and get the position from its projection
            var helper = new google.maps.OverlayView();
            helper.setMap(map);
            helper.draw = function () {
                if (!this.ready) {
                    this.ready = true;
                    google.maps.event.trigger(this, 'ready');
                }
            };
    
            var pixelpos = helper.getProjection().fromLatLngToContainerPixel(marker.getPosition());
            showFriendProfile(marker.content, pixelpos.x, pixelpos.y);
        });
    
        google.maps.event.addListener(marker, "mouseout", function () {
            hideFriendProfile();
        });
    
        return marker;
    }

    One thing to note here is the hack that needs to be used to transform from world (map) to screen coordinates. This is something that I’ve picked up from the Google Maps API bug and feature requests site (see the References section) 

  2. The showFriendProfile and hideFriendProfile functions
    function showFriendProfile(friend, x, y) {
        $('.friendContainer')
            .html($('#friendTemplate').render(friend))
            .css('left', x)
            .css('top', y)
            .show();
    }
    
    function hideFriendProfile() {
        $('.friendContainer').hide();
    }
  3. Some cleaning up functions
    function removeMarkers() {
        while (marker = markersArray.pop()) {
            marker.setMap(null);
        }
    }
    
    function clearFriends() {
        removeMarkers();
    }
  4. The result:

    Figure 3-5. Friend profile showing on mouse over.

T.8 – Handle the friends with no location

Well my time is up. The 36h of coding are gone and I have to admit that I’m a bit rusty. I was hopping to do more in this time:
  • Handle the friends with no location (and the ones where geocoding fails). I had in mind a collapsible panel at the bottom of the screen with all name and pictures for all of them. Clicking a friend picture will give the user the opportunity to post a message to the friend wall and ask him/her to complete and address (or to correct the address)
  • Improve the performance by saving the friends data in the database and only add differences
  • Use the Facebook Real Time Updates and subscribe to modifications (for name, picture and location). Create an endpoint where Facebook can post the ID’s of modified friends. Then do a request and update the modifications in the database
  • Use WebSockets to show the modifications in real time if the user is connected
I promise I will come back and create blog posts for each of the above.

References

Javascript Closures | Google Maps API bug and feature requests (Issue 2342)

Download

Download Part 3 Code

No Comments